ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç PK-B\<44externallib.phpnu[. use mod_forum\local\exporters\post as post_exporter; use mod_forum\local\exporters\discussion as discussion_exporter; use core_external\external_api; use core_external\external_files; use core_external\external_format_value; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use core_external\util as external_util; /** * External forum API * * @package mod_forum * @copyright 2012 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_forum_external extends external_api { /** * Describes the parameters for get_forum. * * @return external_function_parameters * @since Moodle 2.5 */ public static function get_forums_by_courses_parameters() { return new external_function_parameters ( array( 'courseids' => new external_multiple_structure(new external_value(PARAM_INT, 'course ID', VALUE_REQUIRED, '', NULL_NOT_ALLOWED), 'Array of Course IDs', VALUE_DEFAULT, array()), ) ); } /** * Returns a list of forums in a provided list of courses, * if no list is provided all forums that the user can view * will be returned. * * @param array $courseids the course ids * @return array the forum details * @since Moodle 2.5 */ public static function get_forums_by_courses($courseids = array()) { global $CFG; require_once($CFG->dirroot . "/mod/forum/lib.php"); $params = self::validate_parameters(self::get_forums_by_courses_parameters(), array('courseids' => $courseids)); $courses = array(); if (empty($params['courseids'])) { $courses = enrol_get_my_courses(); $params['courseids'] = array_keys($courses); } // Array to store the forums to return. $arrforums = array(); $warnings = array(); // Ensure there are courseids to loop through. if (!empty($params['courseids'])) { list($courses, $warnings) = external_util::validate_courses($params['courseids'], $courses); // Get the forums in this course. This function checks users visibility permissions. $forums = get_all_instances_in_courses("forum", $courses); foreach ($forums as $forum) { $course = $courses[$forum->course]; $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id); $context = context_module::instance($cm->id); // Skip forums we are not allowed to see discussions. if (!has_capability('mod/forum:viewdiscussion', $context)) { continue; } $forum->name = \core_external\util::format_string($forum->name, $context); // Format the intro before being returning using the format setting. $options = array('noclean' => true); [$forum->intro, $forum->introformat] = \core_external\util::format_text( $forum->intro, $forum->introformat, $context, 'mod_forum', 'intro', null, $options ); $forum->introfiles = external_util::get_area_files($context->id, 'mod_forum', 'intro', false, false); $forum->lang = clean_param($forum->lang, PARAM_LANG); // Discussions count. This function does static request cache. $forum->numdiscussions = forum_count_discussions($forum, $cm, $course); $forum->cmid = $forum->coursemodule; $forum->cancreatediscussions = forum_user_can_post_discussion($forum, null, -1, $cm, $context); $forum->istracked = forum_tp_is_tracked($forum); if ($forum->istracked) { $forum->unreadpostscount = forum_tp_count_forum_unread_posts($cm, $course); } // Add the forum to the array to return. $arrforums[$forum->id] = $forum; } } return $arrforums; } /** * Describes the get_forum return value. * * @return external_single_structure * @since Moodle 2.5 */ public static function get_forums_by_courses_returns() { // This should be using helper_for_get_mods_by_courses::standard_coursemodule_elements_returns, but it is so horribly // inconsistent with all similar web serviecs in other modules that we just can't. // Also, the return type declaration is wrong, but I am not changing it now because I don't want ot break things. return new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'Forum id'), 'course' => new external_value(PARAM_INT, 'Course id'), 'type' => new external_value(PARAM_TEXT, 'The forum type'), 'name' => new external_value(PARAM_RAW, 'Forum name'), 'intro' => new external_value(PARAM_RAW, 'The forum intro'), 'introformat' => new external_format_value('intro'), 'introfiles' => new external_files('Files in the introduction text', VALUE_OPTIONAL), 'lang' => new external_value(PARAM_SAFEDIR, 'Forced activity language', VALUE_OPTIONAL), 'duedate' => new external_value(PARAM_INT, 'duedate for the user', VALUE_OPTIONAL), 'cutoffdate' => new external_value(PARAM_INT, 'cutoffdate for the user', VALUE_OPTIONAL), 'assessed' => new external_value(PARAM_INT, 'Aggregate type'), 'assesstimestart' => new external_value(PARAM_INT, 'Assess start time'), 'assesstimefinish' => new external_value(PARAM_INT, 'Assess finish time'), 'scale' => new external_value(PARAM_INT, 'Scale'), 'grade_forum' => new external_value(PARAM_INT, 'Whole forum grade'), 'grade_forum_notify' => new external_value(PARAM_INT, 'Whether to send notifications to students upon grading by default'), 'maxbytes' => new external_value(PARAM_INT, 'Maximum attachment size'), 'maxattachments' => new external_value(PARAM_INT, 'Maximum number of attachments'), 'forcesubscribe' => new external_value(PARAM_INT, 'Force users to subscribe'), 'trackingtype' => new external_value(PARAM_INT, 'Subscription mode'), 'rsstype' => new external_value(PARAM_INT, 'RSS feed for this activity'), 'rssarticles' => new external_value(PARAM_INT, 'Number of RSS recent articles'), 'timemodified' => new external_value(PARAM_INT, 'Time modified'), 'warnafter' => new external_value(PARAM_INT, 'Post threshold for warning'), 'blockafter' => new external_value(PARAM_INT, 'Post threshold for blocking'), 'blockperiod' => new external_value(PARAM_INT, 'Time period for blocking'), 'completiondiscussions' => new external_value(PARAM_INT, 'Student must create discussions'), 'completionreplies' => new external_value(PARAM_INT, 'Student must post replies'), 'completionposts' => new external_value(PARAM_INT, 'Student must post discussions or replies'), 'cmid' => new external_value(PARAM_INT, 'Course module id'), 'numdiscussions' => new external_value(PARAM_INT, 'Number of discussions in the forum', VALUE_OPTIONAL), 'cancreatediscussions' => new external_value(PARAM_BOOL, 'If the user can create discussions', VALUE_OPTIONAL), 'lockdiscussionafter' => new external_value(PARAM_INT, 'After what period a discussion is locked', VALUE_OPTIONAL), 'istracked' => new external_value(PARAM_BOOL, 'If the user is tracking the forum', VALUE_OPTIONAL), 'unreadpostscount' => new external_value(PARAM_INT, 'The number of unread posts for tracked forums', VALUE_OPTIONAL), ), 'forum' ) ); } /** * Get the forum posts in the specified discussion. * * @param int $discussionid * @param string $sortby * @param string $sortdirection * @param bool $includeinlineattachments Whether inline attachments should be included or not. * @return array */ public static function get_discussion_posts(int $discussionid, ?string $sortby, ?string $sortdirection, bool $includeinlineattachments = false) { global $USER; // Validate the parameter. $params = self::validate_parameters(self::get_discussion_posts_parameters(), [ 'discussionid' => $discussionid, 'sortby' => $sortby, 'sortdirection' => $sortdirection, ]); $warnings = []; $vaultfactory = mod_forum\local\container::get_vault_factory(); $discussionvault = $vaultfactory->get_discussion_vault(); $discussion = $discussionvault->get_from_id($params['discussionid']); $forumvault = $vaultfactory->get_forum_vault(); $forum = $forumvault->get_from_id($discussion->get_forum_id()); $context = $forum->get_context(); self::validate_context($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)); } $orderbysql = "{$sortby} {$sortdirection}"; if (!empty($sortby) && $sortby != 'id') { $orderbysql .= ", id {$sortdirection}"; } $managerfactory = mod_forum\local\container::get_manager_factory(); $capabilitymanager = $managerfactory->get_capability_manager($forum); $postvault = $vaultfactory->get_post_vault(); $posts = $postvault->get_from_discussion_id( $USER, $discussion->get_id(), $capabilitymanager->can_view_any_private_reply($USER), $orderbysql ); $builderfactory = mod_forum\local\container::get_builder_factory(); $postbuilder = $builderfactory->get_exported_posts_builder(); $legacydatamapper = mod_forum\local\container::get_legacy_data_mapper_factory(); return [ 'posts' => $postbuilder->build($USER, [$forum], [$discussion], $posts, $includeinlineattachments), 'forumid' => $discussion->get_forum_id(), 'courseid' => $discussion->get_course_id(), 'ratinginfo' => \core_rating\external\util::get_rating_info( $legacydatamapper->get_forum_data_mapper()->to_legacy_object($forum), $forum->get_context(), 'mod_forum', 'post', $legacydatamapper->get_post_data_mapper()->to_legacy_objects($posts) ), 'warnings' => $warnings, ]; } /** * Describe the post parameters. * * @return external_function_parameters */ public static function get_discussion_posts_parameters() { return new external_function_parameters ([ 'discussionid' => new external_value(PARAM_INT, 'The ID of the discussion from which to fetch posts.', 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'), 'includeinlineattachments' => new external_value(PARAM_BOOL, 'Whether inline attachments should be included or not', VALUE_DEFAULT, false), ]); } /** * Describe the post return format. * * @return external_single_structure */ public static function get_discussion_posts_returns() { return new external_single_structure([ 'posts' => new external_multiple_structure(\mod_forum\local\exporters\post::get_read_structure()), 'forumid' => new external_value(PARAM_INT, 'The forum id'), 'courseid' => new external_value(PARAM_INT, 'The forum course id'), 'ratinginfo' => \core_rating\external\util::external_ratings_structure(), 'warnings' => new external_warnings() ]); } /** * Describes the parameters for get_forum_discussions. * * @return external_function_parameters * @since Moodle 3.7 */ public static function get_forum_discussions_parameters() { return new external_function_parameters ( array( 'forumid' => new external_value(PARAM_INT, 'forum instance id', VALUE_REQUIRED), 'sortorder' => new external_value(PARAM_INT, 'sort by this element: numreplies, , created or timemodified', VALUE_DEFAULT, -1), 'page' => new external_value(PARAM_INT, 'current page', VALUE_DEFAULT, -1), 'perpage' => new external_value(PARAM_INT, 'items per page', VALUE_DEFAULT, 0), 'groupid' => new external_value(PARAM_INT, 'group id', VALUE_DEFAULT, 0), ) ); } /** * Returns a list of forum discussions optionally sorted and paginated. * * @param int $forumid the forum instance id * @param int $sortorder The sort order * @param int $page page number * @param int $perpage items per page * @param int $groupid the user course group * * * @return array the forum discussion details including warnings * @since Moodle 3.7 */ public static function get_forum_discussions(int $forumid, ?int $sortorder = -1, ?int $page = -1, ?int $perpage = 0, ?int $groupid = 0) { global $CFG, $DB, $USER; require_once($CFG->dirroot . "/mod/forum/lib.php"); $warnings = array(); $discussions = array(); $params = self::validate_parameters(self::get_forum_discussions_parameters(), array( 'forumid' => $forumid, 'sortorder' => $sortorder, 'page' => $page, 'perpage' => $perpage, 'groupid' => $groupid ) ); // Compact/extract functions are not recommended. $forumid = $params['forumid']; $sortorder = $params['sortorder']; $page = $params['page']; $perpage = $params['perpage']; $groupid = $params['groupid']; $vaultfactory = \mod_forum\local\container::get_vault_factory(); $discussionlistvault = $vaultfactory->get_discussions_in_forum_vault(); $sortallowedvalues = array( $discussionlistvault::SORTORDER_LASTPOST_DESC, $discussionlistvault::SORTORDER_LASTPOST_ASC, $discussionlistvault::SORTORDER_CREATED_DESC, $discussionlistvault::SORTORDER_CREATED_ASC, $discussionlistvault::SORTORDER_REPLIES_DESC, $discussionlistvault::SORTORDER_REPLIES_ASC ); // If sortorder not defined set a default one. if ($sortorder == -1) { $sortorder = $discussionlistvault::SORTORDER_LASTPOST_DESC; } if (!in_array($sortorder, $sortallowedvalues)) { throw new invalid_parameter_exception('Invalid value for sortorder parameter (value: ' . $sortorder . '),' . ' allowed values are: ' . implode(',', $sortallowedvalues)); } $managerfactory = \mod_forum\local\container::get_manager_factory(); $urlfactory = \mod_forum\local\container::get_url_factory(); $legacydatamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory(); $forumvault = $vaultfactory->get_forum_vault(); $forum = $forumvault->get_from_id($forumid); if (!$forum) { throw new \moodle_exception("Unable to find forum with id {$forumid}"); } $forumdatamapper = $legacydatamapperfactory->get_forum_data_mapper(); $forumrecord = $forumdatamapper->to_legacy_object($forum); $capabilitymanager = $managerfactory->get_capability_manager($forum); $course = $DB->get_record('course', array('id' => $forum->get_course_id()), '*', MUST_EXIST); $cm = get_coursemodule_from_instance('forum', $forum->get_id(), $course->id, false, MUST_EXIST); // Validate the module context. It checks everything that affects the module visibility (including groupings, etc..). $modcontext = context_module::instance($cm->id); self::validate_context($modcontext); $canseeanyprivatereply = $capabilitymanager->can_view_any_private_reply($USER); // Check they have the view forum capability. if (!$capabilitymanager->can_view_discussions($USER)) { throw new moodle_exception('noviewdiscussionspermission', 'forum'); } $alldiscussions = mod_forum_get_discussion_summaries($forum, $USER, $groupid, $sortorder, $page, $perpage); if ($alldiscussions) { $discussionids = array_keys($alldiscussions); $postvault = $vaultfactory->get_post_vault(); $postdatamapper = $legacydatamapperfactory->get_post_data_mapper(); // Return the reply count for each discussion in a given forum. $replies = $postvault->get_reply_count_for_discussion_ids($USER, $discussionids, $canseeanyprivatereply); // Return the first post for each discussion in a given forum. $firstposts = $postvault->get_first_post_for_discussion_ids($discussionids); // Get the unreads array, this takes a forum id and returns data for all discussions. $unreads = array(); if ($cantrack = forum_tp_can_track_forums($forumrecord)) { if ($forumtracked = forum_tp_is_tracked($forumrecord)) { $unreads = $postvault->get_unread_count_for_discussion_ids($USER, $discussionids, $canseeanyprivatereply); } } $canlock = $capabilitymanager->can_manage_forum($USER); $usercontext = context_user::instance($USER->id); $ufservice = core_favourites\service_factory::get_service_for_user_context($usercontext); $canfavourite = has_capability('mod/forum:cantogglefavourite', $modcontext, $USER); foreach ($alldiscussions as $discussionsummary) { $discussion = $discussionsummary->get_discussion(); $firstpostauthor = $discussionsummary->get_first_post_author(); $latestpostauthor = $discussionsummary->get_latest_post_author(); // This function checks for qanda forums. $canviewdiscussion = $capabilitymanager->can_view_discussion($USER, $discussion); if (!$canviewdiscussion) { $warning = array(); // Function forum_get_discussions returns forum_posts ids not forum_discussions ones. $warning['item'] = 'post'; $warning['itemid'] = $discussion->get_id(); $warning['warningcode'] = '1'; $warning['message'] = 'You can\'t see this discussion'; $warnings[] = $warning; continue; } $firstpost = $firstposts[$discussion->get_first_post_id()]; $discussionobject = $postdatamapper->to_legacy_object($firstpost); // Fix up the types for these properties. $discussionobject->mailed = $discussionobject->mailed ? 1 : 0; $discussionobject->messagetrust = $discussionobject->messagetrust ? 1 : 0; $discussionobject->mailnow = $discussionobject->mailnow ? 1 : 0; $discussionobject->groupid = $discussion->get_group_id(); $discussionobject->timemodified = $discussion->get_time_modified(); $discussionobject->usermodified = $discussion->get_user_modified(); $discussionobject->timestart = $discussion->get_time_start(); $discussionobject->timeend = $discussion->get_time_end(); $discussionobject->pinned = $discussion->is_pinned(); $discussionobject->numunread = 0; if ($cantrack && $forumtracked) { if (isset($unreads[$discussion->get_id()])) { $discussionobject->numunread = (int) $unreads[$discussion->get_id()]; } } $discussionobject->numreplies = 0; if (!empty($replies[$discussion->get_id()])) { $discussionobject->numreplies = (int) $replies[$discussion->get_id()]; } $discussionobject->name = \core_external\util::format_string($discussion->get_name(), $modcontext); $discussionobject->subject = \core_external\util::format_string($discussionobject->subject, $modcontext); // Rewrite embedded images URLs. $options = array('trusted' => $discussionobject->messagetrust); list($discussionobject->message, $discussionobject->messageformat) = \core_external\util::format_text($discussionobject->message, $discussionobject->messageformat, $modcontext, 'mod_forum', 'post', $discussionobject->id, $options); // List attachments. if (!empty($discussionobject->attachment)) { $discussionobject->attachments = external_util::get_area_files($modcontext->id, 'mod_forum', 'attachment', $discussionobject->id); } $messageinlinefiles = external_util::get_area_files($modcontext->id, 'mod_forum', 'post', $discussionobject->id); if (!empty($messageinlinefiles)) { $discussionobject->messageinlinefiles = $messageinlinefiles; } $discussionobject->locked = $forum->is_discussion_locked($discussion); $discussionobject->canlock = $canlock; $discussionobject->starred = !empty($ufservice) ? $ufservice->favourite_exists('mod_forum', 'discussions', $discussion->get_id(), $modcontext) : false; $discussionobject->canreply = $capabilitymanager->can_post_in_discussion($USER, $discussion); $discussionobject->canfavourite = $canfavourite; if (forum_is_author_hidden($discussionobject, $forumrecord)) { $discussionobject->userid = null; $discussionobject->userfullname = null; $discussionobject->userpictureurl = null; $discussionobject->usermodified = null; $discussionobject->usermodifiedfullname = null; $discussionobject->usermodifiedpictureurl = null; } else { $discussionobject->userfullname = $firstpostauthor->get_full_name(); $discussionobject->userpictureurl = $urlfactory->get_author_profile_image_url($firstpostauthor, null, 2) ->out(false); $discussionobject->usermodifiedfullname = $latestpostauthor->get_full_name(); $discussionobject->usermodifiedpictureurl = $urlfactory->get_author_profile_image_url( $latestpostauthor, null, 2)->out(false); } $discussions[] = (array) $discussionobject; } } $result = array(); $result['discussions'] = $discussions; $result['warnings'] = $warnings; return $result; } /** * Describes the get_forum_discussions return value. * * @return external_single_structure * @since Moodle 3.7 */ public static function get_forum_discussions_returns() { return new external_single_structure( array( 'discussions' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'Post id'), 'name' => new external_value(PARAM_RAW, 'Discussion name'), 'groupid' => new external_value(PARAM_INT, 'Group id'), 'timemodified' => new external_value(PARAM_INT, 'Time modified'), 'usermodified' => new external_value(PARAM_INT, 'The id of the user who last modified'), 'timestart' => new external_value(PARAM_INT, 'Time discussion can start'), 'timeend' => new external_value(PARAM_INT, 'Time discussion ends'), 'discussion' => new external_value(PARAM_INT, 'Discussion id'), 'parent' => new external_value(PARAM_INT, 'Parent id'), 'userid' => new external_value(PARAM_INT, 'User who started the discussion id'), 'created' => new external_value(PARAM_INT, 'Creation time'), 'modified' => new external_value(PARAM_INT, 'Time modified'), 'mailed' => new external_value(PARAM_INT, 'Mailed?'), 'subject' => new external_value(PARAM_RAW, 'The post subject'), 'message' => new external_value(PARAM_RAW, 'The post message'), 'messageformat' => new external_format_value('message'), 'messagetrust' => new external_value(PARAM_INT, 'Can we trust?'), 'messageinlinefiles' => new external_files('post message inline files', VALUE_OPTIONAL), 'attachment' => new external_value(PARAM_RAW, 'Has attachments?'), 'attachments' => new external_files('attachments', VALUE_OPTIONAL), 'totalscore' => new external_value(PARAM_INT, 'The post message total score'), 'mailnow' => new external_value(PARAM_INT, 'Mail now?'), 'userfullname' => new external_value(PARAM_TEXT, 'Post author full name'), 'usermodifiedfullname' => new external_value(PARAM_TEXT, 'Post modifier full name'), 'userpictureurl' => new external_value(PARAM_URL, 'Post author picture.'), 'usermodifiedpictureurl' => new external_value(PARAM_URL, 'Post modifier picture.'), 'numreplies' => new external_value(PARAM_INT, 'The number of replies in the discussion'), 'numunread' => new external_value(PARAM_INT, 'The number of unread discussions.'), 'pinned' => new external_value(PARAM_BOOL, 'Is the discussion pinned'), 'locked' => new external_value(PARAM_BOOL, 'Is the discussion locked'), 'starred' => new external_value(PARAM_BOOL, 'Is the discussion starred'), 'canreply' => new external_value(PARAM_BOOL, 'Can the user reply to the discussion'), 'canlock' => new external_value(PARAM_BOOL, 'Can the user lock the discussion'), 'canfavourite' => new external_value(PARAM_BOOL, 'Can the user star the discussion'), ), 'post' ) ), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.9 */ public static function view_forum_parameters() { return new external_function_parameters( array( 'forumid' => new external_value(PARAM_INT, 'forum instance id') ) ); } /** * Trigger the course module viewed event and update the module completion status. * * @param int $forumid the forum instance id * @return array of warnings and status result * @since Moodle 2.9 * @throws moodle_exception */ public static function view_forum($forumid) { global $DB, $CFG; require_once($CFG->dirroot . "/mod/forum/lib.php"); $params = self::validate_parameters(self::view_forum_parameters(), array( 'forumid' => $forumid )); $warnings = array(); // Request and permission validation. $forum = $DB->get_record('forum', array('id' => $params['forumid']), '*', MUST_EXIST); list($course, $cm) = get_course_and_cm_from_instance($forum, 'forum'); $context = context_module::instance($cm->id); self::validate_context($context); require_capability('mod/forum:viewdiscussion', $context, null, true, 'noviewdiscussionspermission', 'forum'); // Call the forum/lib API. forum_view($forum, $course, $cm, $context); $result = array(); $result['status'] = true; $result['warnings'] = $warnings; return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.9 */ public static function view_forum_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'status: true if success'), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.9 */ public static function view_forum_discussion_parameters() { return new external_function_parameters( array( 'discussionid' => new external_value(PARAM_INT, 'discussion id') ) ); } /** * Trigger the discussion viewed event. * * @param int $discussionid the discussion id * @return array of warnings and status result * @since Moodle 2.9 * @throws moodle_exception */ public static function view_forum_discussion($discussionid) { global $DB, $CFG, $USER; require_once($CFG->dirroot . "/mod/forum/lib.php"); $params = self::validate_parameters(self::view_forum_discussion_parameters(), array( 'discussionid' => $discussionid )); $warnings = array(); $discussion = $DB->get_record('forum_discussions', array('id' => $params['discussionid']), '*', MUST_EXIST); $forum = $DB->get_record('forum', array('id' => $discussion->forum), '*', MUST_EXIST); list($course, $cm) = get_course_and_cm_from_instance($forum, 'forum'); // Validate the module context. It checks everything that affects the module visibility (including groupings, etc..). $modcontext = context_module::instance($cm->id); self::validate_context($modcontext); require_capability('mod/forum:viewdiscussion', $modcontext, null, true, 'noviewdiscussionspermission', 'forum'); // Call the forum/lib API. forum_discussion_view($modcontext, $forum, $discussion); // Mark as read if required. if (!$CFG->forum_usermarksread && forum_tp_is_tracked($forum)) { forum_tp_mark_discussion_read($USER, $discussion->id); } $result = array(); $result['status'] = true; $result['warnings'] = $warnings; return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.9 */ public static function view_forum_discussion_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'status: true if success'), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.0 */ public static function add_discussion_post_parameters() { return new external_function_parameters( array( 'postid' => new external_value(PARAM_INT, 'the post id we are going to reply to (can be the initial discussion post'), 'subject' => new external_value(PARAM_TEXT, 'new post subject'), 'message' => new external_value(PARAM_RAW, 'new post message (html assumed if messageformat is not provided)'), 'options' => new external_multiple_structure ( new external_single_structure( array( 'name' => new external_value(PARAM_ALPHANUM, 'The allowed keys (value format) are: discussionsubscribe (bool); subscribe to the discussion?, default to true private (bool); make this reply private to the author of the parent post, default to false. inlineattachmentsid (int); the draft file area id for inline attachments attachmentsid (int); the draft file area id for attachments topreferredformat (bool); convert the message & messageformat to FORMAT_HTML, defaults to false '), 'value' => new external_value(PARAM_RAW, 'the value of the option, this param is validated in the external function.' ) ) ), 'Options', VALUE_DEFAULT, array()), 'messageformat' => new external_format_value('message', VALUE_DEFAULT) ) ); } /** * Create new posts into an existing discussion. * * @param int $postid the post id we are going to reply to * @param string $subject new post subject * @param string $message new post message (html assumed if messageformat is not provided) * @param array $options optional settings * @param string $messageformat The format of the message, defaults to FORMAT_HTML for BC * @return array of warnings and the new post id * @since Moodle 3.0 * @throws moodle_exception */ public static function add_discussion_post($postid, $subject, $message, $options = array(), $messageformat = FORMAT_HTML) { global $CFG, $USER; require_once($CFG->dirroot . "/mod/forum/lib.php"); // Get all the factories that are required. $vaultfactory = mod_forum\local\container::get_vault_factory(); $entityfactory = mod_forum\local\container::get_entity_factory(); $datamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory(); $managerfactory = mod_forum\local\container::get_manager_factory(); $discussionvault = $vaultfactory->get_discussion_vault(); $forumvault = $vaultfactory->get_forum_vault(); $discussiondatamapper = $datamapperfactory->get_discussion_data_mapper(); $forumdatamapper = $datamapperfactory->get_forum_data_mapper(); $params = self::validate_parameters(self::add_discussion_post_parameters(), array( 'postid' => $postid, 'subject' => $subject, 'message' => $message, 'options' => $options, 'messageformat' => $messageformat, ) ); $warnings = array(); if (!$parent = forum_get_post_full($params['postid'])) { throw new moodle_exception('invalidparentpostid', 'forum'); } if (!$discussion = $discussionvault->get_from_id($parent->discussion)) { throw new moodle_exception('notpartofdiscussion', 'forum'); } // Request and permission validation. $forum = $forumvault->get_from_id($discussion->get_forum_id()); $capabilitymanager = $managerfactory->get_capability_manager($forum); $course = $forum->get_course_record(); $cm = $forum->get_course_module_record(); $discussionrecord = $discussiondatamapper->to_legacy_object($discussion); $forumrecord = $forumdatamapper->to_legacy_object($forum); $context = context_module::instance($cm->id); self::validate_context($context); $coursecontext = \context_course::instance($forum->get_course_id()); $discussionsubscribe = \mod_forum\subscriptions::get_user_default_subscription($forumrecord, $coursecontext, $cm, null); // Validate options. $options = array( 'discussionsubscribe' => $discussionsubscribe, 'private' => false, 'inlineattachmentsid' => 0, 'attachmentsid' => null, 'topreferredformat' => false ); foreach ($params['options'] as $option) { $name = trim($option['name']); switch ($name) { case 'discussionsubscribe': $value = clean_param($option['value'], PARAM_BOOL); break; case 'private': $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); // Ensure that the user has permissions to create attachments. if (!has_capability('mod/forum:createattachment', $context)) { $value = 0; } break; case 'topreferredformat': $value = clean_param($option['value'], PARAM_BOOL); break; default: throw new moodle_exception('errorinvalidparam', 'webservice', '', $name); } $options[$name] = $value; } if (!$capabilitymanager->can_post_in_discussion($USER, $discussion)) { throw new moodle_exception('nopostforum', 'forum'); } $thresholdwarning = forum_check_throttling($forumrecord, $cm); forum_check_blocking_threshold($thresholdwarning); // If we want to force a conversion to the preferred format, let's do it now. if ($options['topreferredformat']) { // We always are going to honor the preferred format. We are creating a new post. $preferredformat = editors_get_preferred_format(); // If the post is not HTML and the preferred format is HTML, convert to it. if ($params['messageformat'] != FORMAT_HTML and $preferredformat == FORMAT_HTML) { $params['message'] = format_text($params['message'], $params['messageformat'], ['filter' => false]); } $params['messageformat'] = $preferredformat; } // Create the post. $post = new stdClass(); $post->discussion = $discussion->get_id(); $post->parent = $parent->id; $post->subject = $params['subject']; $post->message = $params['message']; $post->messageformat = $params['messageformat']; $post->messagetrust = trusttext_trusted($context); $post->itemid = $options['inlineattachmentsid']; $post->attachments = $options['attachmentsid']; $post->isprivatereply = $options['private']; $post->deleted = 0; $fakemform = $post->attachments; if ($postid = forum_add_new_post($post, $fakemform)) { $post->id = $postid; // Trigger events and completion. $params = array( 'context' => $context, 'objectid' => $post->id, 'other' => array( 'discussionid' => $discussion->get_id(), 'forumid' => $forum->get_id(), 'forumtype' => $forum->get_type(), ) ); $event = \mod_forum\event\post_created::create($params); $event->add_record_snapshot('forum_posts', $post); $event->add_record_snapshot('forum_discussions', $discussionrecord); $event->trigger(); // Update completion state. $completion = new completion_info($course); if ($completion->is_enabled($cm) && ($forum->get_completion_replies() || $forum->get_completion_posts())) { $completion->update_state($cm, COMPLETION_COMPLETE); } if ($options['discussionsubscribe']) { $settings = new stdClass(); $settings->discussionsubscribe = $options['discussionsubscribe']; forum_post_subscription($settings, $forumrecord, $discussionrecord); } } else { throw new moodle_exception('couldnotadd', 'forum'); } $builderfactory = \mod_forum\local\container::get_builder_factory(); $exportedpostsbuilder = $builderfactory->get_exported_posts_builder(); $postentity = $entityfactory->get_post_from_stdClass($post); $exportedposts = $exportedpostsbuilder->build($USER, [$forum], [$discussion], [$postentity]); $exportedpost = $exportedposts[0]; $message = []; $message[] = [ 'type' => 'success', 'message' => get_string("postaddedsuccess", "forum") ]; $message[] = [ 'type' => 'success', 'message' => get_string("postaddedtimeleft", "forum", format_time($CFG->maxeditingtime)) ]; $result = array(); $result['postid'] = $postid; $result['warnings'] = $warnings; $result['post'] = $exportedpost; $result['messages'] = $message; return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.0 */ public static function add_discussion_post_returns() { return new external_single_structure( array( 'postid' => new external_value(PARAM_INT, 'new post id'), 'warnings' => new external_warnings(), 'post' => post_exporter::get_read_structure(), 'messages' => new external_multiple_structure( new external_single_structure( array( 'type' => new external_value(PARAM_TEXT, "The classification to be used in the client side", VALUE_REQUIRED), 'message' => new external_value(PARAM_TEXT,'untranslated english message to explain the warning', VALUE_REQUIRED) ), 'Messages'), 'list of warnings', VALUE_OPTIONAL ), //'alertmessage' => new external_value(PARAM_RAW, 'Success message to be displayed to the user.'), ) ); } /** * Toggle the favouriting value for the discussion provided * * @param int $discussionid The discussion we need to favourite * @param bool $targetstate The state of the favourite value * @return array The exported discussion */ public static function toggle_favourite_state($discussionid, $targetstate) { global $DB, $PAGE, $USER; $params = self::validate_parameters(self::toggle_favourite_state_parameters(), [ 'discussionid' => $discussionid, 'targetstate' => $targetstate ]); $vaultfactory = mod_forum\local\container::get_vault_factory(); // Get the discussion vault and the corresponding discussion entity. $discussionvault = $vaultfactory->get_discussion_vault(); $discussion = $discussionvault->get_from_id($params['discussionid']); $forumvault = $vaultfactory->get_forum_vault(); $forum = $forumvault->get_from_id($discussion->get_forum_id()); $forumcontext = $forum->get_context(); self::validate_context($forumcontext); $managerfactory = mod_forum\local\container::get_manager_factory(); $capabilitymanager = $managerfactory->get_capability_manager($forum); // Does the user have the ability to favourite the discussion? if (!$capabilitymanager->can_favourite_discussion($USER)) { throw new moodle_exception('cannotfavourite', 'forum'); } $usercontext = context_user::instance($USER->id); $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext); $isfavourited = $ufservice->favourite_exists('mod_forum', 'discussions', $discussion->get_id(), $forumcontext); $favouritefunction = $targetstate ? 'create_favourite' : 'delete_favourite'; if ($isfavourited != (bool) $params['targetstate']) { $ufservice->{$favouritefunction}('mod_forum', 'discussions', $discussion->get_id(), $forumcontext); } $exporterfactory = mod_forum\local\container::get_exporter_factory(); $builder = mod_forum\local\container::get_builder_factory()->get_exported_discussion_builder(); $favourited = ($builder->is_favourited($discussion, $forumcontext, $USER) ? [$discussion->get_id()] : []); $exporter = $exporterfactory->get_discussion_exporter($USER, $forum, $discussion, [], $favourited); return $exporter->export($PAGE->get_renderer('mod_forum')); } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.0 */ public static function toggle_favourite_state_returns() { return discussion_exporter::get_read_structure(); } /** * Defines the parameters for the toggle_favourite_state method * * @return external_function_parameters */ public static function toggle_favourite_state_parameters() { return new external_function_parameters( [ 'discussionid' => new external_value(PARAM_INT, 'The discussion to subscribe or unsubscribe'), 'targetstate' => new external_value(PARAM_BOOL, 'The target state') ] ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.0 */ public static function add_discussion_parameters() { return new external_function_parameters( array( 'forumid' => new external_value(PARAM_INT, 'Forum instance ID'), 'subject' => new external_value(PARAM_TEXT, 'New Discussion subject'), 'message' => new external_value(PARAM_RAW, 'New Discussion message (only html format allowed)'), 'groupid' => new external_value(PARAM_INT, 'The group, default to 0', VALUE_DEFAULT, 0), 'options' => new external_multiple_structure ( new external_single_structure( array( 'name' => new external_value(PARAM_ALPHANUM, 'The allowed keys (value format) are: discussionsubscribe (bool); subscribe to the discussion?, default to true discussionpinned (bool); is the discussion pinned, default to false inlineattachmentsid (int); the draft file area id for inline attachments attachmentsid (int); the draft file area id for attachments '), 'value' => new external_value(PARAM_RAW, 'The value of the option, This param is validated in the external function.' ) ) ), 'Options', VALUE_DEFAULT, array()) ) ); } /** * Add a new discussion into an existing forum. * * @param int $forumid the forum instance id * @param string $subject new discussion subject * @param string $message new discussion message (only html format allowed) * @param int $groupid the user course group * @param array $options optional settings * @return array of warnings and the new discussion id * @since Moodle 3.0 * @throws moodle_exception */ public static function add_discussion($forumid, $subject, $message, $groupid = 0, $options = array()) { global $DB, $CFG; require_once($CFG->dirroot . "/mod/forum/lib.php"); $params = self::validate_parameters(self::add_discussion_parameters(), array( 'forumid' => $forumid, 'subject' => $subject, 'message' => $message, 'groupid' => $groupid, 'options' => $options )); $warnings = array(); // Request and permission validation. $forum = $DB->get_record('forum', array('id' => $params['forumid']), '*', MUST_EXIST); list($course, $cm) = get_course_and_cm_from_instance($forum, 'forum'); $context = context_module::instance($cm->id); self::validate_context($context); // Validate options. $options = array( 'discussionsubscribe' => true, 'discussionpinned' => false, 'inlineattachmentsid' => 0, 'attachmentsid' => null ); foreach ($params['options'] as $option) { $name = trim($option['name']); switch ($name) { case 'discussionsubscribe': $value = clean_param($option['value'], PARAM_BOOL); break; case 'discussionpinned': $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); // Ensure that the user has permissions to create attachments. if (!has_capability('mod/forum:createattachment', $context)) { $value = 0; } break; default: throw new moodle_exception('errorinvalidparam', 'webservice', '', $name); } $options[$name] = $value; } // Normalize group. if (!groups_get_activity_groupmode($cm)) { // Groups not supported, force to -1. $groupid = -1; } else { // Check if we receive the default or and empty value for groupid, // in this case, get the group for the user in the activity. if (empty($params['groupid'])) { $groupid = groups_get_activity_group($cm); } else { // Here we rely in the group passed, forum_user_can_post_discussion will validate the group. $groupid = $params['groupid']; } } if (!forum_user_can_post_discussion($forum, $groupid, -1, $cm, $context)) { throw new moodle_exception('cannotcreatediscussion', 'forum'); } $thresholdwarning = forum_check_throttling($forum, $cm); forum_check_blocking_threshold($thresholdwarning); // Create the discussion. $discussion = new stdClass(); $discussion->course = $course->id; $discussion->forum = $forum->id; $discussion->message = $params['message']; $discussion->messageformat = FORMAT_HTML; // Force formatting for now. $discussion->messagetrust = trusttext_trusted($context); $discussion->itemid = $options['inlineattachmentsid']; $discussion->groupid = $groupid; $discussion->mailnow = 0; $discussion->subject = $params['subject']; $discussion->name = $discussion->subject; $discussion->timestart = 0; $discussion->timeend = 0; $discussion->timelocked = 0; $discussion->attachments = $options['attachmentsid']; if (has_capability('mod/forum:pindiscussions', $context) && $options['discussionpinned']) { $discussion->pinned = FORUM_DISCUSSION_PINNED; } else { $discussion->pinned = FORUM_DISCUSSION_UNPINNED; } $fakemform = $options['attachmentsid']; if ($discussionid = forum_add_discussion($discussion, $fakemform)) { $discussion->id = $discussionid; // Trigger events and completion. $params = array( 'context' => $context, 'objectid' => $discussion->id, 'other' => array( 'forumid' => $forum->id, ) ); $event = \mod_forum\event\discussion_created::create($params); $event->add_record_snapshot('forum_discussions', $discussion); $event->trigger(); $completion = new completion_info($course); if ($completion->is_enabled($cm) && ($forum->completiondiscussions || $forum->completionposts)) { $completion->update_state($cm, COMPLETION_COMPLETE); } $settings = new stdClass(); $settings->discussionsubscribe = $options['discussionsubscribe']; forum_post_subscription($settings, $forum, $discussion); } else { throw new moodle_exception('couldnotadd', 'forum'); } $result = array(); $result['discussionid'] = $discussionid; $result['warnings'] = $warnings; return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.0 */ public static function add_discussion_returns() { return new external_single_structure( array( 'discussionid' => new external_value(PARAM_INT, 'New Discussion ID'), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.1 */ public static function can_add_discussion_parameters() { return new external_function_parameters( array( 'forumid' => new external_value(PARAM_INT, 'Forum instance ID'), 'groupid' => new external_value(PARAM_INT, 'The group to check, default to active group. Use -1 to check if the user can post in all the groups.', VALUE_DEFAULT, null) ) ); } /** * Check if the current user can add discussions in the given forum (and optionally for the given group). * * @param int $forumid the forum instance id * @param int $groupid the group to check, default to active group. Use -1 to check if the user can post in all the groups. * @return array of warnings and the status (true if the user can add discussions) * @since Moodle 3.1 * @throws moodle_exception */ public static function can_add_discussion($forumid, $groupid = null) { global $DB, $CFG; require_once($CFG->dirroot . "/mod/forum/lib.php"); $params = self::validate_parameters(self::can_add_discussion_parameters(), array( 'forumid' => $forumid, 'groupid' => $groupid, )); $warnings = array(); // Request and permission validation. $forum = $DB->get_record('forum', array('id' => $params['forumid']), '*', MUST_EXIST); list($course, $cm) = get_course_and_cm_from_instance($forum, 'forum'); $context = context_module::instance($cm->id); self::validate_context($context); $status = forum_user_can_post_discussion($forum, $params['groupid'], -1, $cm, $context); $result = array(); $result['status'] = $status; $result['canpindiscussions'] = has_capability('mod/forum:pindiscussions', $context); $result['cancreateattachment'] = forum_can_create_attachment($forum, $context); $result['warnings'] = $warnings; return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.1 */ public static function can_add_discussion_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'True if the user can add discussions, false otherwise.'), 'canpindiscussions' => new external_value(PARAM_BOOL, 'True if the user can pin discussions, false otherwise.', VALUE_OPTIONAL), 'cancreateattachment' => new external_value(PARAM_BOOL, 'True if the user can add attachments, false otherwise.', VALUE_OPTIONAL), 'warnings' => new external_warnings() ) ); } /** * Describes the parameters for get_forum_access_information. * * @return external_external_function_parameters * @since Moodle 3.7 */ public static function get_forum_access_information_parameters() { return new external_function_parameters ( array( 'forumid' => new external_value(PARAM_INT, 'Forum instance id.') ) ); } /** * Return access information for a given forum. * * @param int $forumid forum instance id * @return array of warnings and the access information * @since Moodle 3.7 * @throws moodle_exception */ public static function get_forum_access_information($forumid) { global $DB; $params = self::validate_parameters(self::get_forum_access_information_parameters(), array('forumid' => $forumid)); // Request and permission validation. $forum = $DB->get_record('forum', array('id' => $params['forumid']), '*', MUST_EXIST); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = context_module::instance($cm->id); self::validate_context($context); $result = array(); // Return all the available capabilities. $capabilities = load_capability_def('mod_forum'); foreach ($capabilities as $capname => $capdata) { // Get fields like cansubmit so it is consistent with the access_information function implemented in other modules. $field = 'can' . str_replace('mod/forum:', '', $capname); $result[$field] = has_capability($capname, $context); } $result['warnings'] = array(); return $result; } /** * Describes the get_forum_access_information return value. * * @return external_single_structure * @since Moodle 3.7 */ public static function get_forum_access_information_returns() { $structure = array( 'warnings' => new external_warnings() ); $capabilities = load_capability_def('mod_forum'); foreach ($capabilities as $capname => $capdata) { // Get fields like cansubmit so it is consistent with the access_information function implemented in other modules. $field = 'can' . str_replace('mod/forum:', '', $capname); $structure[$field] = new external_value(PARAM_BOOL, 'Whether the user has the capability ' . $capname . ' allowed.', VALUE_OPTIONAL); } return new external_single_structure($structure); } /** * Set the subscription state. * * @param int $forumid * @param int $discussionid * @param bool $targetstate * @return \stdClass */ public static function set_subscription_state($forumid, $discussionid, $targetstate) { global $PAGE, $USER; $params = self::validate_parameters(self::set_subscription_state_parameters(), [ 'forumid' => $forumid, 'discussionid' => $discussionid, 'targetstate' => $targetstate ]); $vaultfactory = mod_forum\local\container::get_vault_factory(); $forumvault = $vaultfactory->get_forum_vault(); $forum = $forumvault->get_from_id($params['forumid']); $coursemodule = $forum->get_course_module_record(); $context = $forum->get_context(); self::validate_context($context); $discussionvault = $vaultfactory->get_discussion_vault(); $discussion = $discussionvault->get_from_id($params['discussionid']); $legacydatamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory(); $forumrecord = $legacydatamapperfactory->get_forum_data_mapper()->to_legacy_object($forum); $discussionrecord = $legacydatamapperfactory->get_discussion_data_mapper()->to_legacy_object($discussion); if (!\mod_forum\subscriptions::is_subscribable($forumrecord)) { // Nothing to do. We won't actually output any content here though. throw new \moodle_exception('cannotsubscribe', 'mod_forum'); } $issubscribed = \mod_forum\subscriptions::is_subscribed( $USER->id, $forumrecord, $discussion->get_id(), $coursemodule ); // If the current state doesn't equal the desired state then update the current // state to the desired state. if ($issubscribed != (bool) $params['targetstate']) { if ($params['targetstate']) { \mod_forum\subscriptions::subscribe_user_to_discussion($USER->id, $discussionrecord, $context); } else { \mod_forum\subscriptions::unsubscribe_user_from_discussion($USER->id, $discussionrecord, $context); } } $exporterfactory = mod_forum\local\container::get_exporter_factory(); $exporter = $exporterfactory->get_discussion_exporter($USER, $forum, $discussion); return $exporter->export($PAGE->get_renderer('mod_forum')); } /** * Returns description of method parameters. * * @return external_function_parameters */ public static function set_subscription_state_parameters() { return new external_function_parameters( [ 'forumid' => new external_value(PARAM_INT, 'Forum that the discussion is in'), 'discussionid' => new external_value(PARAM_INT, 'The discussion to subscribe or unsubscribe'), 'targetstate' => new external_value(PARAM_BOOL, 'The target state') ] ); } /** * Returns description of method result value. * * @return \core_external\external_description */ public static function set_subscription_state_returns() { return discussion_exporter::get_read_structure(); } /** * Set the lock state. * * @param int $forumid * @param int $discussionid * @param string $targetstate * @return \stdClass */ public static function set_lock_state($forumid, $discussionid, $targetstate) { global $DB, $PAGE, $USER; $params = self::validate_parameters(self::set_lock_state_parameters(), [ 'forumid' => $forumid, 'discussionid' => $discussionid, 'targetstate' => $targetstate ]); $vaultfactory = mod_forum\local\container::get_vault_factory(); $forumvault = $vaultfactory->get_forum_vault(); $forum = $forumvault->get_from_id($params['forumid']); $managerfactory = mod_forum\local\container::get_manager_factory(); $capabilitymanager = $managerfactory->get_capability_manager($forum); if (!$capabilitymanager->can_manage_forum($USER)) { throw new moodle_exception('errorcannotlock', 'forum'); } // If the targetstate(currentstate) is not 0 then it should be set to the current time. $lockedvalue = $targetstate ? 0 : time(); self::validate_context($forum->get_context()); $discussionvault = $vaultfactory->get_discussion_vault(); $discussion = $discussionvault->get_from_id($params['discussionid']); // If the current state doesn't equal the desired state then update the current. // state to the desired state. $discussion->toggle_locked_state($lockedvalue); $response = $discussionvault->update_discussion($discussion); $discussion = !$response ? $response : $discussion; // Trigger an event. $params = [ 'context' => $forum->get_context(), 'objectid' => $discussion->get_id(), 'other' => ['forumid' => $forum->get_id(), 'status' => $lockedvalue ? 'locked' : 'unlocked'], ]; $event = \mod_forum\event\discussion_lock_updated::create($params); $discussionrecord = $DB->get_record('forum_discussions', ['id' => $discussion->get_id()]); $event->add_record_snapshot('forum_discussions', $discussionrecord); $event->trigger(); $exporterfactory = mod_forum\local\container::get_exporter_factory(); $exporter = $exporterfactory->get_discussion_exporter($USER, $forum, $discussion); return $exporter->export($PAGE->get_renderer('mod_forum')); } /** * Returns description of method parameters. * * @return external_function_parameters */ public static function set_lock_state_parameters() { return new external_function_parameters( [ 'forumid' => new external_value(PARAM_INT, 'Forum that the discussion is in'), 'discussionid' => new external_value(PARAM_INT, 'The discussion to lock / unlock'), 'targetstate' => new external_value(PARAM_INT, 'The timestamp for the lock state') ] ); } /** * Returns description of method result value. * * @return \core_external\external_description */ public static function set_lock_state_returns() { return new external_single_structure([ 'id' => new external_value(PARAM_INT, 'The discussion we are locking.'), 'locked' => new external_value(PARAM_BOOL, 'The locked state of the discussion.'), 'times' => new external_single_structure([ 'locked' => new external_value(PARAM_INT, 'The locked time of the discussion.'), ]) ]); } /** * Set the pin state. * * @param int $discussionid * @param bool $targetstate * @return \stdClass */ public static function set_pin_state($discussionid, $targetstate) { global $PAGE, $USER; $params = self::validate_parameters(self::set_pin_state_parameters(), [ 'discussionid' => $discussionid, 'targetstate' => $targetstate, ]); $vaultfactory = mod_forum\local\container::get_vault_factory(); $managerfactory = mod_forum\local\container::get_manager_factory(); $forumvault = $vaultfactory->get_forum_vault(); $discussionvault = $vaultfactory->get_discussion_vault(); $discussion = $discussionvault->get_from_id($params['discussionid']); $forum = $forumvault->get_from_id($discussion->get_forum_id()); $capabilitymanager = $managerfactory->get_capability_manager($forum); self::validate_context($forum->get_context()); $legacydatamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory(); if (!$capabilitymanager->can_pin_discussions($USER)) { // Nothing to do. We won't actually output any content here though. throw new \moodle_exception('cannotpindiscussions', 'mod_forum'); } $discussion->set_pinned($targetstate); $discussionvault->update_discussion($discussion); $exporterfactory = mod_forum\local\container::get_exporter_factory(); $exporter = $exporterfactory->get_discussion_exporter($USER, $forum, $discussion); return $exporter->export($PAGE->get_renderer('mod_forum')); } /** * Returns description of method parameters. * * @return external_function_parameters */ public static function set_pin_state_parameters() { return new external_function_parameters( [ 'discussionid' => new external_value(PARAM_INT, 'The discussion to pin or unpin', VALUE_REQUIRED, null, NULL_NOT_ALLOWED), 'targetstate' => new external_value(PARAM_INT, 'The target state', VALUE_REQUIRED, null, NULL_NOT_ALLOWED), ] ); } /** * Returns description of method result value. * * @return external_single_structure */ public static function set_pin_state_returns() { return discussion_exporter::get_read_structure(); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.8 */ public static function delete_post_parameters() { return new external_function_parameters( array( 'postid' => new external_value(PARAM_INT, 'Post to be deleted. It can be a discussion topic post.'), ) ); } /** * Deletes a post or a discussion completely when the post is the discussion topic. * * @param int $postid post to be deleted, it can be a discussion topic post. * @return array of warnings and the status (true if the post/discussion was deleted) * @since Moodle 3.8 * @throws moodle_exception */ public static function delete_post($postid) { global $USER, $CFG; require_once($CFG->dirroot . "/mod/forum/lib.php"); $params = self::validate_parameters(self::delete_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'); } $context = $forumentity->get_context(); self::validate_context($context); $managerfactory = mod_forum\local\container::get_manager_factory(); $legacydatamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory(); $capabilitymanager = $managerfactory->get_capability_manager($forumentity); $forumdatamapper = $legacydatamapperfactory->get_forum_data_mapper(); $discussiondatamapper = $legacydatamapperfactory->get_discussion_data_mapper(); $postdatamapper = $legacydatamapperfactory->get_post_data_mapper(); $replycount = $postvault->get_reply_count_for_post_id_in_discussion_id($USER, $postentity->get_id(), $discussionentity->get_id(), true); $hasreplies = $replycount > 0; $capabilitymanager->validate_delete_post($USER, $discussionentity, $postentity, $hasreplies); if (!$postentity->has_parent()) { $status = forum_delete_discussion( $discussiondatamapper->to_legacy_object($discussionentity), false, $forumentity->get_course_record(), $forumentity->get_course_module_record(), $forumdatamapper->to_legacy_object($forumentity) ); } else { $status = forum_delete_post( $postdatamapper->to_legacy_object($postentity), has_capability('mod/forum:deleteanypost', $context), $forumentity->get_course_record(), $forumentity->get_course_module_record(), $forumdatamapper->to_legacy_object($forumentity) ); } $result = array(); $result['status'] = $status; $result['warnings'] = $warnings; return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.8 */ public static function delete_post_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'True if the post/discussion was deleted, false otherwise.'), 'warnings' => new external_warnings() ) ); } /** * 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, 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); $discussionsummariesvault = $vaultfactory->get_discussions_in_forum_vault(); $discussionsummaries = $discussionsummariesvault->get_from_forum_id( $forum->get_id(), true, null, $discussionsummariesvault::SORTORDER_CREATED_ASC, 0, 0 ); $postvault = $vaultfactory->get_post_vault(); $builderfactory = mod_forum\local\container::get_builder_factory(); $postbuilder = $builderfactory->get_exported_posts_builder(); $builtdiscussions = []; foreach ($discussionsummaries as $discussionsummary) { $discussion = $discussionsummary->get_discussion(); if (!$capabilitymanager->can_view_discussion($USER, $discussion)) { continue; } $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)) ); } $discussionauthor = $discussionsummary->get_first_post_author(); $firstpost = $discussionsummary->get_first_post(); $builtdiscussions[] = [ 'name' => $discussion->get_name(), 'id' => $discussion->get_id(), 'timecreated' => $firstpost->get_time_created(), 'authorfullname' => $discussionauthor->get_full_name(), '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'), 'timecreated' => new external_value(PARAM_INT, 'Timestamp of the discussion start'), 'authorfullname' => new external_value(PARAM_RAW, 'Full name of the user that started 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 \core_external\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 \core_external\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 ($params['subject'] !== '') { $updatepost->subject = $params['subject']; } if ($params['message'] !== '' && isset($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 \core_external\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() ] ); } } PK-B\Fo]] styles.cssnu[/** * Structure of a forum post * div.forumpost(.read.unread) * div.header.row * div.picture.left * div.topic(.starter) * div.subject * div.author * div.row * div.side.left * div.content-mask * div.content * div.attachments * div.posting(.shortenedpost|.fullpost) * // message * div.attachedimages * // attachedimages * div.row * div.left * div.options * div.commands * div.post-word-count * div.forum-post-rating * div.link * div.footer */ .forumpost { display: block; position: relative; margin: 0 0 1em 0; padding: 0; border: 1px solid #000; max-width: 100%; } .forumpost .row { width: 100%; position: relative; } .forumpost .row .left { float: left; width: 43px; overflow: hidden; } .forumpost .row .left .grouppictures a { text-align: center; display: block; margin: 6px 2px 0 2px; } .forumpost .row .left .grouppicture { width: 20px; height: 20px; } .forumpost .row .topic, .forumpost .row .content-mask, .forumpost .row .options { margin-left: 43px; } .forumpost .picture img { margin: 4px; } .forumpost .options .commands, .forumpost .content .attachments, .forumpost .options .footer, .forumpost .options .link { text-align: right; } .forumpost .options .forum-post-rating { float: left; } .forumpost .content .posting { overflow: auto; max-width: 100%; } .forumpost .content .attachedimages img { max-width: 100%; } .forumpost .post-word-count { font-size: .85em; font-style: italic; } .forumpost .shortenedpost .post-word-count { display: inline; padding: 0 .3em; } .path-mod-forum .forumolddiscuss, #page-mod-forum-search .c0 { text-align: right; } .path-mod-forum .indent { margin-left: 3%; } .path-mod-forum .forumheaderlist { width: 100%; border-width: 1px; border-style: solid; border-collapse: separate; margin-top: 10px; } .path-mod-forum .forumheaderlist td { border-width: 1px 0 0 1px; border-style: solid; } .path-mod-forum .forumheaderlist th.header.replies .iconsmall { margin: 0 .3em; } .path-mod-forum .forumheaderlist .discussion .starter { vertical-align: middle; } .path-mod-forum .forumheaderlist .discussion .pinned img { padding: 5px; } .path-mod-forum .forumheaderlist .discussion .lastpost { white-space: nowrap; text-align: right; } .path-mod-forum .forumheaderlist .replies { white-space: nowrap; } .path-mod-forum .forumheaderlist thead .discussionsubscription { text-align: center; } /** Styles for subscribers.php */ #page-mod-forum-subscribers .subscriberdiv, #page-mod-forum-subscribers .subscribertable { width: 100%; vertical-align: top; } #page-mod-forum-subscribers .subscribertable tr td { vertical-align: top; } #page-mod-forum-subscribers .subscribertable tr td.actions { width: 16%; padding-top: 3em; } #page-mod-forum-subscribers .subscribertable tr td.actions .actionbutton { margin: 0.3em 0; padding: 0.5em 0; width: 100%; } #page-mod-forum-subscribers .subscribertable tr td.existing, #page-mod-forum-subscribers .subscribertable tr td.potential { width: 42%; } /** Styles for discuss.php **/ #page-mod-forum-discuss .discussioncontrols { width: 100%; margin: 5px; } #page-mod-forum-discuss .discussioncontrols .controlscontainer { width: 100%; float: right; } #page-mod-forum-discuss .discussioncontrols .discussioncontrol { float: left; } #page-mod-forum-discuss .discussioncontrol.exporttoportfolio { text-align: left; } #page-mod-forum-discuss .discussioncontrol.displaymode { padding-right: 10px; } #page-mod-forum-discuss .discussioncontrol.movediscussion { padding-right: 10px; } /** Styles for view.php **/ #page-mod-forum-view .forumaddnew { margin-bottom: 20px; } #page-mod-forum-view .groupmenu { float: left; text-align: left; white-space: nowrap; } #page-mod-forum-index .subscription, #page-mod-forum-view .subscription { float: right; text-align: right; white-space: nowrap; margin: 5px 0; } .discussion-list .userpicture, .discussion-list .grouppicture { height: 35px; width: 35px; } /** Styles for search.php */ #page-mod-forum-search .introcontent { padding: 15px; font-weight: bold; } .path-mod-forum span.unread a:first-child { padding-right: 10px; } .path-mod-forum span.unread img { margin-left: 5px; } /** Unknown Styles ??? */ #email .unsubscribelink { margin-top: 20px; } /* Forumpost unread -------------------------*/ .forumpost.unread .header, .path-course-view .unread, span.unread { background-color: #ffd; } .forumpost.unread .header { border-bottom: 1px solid #ddd; } /* Forumpost hash anchor target */ .path-mod-forum :target > .forumpost:before { display: block; content: ''; width: 4px; position: absolute; background: #0070a8; left: -1px; top: -1px; bottom: -1px; } /* Discussion navigation */ .path-mod-forum .discussion-nav { margin: .5em 0; } .path-mod-forum .discussion-nav ul { margin: 0; list-style: none; padding-left: 0; } .path-mod-forum .discussion-nav .next-discussion { float: right; } .path-mod-forum .discussion-nav .prev-discussion { float: left; } .path-mod-forum .preload-subscribe { background: url([[pix:mod_forum|t/subscribed]]) no-repeat -9999px -9999px; } .path-mod-forum .preload-unsubscribe { background: url([[pix:mod_forum|t/unsubscribed]]) no-repeat -9999px -9999px; } .path-mod-forum .discussionsubscription, .path-mod-forum .discussion-settings-menu, .path-mod-forum .discussionsubscription { margin-top: -10px; text-align: right; margin-bottom: 10px; } .path-mod-forum .discussionsubscription > a > img, .path-mod-forum .discussionlock > a > img, .path-mod-forum .favourite-discussion > a > img, .path-mod-forum .discussion-settings-menu > a > img { width: 12px; padding: 0 4px; } #page-mod-forum-view img.timedpost { margin-right: 5px; } .path-mod-forum article .nav .nav-link:first-of-type { margin-left: auto; } .path-mod-forum.dir-rtl article .nav .nav-link:first-of-type { margin-left: 0; margin-right: auto; } .path-mod-forum article .nav .nav-link + .nav-link { border-left: 1px solid #ddd; } .privatereplyinfo { font-size: 80%; } .forum-post-container:focus { outline: 0; } /* This style is copied directly from the Bootstrap reboot file which adds button outline */ /* stylelint-disable declaration-block-no-duplicate-properties */ .post-actions .btn:focus { outline: 1px dotted; outline: 5px auto -webkit-focus-ring-color; } .forum-post-container:focus > .focus-target { outline: 1px dotted; outline: 5px auto -webkit-focus-ring-color; } /* stylelint-enable declaration-block-no-duplicate-properties */ /* Forum grading non striked through line */ .hr-sect { display: flex; flex-basis: 100%; font-weight: bold; align-items: center; margin: 8px 0; } .hr-sect::before, .hr-sect::after { content: ""; flex-grow: 1; background: rgba(0, 0, 0, 0.35); height: 1px; font-size: 0; line-height: 0; } .hr-sect::before { margin-right: 15px; } .hr-sect::after { margin-left: 15px; } PK-B\qpost.phpnu[. /** * Edit and save a new post to a discussion * * @package mod_forum * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../../config.php'); require_once('lib.php'); require_once($CFG->libdir.'/completionlib.php'); $reply = optional_param('reply', 0, PARAM_INT); $forum = optional_param('forum', 0, PARAM_INT); $edit = optional_param('edit', 0, PARAM_INT); $delete = optional_param('delete', 0, PARAM_INT); $prune = optional_param('prune', 0, PARAM_INT); $name = optional_param('name', '', PARAM_CLEAN); $confirm = optional_param('confirm', 0, PARAM_INT); $groupid = optional_param('groupid', null, PARAM_INT); $subject = optional_param('subject', '', PARAM_TEXT); // Values posted via the inpage reply form. $prefilledpost = optional_param('post', '', PARAM_TEXT); $prefilledpostformat = optional_param('postformat', FORMAT_MOODLE, PARAM_INT); $prefilledprivatereply = optional_param('privatereply', false, PARAM_BOOL); $PAGE->set_url('/mod/forum/post.php', array( 'reply' => $reply, 'forum' => $forum, 'edit' => $edit, 'delete' => $delete, 'prune' => $prune, 'name' => $name, 'confirm' => $confirm, 'groupid' => $groupid, )); // These page_params will be passed as hidden variables later in the form. $pageparams = array('reply' => $reply, 'forum' => $forum, 'edit' => $edit); $sitecontext = context_system::instance(); $entityfactory = mod_forum\local\container::get_entity_factory(); $vaultfactory = mod_forum\local\container::get_vault_factory(); $managerfactory = mod_forum\local\container::get_manager_factory(); $legacydatamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory(); $urlfactory = mod_forum\local\container::get_url_factory(); $forumvault = $vaultfactory->get_forum_vault(); $forumdatamapper = $legacydatamapperfactory->get_forum_data_mapper(); $discussionvault = $vaultfactory->get_discussion_vault(); $discussiondatamapper = $legacydatamapperfactory->get_discussion_data_mapper(); $postvault = $vaultfactory->get_post_vault(); $postdatamapper = $legacydatamapperfactory->get_post_data_mapper(); if (!isloggedin() or isguestuser()) { if (!isloggedin() and !get_local_referer()) { // No referer+not logged in - probably coming in via email See MDL-9052. require_login(); } if (!empty($forum)) { // User is starting a new discussion in a forum. $forumentity = $forumvault->get_from_id($forum); if (empty($forumentity)) { throw new \moodle_exception('invalidforumid', 'forum'); } } else if (!empty($reply)) { // User is writing a new reply. $forumentity = $forumvault->get_from_post_id($reply); if (empty($forumentity)) { throw new \moodle_exception('invalidparentpostid', 'forum'); } } $forum = $forumdatamapper->to_legacy_object($forumentity); $modcontext = $forumentity->get_context(); $course = $forumentity->get_course_record(); if (!$cm = get_coursemodule_from_instance("forum", $forum->id, $course->id)) { throw new \moodle_exception("invalidcoursemodule"); } $PAGE->set_cm($cm, $course, $forum); $PAGE->set_context($modcontext); $PAGE->set_title($course->shortname); $PAGE->set_heading($course->fullname); $referer = get_local_referer(false); echo $OUTPUT->header(); echo $OUTPUT->confirm(get_string('noguestpost', 'forum'), get_login_url(), $referer, [ 'confirmtitle' => get_string('noguestpost:title', 'forum'), 'continuestr' => get_string('login') ]); echo $OUTPUT->footer(); exit; } require_login(0, false); // Script is useless unless they're logged in. $canreplyprivately = false; if (!empty($forum)) { // User is starting a new discussion in a forum. $forumentity = $forumvault->get_from_id($forum); if (empty($forumentity)) { throw new \moodle_exception('invalidforumid', 'forum'); } $capabilitymanager = $managerfactory->get_capability_manager($forumentity); $forum = $forumdatamapper->to_legacy_object($forumentity); $course = $forumentity->get_course_record(); if (!$cm = get_coursemodule_from_instance("forum", $forum->id, $course->id)) { throw new \moodle_exception("invalidcoursemodule"); } // Retrieve the contexts. $modcontext = $forumentity->get_context(); $coursecontext = context_course::instance($course->id); if ($forumentity->is_in_group_mode() && null === $groupid) { $groupid = groups_get_activity_group($cm); } if (!$capabilitymanager->can_create_discussions($USER, $groupid)) { if (!isguestuser()) { if (!is_enrolled($coursecontext)) { if (enrol_selfenrol_available($course->id)) { $SESSION->wantsurl = qualified_me(); $SESSION->enrolcancel = get_local_referer(false); redirect(new moodle_url('/enrol/index.php', array('id' => $course->id, 'returnurl' => '/mod/forum/view.php?f=' . $forum->id)), get_string('youneedtoenrol')); } } } throw new \moodle_exception('nopostforum', 'forum'); } if (!$cm->visible and !has_capability('moodle/course:viewhiddenactivities', $modcontext)) { redirect( $urlfactory->get_course_url_from_forum($forumentity), get_string('activityiscurrentlyhidden'), null, \core\output\notification::NOTIFY_ERROR ); } // Load up the $post variable. $post = new stdClass(); $post->course = $course->id; $post->forum = $forum->id; $post->discussion = 0; // Ie discussion # not defined yet. $post->parent = 0; $post->subject = $subject; $post->userid = $USER->id; $post->message = $prefilledpost; $post->messageformat = editors_get_preferred_format(); $post->messagetrust = 0; $post->groupid = $groupid; // Unsetting this will allow the correct return URL to be calculated later. unset($SESSION->fromdiscussion); } else if (!empty($reply)) { // User is writing a new reply. $parententity = $postvault->get_from_id($reply); if (empty($parententity)) { throw new \moodle_exception('invalidparentpostid', 'forum'); } $discussionentity = $discussionvault->get_from_id($parententity->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'); } $capabilitymanager = $managerfactory->get_capability_manager($forumentity); $parent = $postdatamapper->to_legacy_object($parententity); $discussion = $discussiondatamapper->to_legacy_object($discussionentity); $forum = $forumdatamapper->to_legacy_object($forumentity); $course = $forumentity->get_course_record(); $modcontext = $forumentity->get_context(); $coursecontext = context_course::instance($course->id); if (!$cm = get_coursemodule_from_instance("forum", $forum->id, $course->id)) { throw new \moodle_exception('invalidcoursemodule'); } // Ensure lang, theme, etc. is set up properly. MDL-6926. $PAGE->set_cm($cm, $course, $forum); if (!$capabilitymanager->can_reply_to_post($USER, $discussionentity, $parententity)) { if (!isguestuser()) { if (!is_enrolled($coursecontext)) { // User is a guest here! $SESSION->wantsurl = qualified_me(); $SESSION->enrolcancel = get_local_referer(false); redirect(new moodle_url('/enrol/index.php', array('id' => $course->id, 'returnurl' => '/mod/forum/view.php?f=' . $forum->id)), get_string('youneedtoenrol')); } // The forum has been locked. Just redirect back to the discussion page. if (forum_discussion_is_locked($forum, $discussion)) { redirect(new moodle_url('/mod/forum/discuss.php', array('d' => $discussion->id))); } } throw new \moodle_exception('nopostforum', 'forum'); } // Make sure user can post here. if (isset($cm->groupmode) && empty($course->groupmodeforce)) { $groupmode = $cm->groupmode; } else { $groupmode = $course->groupmode; } if ($groupmode == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $modcontext)) { if ($discussion->groupid == -1) { throw new \moodle_exception('nopostforum', 'forum'); } else { if (!groups_is_member($discussion->groupid)) { throw new \moodle_exception('nopostforum', 'forum'); } } } if (!$cm->visible and !has_capability('moodle/course:viewhiddenactivities', $modcontext)) { throw new \moodle_exception("activityiscurrentlyhidden"); } if ($parententity->is_private_reply()) { throw new \moodle_exception('cannotreplytoprivatereply', 'forum'); } // We always are going to honor the preferred format. We are creating a new post. $preferredformat = editors_get_preferred_format(); // Only if there are prefilled contents coming. if (!empty($prefilledpost)) { // If the prefilled post is not HTML and the preferred format is HTML, convert to it. if ($prefilledpostformat != FORMAT_HTML and $preferredformat == FORMAT_HTML) { $prefilledpost = format_text($prefilledpost, $prefilledpostformat, ['context' => $modcontext]); } } // Load up the $post variable. $post = new stdClass(); $post->course = $course->id; $post->forum = $forum->id; $post->discussion = $parent->discussion; $post->parent = $parent->id; $post->subject = $subject ? $subject : $parent->subject; $post->userid = $USER->id; $post->parentpostauthor = $parent->userid; $post->message = $prefilledpost; $post->messageformat = $preferredformat; $post->isprivatereply = $prefilledprivatereply; $canreplyprivately = $capabilitymanager->can_reply_privately_to_post($USER, $parententity); $post->groupid = ($discussion->groupid == -1) ? 0 : $discussion->groupid; // Unsetting this will allow the correct return URL to be calculated later. unset($SESSION->fromdiscussion); } else if (!empty($edit)) { // User is editing their own post. $postentity = $postvault->get_from_id($edit); if (empty($postentity)) { throw new \moodle_exception('invalidpostid', 'forum'); } if ($postentity->has_parent()) { $parententity = $postvault->get_from_id($postentity->get_parent_id()); $parent = $postdatamapper->to_legacy_object($parententity); } $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'); } $capabilitymanager = $managerfactory->get_capability_manager($forumentity); $post = $postdatamapper->to_legacy_object($postentity); $discussion = $discussiondatamapper->to_legacy_object($discussionentity); $forum = $forumdatamapper->to_legacy_object($forumentity); $course = $forumentity->get_course_record(); $modcontext = $forumentity->get_context(); $coursecontext = context_course::instance($course->id); if (!$cm = get_coursemodule_from_instance("forum", $forum->id, $course->id)) { throw new \moodle_exception('invalidcoursemodule'); } $PAGE->set_cm($cm, $course, $forum); if (!($forum->type == 'news' && !$post->parent && $discussion->timestart > time())) { if (((time() - $post->created) > $CFG->maxeditingtime) and !has_capability('mod/forum:editanypost', $modcontext)) { throw new \moodle_exception('maxtimehaspassed', 'forum', '', format_time($CFG->maxeditingtime)); } } if (($post->userid <> $USER->id) and !has_capability('mod/forum:editanypost', $modcontext)) { throw new \moodle_exception('cannoteditposts', 'forum'); } // Load up the $post variable. $post->edit = $edit; $post->course = $course->id; $post->forum = $forum->id; $post->groupid = ($discussion->groupid == -1) ? 0 : $discussion->groupid; if ($postentity->has_parent()) { $canreplyprivately = forum_user_can_reply_privately($modcontext, $parent); } $post = trusttext_pre_edit($post, 'message', $modcontext); // Unsetting this will allow the correct return URL to be calculated later. unset($SESSION->fromdiscussion); } else if (!empty($delete)) { // User is deleting a post. $postentity = $postvault->get_from_id($delete); 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'); } $capabilitymanager = $managerfactory->get_capability_manager($forumentity); $course = $forumentity->get_course_record(); $cm = $forumentity->get_course_module_record(); $modcontext = $forumentity->get_context(); require_login($course, false, $cm); $replycount = $postvault->get_reply_count_for_post_id_in_discussion_id( $USER, $postentity->get_id(), $discussionentity->get_id(), true); if (!empty($confirm) && confirm_sesskey()) { // Do further checks and delete the post. $hasreplies = $replycount > 0; try { $capabilitymanager->validate_delete_post($USER, $discussionentity, $postentity, $hasreplies); if (!$postentity->has_parent()) { forum_delete_discussion( $discussiondatamapper->to_legacy_object($discussionentity), false, $forumentity->get_course_record(), $forumentity->get_course_module_record(), $forumdatamapper->to_legacy_object($forumentity) ); redirect( $urlfactory->get_forum_view_url_from_forum($forumentity), get_string('eventdiscussiondeleted', 'forum'), null, \core\output\notification::NOTIFY_SUCCESS ); } else { forum_delete_post( $postdatamapper->to_legacy_object($postentity), has_capability('mod/forum:deleteanypost', $modcontext), $forumentity->get_course_record(), $forumentity->get_course_module_record(), $forumdatamapper->to_legacy_object($forumentity) ); if ($forumentity->get_type() == 'single') { // Single discussion forums are an exception. // We show the forum itself since it only has one discussion thread. $discussionurl = $urlfactory->get_forum_view_url_from_forum($forumentity); } else { $discussionurl = $urlfactory->get_discussion_view_url_from_discussion($discussionentity); } redirect( forum_go_back_to($discussionurl), get_string('eventpostdeleted', 'forum'), null, \core\output\notification::NOTIFY_SUCCESS ); } } catch (Exception $e) { redirect( $urlfactory->get_discussion_view_url_from_discussion($discussionentity), $e->getMessage(), null, \core\output\notification::NOTIFY_ERROR ); } } else { if (!$capabilitymanager->can_delete_post($USER, $discussionentity, $postentity)) { redirect( $urlfactory->get_discussion_view_url_from_discussion($discussionentity), get_string('cannotdeletepost', 'forum'), null, \core\output\notification::NOTIFY_ERROR ); } $post = $postdatamapper->to_legacy_object($postentity); $forum = $forumdatamapper->to_legacy_object($forumentity); // User just asked to delete something. forum_set_return(); $PAGE->navbar->add(get_string('delete', 'forum')); $PAGE->set_title($course->shortname); $PAGE->set_heading($course->fullname); $PAGE->set_secondary_active_tab('modulepage'); $PAGE->activityheader->disable(); if ($replycount) { if (!has_capability('mod/forum:deleteanypost', $modcontext)) { redirect( forum_go_back_to($urlfactory->get_view_post_url_from_post($postentity)), get_string('couldnotdeletereplies', 'forum'), null, \core\output\notification::NOTIFY_ERROR ); } echo $OUTPUT->header(); if (!$PAGE->has_secondary_navigation()) { echo $OUTPUT->heading(format_string($forum->name), 2); } echo $OUTPUT->confirm(get_string("deletesureplural", "forum", $replycount + 1), "post.php?delete=$delete&confirm=$delete", $CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'#p'.$post->id); $postentities = [$postentity]; if (empty($post->edit)) { $postvault = $vaultfactory->get_post_vault(); $replies = $postvault->get_replies_to_post( $USER, $postentity, // Note: All replies are fetched here as the user has deleteanypost. true, 'created ASC' ); $postentities = array_merge($postentities, $replies); } $rendererfactory = mod_forum\local\container::get_renderer_factory(); $postsrenderer = $rendererfactory->get_single_discussion_posts_renderer(FORUM_MODE_NESTED, true); echo $postsrenderer->render($USER, [$forumentity], [$discussionentity], $postentities); } else { echo $OUTPUT->header(); if (!$PAGE->has_secondary_navigation()) { echo $OUTPUT->heading(format_string($forum->name), 2); } echo $OUTPUT->confirm(get_string("deletesure", "forum", $replycount), "post.php?delete=$delete&confirm=$delete", $CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'#p'.$post->id); $rendererfactory = mod_forum\local\container::get_renderer_factory(); $postsrenderer = $rendererfactory->get_single_discussion_posts_renderer(null, true); echo $postsrenderer->render($USER, [$forumentity], [$discussionentity], [$postentity]); } } echo $OUTPUT->footer(); die; } else if (!empty($prune)) { // Pruning. $postentity = $postvault->get_from_id($prune); 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'); } $capabilitymanager = $managerfactory->get_capability_manager($forumentity); $post = $postdatamapper->to_legacy_object($postentity); $discussion = $discussiondatamapper->to_legacy_object($discussionentity); $forum = $forumdatamapper->to_legacy_object($forumentity); $course = $forumentity->get_course_record(); $modcontext = $forumentity->get_context(); $coursecontext = context_course::instance($course->id); if (!$cm = get_coursemodule_from_instance("forum", $forum->id, $course->id)) { throw new \moodle_exception('invalidcoursemodule'); } if (!$postentity->has_parent()) { redirect( $urlfactory->get_discussion_view_url_from_discussion($discussionentity), get_string('alreadyfirstpost', 'forum'), null, \core\output\notification::NOTIFY_ERROR ); } if (!$capabilitymanager->can_split_post($USER, $discussionentity, $postentity)) { redirect( $urlfactory->get_discussion_view_url_from_discussion($discussionentity), get_string('cannotsplit', 'forum'), null, \core\output\notification::NOTIFY_ERROR ); } $PAGE->set_cm($cm); $PAGE->set_context($modcontext); $PAGE->set_secondary_active_tab('modulepage'); $PAGE->activityheader->disable(); $prunemform = new mod_forum_prune_form(null, array('prune' => $prune, 'confirm' => $prune)); if ($prunemform->is_cancelled()) { redirect(forum_go_back_to($urlfactory->get_discussion_view_url_from_discussion($discussionentity))); } else if ($fromform = $prunemform->get_data()) { // User submits the data. $newdiscussion = new stdClass(); $newdiscussion->course = $discussion->course; $newdiscussion->forum = $discussion->forum; $newdiscussion->name = $name; $newdiscussion->firstpost = $post->id; $newdiscussion->userid = $post->userid; $newdiscussion->groupid = $discussion->groupid; $newdiscussion->assessed = $discussion->assessed; $newdiscussion->usermodified = $post->userid; $newdiscussion->timestart = $discussion->timestart; $newdiscussion->timeend = $discussion->timeend; $newid = $DB->insert_record('forum_discussions', $newdiscussion); $newpost = new stdClass(); $newpost->id = $post->id; $newpost->parent = 0; $newpost->subject = $name; $DB->update_record("forum_posts", $newpost); $postentity = $postvault->get_from_id($postentity->get_id()); forum_change_discussionid($post->id, $newid); // Update last post in each discussion. forum_discussion_update_last_post($discussion->id); forum_discussion_update_last_post($newid); // Fire events to reflect the split.. $params = array( 'context' => $modcontext, 'objectid' => $discussion->id, 'other' => array( 'forumid' => $forum->id, ) ); $event = \mod_forum\event\discussion_updated::create($params); $event->trigger(); $params = array( 'context' => $modcontext, 'objectid' => $newid, 'other' => array( 'forumid' => $forum->id, ) ); $event = \mod_forum\event\discussion_created::create($params); $event->trigger(); $params = array( 'context' => $modcontext, 'objectid' => $post->id, 'other' => array( 'discussionid' => $newid, 'forumid' => $forum->id, 'forumtype' => $forum->type, ) ); $event = \mod_forum\event\post_updated::create($params); $event->add_record_snapshot('forum_discussions', $discussion); $event->trigger(); redirect( forum_go_back_to($urlfactory->get_discussion_view_url_from_post($postentity)), get_string('discussionsplit', 'forum'), null, \core\output\notification::NOTIFY_SUCCESS ); } else { // Display the prune form. $course = $DB->get_record('course', array('id' => $forum->course)); $subjectstr = format_string($post->subject, true); $PAGE->navbar->add($subjectstr, new moodle_url('/mod/forum/discuss.php', array('d' => $discussion->id))); $PAGE->navbar->add(get_string("prunediscussion", "forum")); $PAGE->set_title(format_string($discussion->name).": ".format_string($post->subject)); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); if (!$PAGE->has_secondary_navigation()) { echo $OUTPUT->heading(format_string($forum->name), 2); } echo $OUTPUT->heading(get_string('pruneheading', 'forum'), 3); $prunemform->display(); $postentity = $entityfactory->get_post_from_stdclass($post); $discussionentity = $entityfactory->get_discussion_from_stdclass($discussion); $forumentity = $entityfactory->get_forum_from_stdclass($forum, $modcontext, $cm, $course); $rendererfactory = mod_forum\local\container::get_renderer_factory(); $postsrenderer = $rendererfactory->get_single_discussion_posts_renderer(null, true); echo $postsrenderer->render($USER, [$forumentity], [$discussionentity], [$postentity]); } echo $OUTPUT->footer(); die; } else { throw new \moodle_exception('unknowaction'); } // From now on user must be logged on properly. require_login($course, false, $cm); if (isguestuser()) { // Just in case. throw new \moodle_exception('noguest'); } $thresholdwarning = forum_check_throttling($forum, $cm); $mformpost = new mod_forum_post_form('post.php', [ 'course' => $course, 'cm' => $cm, 'coursecontext' => $coursecontext, 'modcontext' => $modcontext, 'forum' => $forum, 'post' => $post, 'subscribe' => \mod_forum\subscriptions::is_subscribed($USER->id, $forum, null, $cm), 'thresholdwarning' => $thresholdwarning, 'edit' => $edit, 'canreplyprivately' => $canreplyprivately, ], 'post', '', array('id' => 'mformforum')); $draftitemid = file_get_submitted_draft_itemid('attachments'); $postid = empty($post->id) ? null : $post->id; $attachoptions = mod_forum_post_form::attachment_options($forum); file_prepare_draft_area($draftitemid, $modcontext->id, 'mod_forum', 'attachment', $postid, $attachoptions); // Load data into form NOW! if ($USER->id != $post->userid) { // Not the original author, so add a message to the end. $data = new stdClass(); $data->date = userdate($post->created); if ($post->messageformat == FORMAT_HTML) { $data->name = ''. fullname($USER).''; $post->message .= '

('.get_string('editedby', 'forum', $data).')

'; } else { $data->name = fullname($USER); $post->message .= "\n\n(".get_string('editedby', 'forum', $data).')'; } unset($data); } $formheading = ''; if (!empty($parent)) { $heading = get_string("yourreply", "forum"); $formheading = get_string('reply', 'forum'); } else { if ($forum->type == 'qanda') { $heading = get_string('yournewquestion', 'forum'); } else { $heading = get_string('yournewtopic', 'forum'); } } $postid = empty($post->id) ? null : $post->id; $draftideditor = file_get_submitted_draft_itemid('message'); $editoropts = mod_forum_post_form::editor_options($modcontext, $postid); $currenttext = file_prepare_draft_area($draftideditor, $modcontext->id, 'mod_forum', 'post', $postid, $editoropts, $post->message); $discussionid = isset($discussion) ? $discussion->id : null; $discussionsubscribe = \mod_forum\subscriptions::get_user_default_subscription($forum, $coursecontext, $cm, $discussionid); $mformpost->set_data( array( 'attachments' => $draftitemid, 'general' => $heading, 'subject' => $post->subject, 'message' => array( 'text' => $currenttext, 'format' => !isset($post->messageformat) || !is_numeric($post->messageformat) ? editors_get_preferred_format() : $post->messageformat, 'itemid' => $draftideditor ), 'discussionsubscribe' => $discussionsubscribe, 'mailnow' => !empty($post->mailnow), 'userid' => $post->userid, 'parent' => $post->parent, 'discussion' => $post->discussion, 'course' => $course->id, 'isprivatereply' => $post->isprivatereply ?? false ) + $pageparams + (isset($post->format) ? array('format' => $post->format) : array()) + (isset($discussion->timestart) ? array('timestart' => $discussion->timestart) : array()) + (isset($discussion->timeend) ? array('timeend' => $discussion->timeend) : array()) + (isset($discussion->pinned) ? array('pinned' => $discussion->pinned) : array()) + (isset($post->groupid) ? array('groupid' => $post->groupid) : array()) + (isset($discussion->id) ? array('discussion' => $discussion->id) : array()) ); // If we are being redirected via a no_submit_button press OR if the message is being prefilled. // then set the initial 'dirty' state. // - A prefilled post will exist when being redirected from the inpage reply form. // - A no_submit_button press occurs when being redirected from the inpage add new discussion post form. $dirty = $prefilledpost ? true : false; if ($mformpost->no_submit_button_pressed()) { $data = $mformpost->get_submitted_data(); // If a no submit button has been pressed but the default values haven't been then reset the form change. if (!$dirty && isset($data->message['text']) && !empty(trim($data->message['text']))) { $dirty = true; } if (!$dirty && isset($data->message['message']) && !empty(trim($data->message['message']))) { $dirty = true; } } $mformpost->set_initial_dirty_state($dirty); if ($mformpost->is_cancelled()) { if (!isset($discussion->id) || $forum->type === 'single') { // Single forums don't have a discussion page. redirect($urlfactory->get_forum_view_url_from_forum($forumentity)); } else { redirect($urlfactory->get_discussion_view_url_from_discussion($discussionentity)); } } else if ($mformpost->is_submitted() && !$mformpost->no_submit_button_pressed() && $fromform = $mformpost->get_data()) { $errordestination = get_local_referer(false) ?: $urlfactory->get_forum_view_url_from_forum($forumentity); $fromform->itemid = $fromform->message['itemid']; $fromform->messageformat = $fromform->message['format']; $fromform->message = $fromform->message['text']; // WARNING: the $fromform->message array has been overwritten, do not use it anymore! $fromform->messagetrust = trusttext_trusted($modcontext); // Do not clean text here, text cleaning can be done only after conversion to HTML. // Word counting now uses text formatting, there is no need to abuse trusttext_pre_edit() here. if ($fromform->edit) { // Updating a post. unset($fromform->groupid); $fromform->id = $fromform->edit; $message = ''; if (!$capabilitymanager->can_edit_post($USER, $discussionentity, $postentity)) { redirect( $urlfactory->get_view_post_url_from_post($postentity), get_string('cannotupdatepost', 'forum'), null, \core\output\notification::ERROR ); } if (isset($fromform->groupinfo) && $capabilitymanager->can_move_discussions($USER)) { // If the user has access to all groups and they are changing the group, then update the post. if (empty($fromform->groupinfo)) { $fromform->groupinfo = -1; } if (!$capabilitymanager->can_create_discussions($USER, $fromform->groupinfo)) { redirect( $urlfactory->get_view_post_url_from_post($postentity), get_string('cannotupdatepost', 'forum'), null, \core\output\notification::ERROR ); } if ($discussionentity->get_group_id() != $fromform->groupinfo) { $DB->set_field('forum_discussions', 'groupid', $fromform->groupinfo, array('firstpost' => $fromform->id)); } } // When editing first post/discussion. if (!$postentity->has_parent()) { if ($capabilitymanager->can_pin_discussions($USER)) { // Can change pinned if we have capability. $fromform->pinned = !empty($fromform->pinned) ? FORUM_DISCUSSION_PINNED : FORUM_DISCUSSION_UNPINNED; } else { // We don't have the capability to change so keep to previous value. unset($fromform->pinned); } } $updatepost = $fromform; $updatepost->forum = $forum->id; if (!forum_update_post($updatepost, $mformpost)) { throw new \moodle_exception("couldnotupdate", "forum", $errordestination); } forum_trigger_post_updated_event($post, $discussion, $modcontext, $forum); if ($USER->id === $postentity->get_author_id()) { $message .= get_string("postupdated", "forum"); } else { $realuser = \core_user::get_user($postentity->get_author_id()); $message .= get_string("editedpostupdated", "forum", fullname($realuser)); } $subscribemessage = forum_post_subscription($fromform, $forum, $discussion); if ('single' == $forumentity->get_type()) { // Single discussion forums are an exception. // We show the forum itself since it only has one discussion thread. $discussionurl = $urlfactory->get_forum_view_url_from_forum($forumentity); } else { $discussionurl = $urlfactory->get_view_post_url_from_post($postentity); } redirect( forum_go_back_to($discussionurl), $message . $subscribemessage, null, \core\output\notification::NOTIFY_SUCCESS ); } else if ($fromform->discussion) { // Adding a new post to an existing discussion // Before we add this we must check that the user will not exceed the blocking threshold. forum_check_blocking_threshold($thresholdwarning); unset($fromform->groupid); $message = ''; $addpost = $fromform; $addpost->forum = $forum->id; if ($fromform->id = forum_add_new_post($addpost, $mformpost)) { $postentity = $postvault->get_from_id($fromform->id); $fromform->deleted = 0; $subscribemessage = forum_post_subscription($fromform, $forum, $discussion); if (!empty($fromform->mailnow)) { $message .= get_string("postmailnow", "forum"); } else { $message .= '

'.get_string("postaddedsuccess", "forum") . '

'; $message .= '

'.get_string("postaddedtimeleft", "forum", format_time($CFG->maxeditingtime)) . '

'; } if ($forum->type == 'single') { // Single discussion forums are an exception. // We show the forum itself since it only has one discussion thread. $discussionurl = $urlfactory->get_forum_view_url_from_forum($forumentity); } else { $discussionurl = $urlfactory->get_view_post_url_from_post($postentity); } $params = array( 'context' => $modcontext, 'objectid' => $fromform->id, 'other' => array( 'discussionid' => $discussion->id, 'forumid' => $forum->id, 'forumtype' => $forum->type, ) ); $event = \mod_forum\event\post_created::create($params); $event->add_record_snapshot('forum_posts', $fromform); $event->add_record_snapshot('forum_discussions', $discussion); $event->trigger(); // Update completion state. $completion = new completion_info($course); if ($completion->is_enabled($cm) && ($forum->completionreplies || $forum->completionposts)) { $completion->update_state($cm, COMPLETION_COMPLETE); } redirect( forum_go_back_to($discussionurl), $message . $subscribemessage, null, \core\output\notification::NOTIFY_SUCCESS ); } else { throw new \moodle_exception("couldnotadd", "forum", $errordestination); } exit; } else { // Adding a new discussion. // The location to redirect to after successfully posting. $redirectto = new moodle_url('/mod/forum/view.php', array('f' => $fromform->forum)); $fromform->mailnow = empty($fromform->mailnow) ? 0 : 1; $discussion = $fromform; $discussion->name = $fromform->subject; $discussion->timelocked = 0; $newstopic = false; if ($forum->type == 'news' && !$fromform->parent) { $newstopic = true; } if (!empty($fromform->pinned) && $capabilitymanager->can_pin_discussions($USER)) { $discussion->pinned = FORUM_DISCUSSION_PINNED; } else { $discussion->pinned = FORUM_DISCUSSION_UNPINNED; } $allowedgroups = array(); $groupstopostto = array(); // If we are posting a copy to all groups the user has access to. if (isset($fromform->posttomygroups)) { // Post to each of my groups. require_capability('mod/forum:canposttomygroups', $modcontext); // Fetch all of this user's groups. // Note: all groups are returned when in visible groups mode so we must manually filter. $allowedgroups = groups_get_activity_allowed_groups($cm); foreach ($allowedgroups as $groupid => $group) { if ($capabilitymanager->can_create_discussions($USER, $groupid)) { $groupstopostto[] = $groupid; } } } else if (isset($fromform->groupinfo)) { // Use the value provided in the dropdown group selection. $groupstopostto[] = $fromform->groupinfo; $redirectto->param('group', $fromform->groupinfo); } else if (isset($fromform->groupid) && !empty($fromform->groupid)) { // Use the value provided in the hidden form element instead. $groupstopostto[] = $fromform->groupid; $redirectto->param('group', $fromform->groupid); } else { // Use the value for all participants instead. $groupstopostto[] = -1; } // Before we post this we must check that the user will not exceed the blocking threshold. forum_check_blocking_threshold($thresholdwarning); foreach ($groupstopostto as $group) { if (!$capabilitymanager->can_create_discussions($USER, $group)) { throw new \moodle_exception('cannotcreatediscussion', 'forum'); } $discussion->groupid = $group; $message = ''; if ($discussion->id = forum_add_discussion($discussion, $mformpost)) { $params = array( 'context' => $modcontext, 'objectid' => $discussion->id, 'other' => array( 'forumid' => $forum->id, ) ); $event = \mod_forum\event\discussion_created::create($params); $event->add_record_snapshot('forum_discussions', $discussion); $event->trigger(); if ($fromform->mailnow) { $message .= get_string("postmailnow", "forum"); } else { $message .= '

'.get_string("postaddedsuccess", "forum") . '

'; $message .= '

'.get_string("postaddedtimeleft", "forum", format_time($CFG->maxeditingtime)) . '

'; } $subscribemessage = forum_post_subscription($fromform, $forum, $discussion); } else { throw new \moodle_exception("couldnotadd", "forum", $errordestination); } } // Update completion status. $completion = new completion_info($course); if ($completion->is_enabled($cm) && ($forum->completiondiscussions || $forum->completionposts)) { $completion->update_state($cm, COMPLETION_COMPLETE); } // Redirect back to the discussion. redirect( forum_go_back_to($redirectto->out()), $message . $subscribemessage, null, \core\output\notification::NOTIFY_SUCCESS ); } } // This section is only shown after all checks are in place, and the forumentity and any relevant discussion and post // entity are available. if (!empty($discussionentity)) { $titlesubject = format_string($discussionentity->get_name(), true); } else if ('news' == $forumentity->get_type()) { $titlesubject = get_string("addanewtopic", "forum"); } else { $titlesubject = get_string("addanewdiscussion", "forum"); } if (empty($post->edit)) { $post->edit = ''; } if (empty($discussion->name)) { if (empty($discussion)) { $discussion = new stdClass(); } $discussion->name = $forum->name; } $strdiscussionname = ''; if ('single' == $forumentity->get_type()) { // There is only one discussion thread for this forum type. We should // not show the discussion name (same as forum name in this case) in // the breadcrumbs. $strdiscussionname = ''; } else if (!empty($discussionentity)) { // Show the discussion name in the breadcrumbs. $strdiscussionname = format_string($discussionentity->get_name()) . ': '; } $forcefocus = empty($reply) ? null : 'message'; if (!empty($discussion->id)) { $PAGE->navbar->add($titlesubject, $urlfactory->get_discussion_view_url_from_discussion($discussionentity)); } if ($edit) { $PAGE->navbar->add(get_string('editdiscussiontopic', 'forum'), $PAGE->url); } else if ($reply) { $PAGE->navbar->add(get_string('addreply', 'forum')); } else { $PAGE->navbar->add(get_string('addanewdiscussion', 'forum'), $PAGE->url); } $PAGE->set_title("{$course->shortname}: {$strdiscussionname}{$titlesubject}"); $PAGE->set_heading($course->fullname); $PAGE->set_secondary_active_tab("modulepage"); $activityheaderconfig['hidecompletion'] = true; $activityheaderconfig['description'] = ''; // Remove the activity description. $PAGE->activityheader->set_attrs($activityheaderconfig); echo $OUTPUT->header(); if ($edit) { echo $OUTPUT->heading(get_string('editdiscussiontopic', 'forum'), 2); } else if ($reply) { echo $OUTPUT->heading(get_string('replypostdiscussion', 'forum'), 2); } else { echo $OUTPUT->heading(get_string('addanewdiscussion', 'forum'), 2); } // Checkup. if (!empty($parententity) && !$capabilitymanager->can_view_post($USER, $discussionentity, $parententity)) { throw new \moodle_exception('cannotreply', 'forum'); } if (empty($parententity) && empty($edit) && !$capabilitymanager->can_create_discussions($USER, $groupid)) { throw new \moodle_exception('cannotcreatediscussion', 'forum'); } if (!empty($discussionentity) && 'qanda' == $forumentity->get_type()) { $displaywarning = $capabilitymanager->must_post_before_viewing_discussion($USER, $discussionentity); $displaywarning = $displaywarning && !forum_user_has_posted($forumentity->get_id(), $discussionentity->get_id(), $USER->id); if ($displaywarning) { echo $OUTPUT->notification(get_string('qandanotify', 'forum')); } } // If there is a warning message and we are not editing a post we need to handle the warning. if (!empty($thresholdwarning) && !$edit) { // Here we want to throw an exception if they are no longer allowed to post. forum_check_blocking_threshold($thresholdwarning); } if (!empty($parententity)) { $postentities = [$parententity]; if (empty($post->edit)) { if ('qanda' != $forumentity->get_type() || forum_user_can_see_discussion($forum, $discussion, $modcontext)) { $replies = $postvault->get_replies_to_post( $USER, $parententity, $capabilitymanager->can_view_any_private_reply($USER), 'created ASC' ); $postentities = array_merge($postentities, $replies); } } $rendererfactory = mod_forum\local\container::get_renderer_factory(); $postsrenderer = $rendererfactory->get_single_discussion_posts_renderer(FORUM_MODE_THREADED, true); echo $postsrenderer->render($USER, [$forumentity], [$discussionentity], $postentities); } // Call print disclosure for enabled plagiarism plugins. if (!empty($CFG->enableplagiarism)) { require_once($CFG->libdir.'/plagiarismlib.php'); echo plagiarism_print_disclosure($cm->id); } if (!empty($formheading)) { echo $OUTPUT->heading($formheading, 2, array('class' => 'accesshide')); } if (!empty($postentity)) { $data = (object) [ 'tags' => core_tag_tag::get_item_tags_array('mod_forum', 'forum_posts', $postentity->get_id()) ]; $mformpost->set_data($data); } $mformpost->display(); echo $OUTPUT->footer(); PK-B\h#// renderer.phpnu[. /** * This file contains a custom renderer class used by the forum module. * * @package mod_forum * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * A custom renderer class that extends the plugin_renderer_base and * is used by the forum module. * * @package mod_forum * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later **/ class mod_forum_renderer extends plugin_renderer_base { /** * Returns the navigation to the previous and next discussion. * * @param mixed $prev Previous discussion record, or false. * @param mixed $next Next discussion record, or false. * @return string The output. */ public function neighbouring_discussion_navigation($prev, $next) { $html = ''; if ($prev || $next) { $html .= html_writer::start_tag('nav', [ 'class' => 'discussion-nav clearfix', 'aria-label' => get_string('browsediscussions', 'mod_forum'), ]); $html .= html_writer::start_tag('ul'); if ($prev) { $url = new moodle_url('/mod/forum/discuss.php', array('d' => $prev->id)); $html .= html_writer::start_tag('li', array('class' => 'prev-discussion')); $html .= html_writer::link($url, $this->output->larrow() . ' ' . format_string($prev->name), array('aria-label' => get_string('prevdiscussiona', 'mod_forum', format_string($prev->name)), 'class' => 'btn btn-link')); $html .= html_writer::end_tag('li'); } if ($next) { $url = new moodle_url('/mod/forum/discuss.php', array('d' => $next->id)); $html .= html_writer::start_tag('li', array('class' => 'next-discussion')); $html .= html_writer::link($url, format_string($next->name) . ' ' . $this->output->rarrow(), array('aria-label' => get_string('nextdiscussiona', 'mod_forum', format_string($next->name)), 'class' => 'btn btn-link')); $html .= html_writer::end_tag('li'); } $html .= html_writer::end_tag('ul'); $html .= html_writer::end_tag('nav'); } return $html; } /** * This method is used to generate HTML for a subscriber selection form that * uses two user_selector controls * * @param user_selector_base $existinguc * @param user_selector_base $potentialuc * @return string */ public function subscriber_selection_form(user_selector_base $existinguc, user_selector_base $potentialuc) { $output = ''; $formattributes = array(); $formattributes['id'] = 'subscriberform'; $formattributes['action'] = ''; $formattributes['method'] = 'post'; $output .= html_writer::start_tag('form', $formattributes); $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'sesskey', 'value'=>sesskey())); $existingcell = new html_table_cell(); $existingcell->text = $existinguc->display(true); $existingcell->attributes['class'] = 'existing'; $actioncell = new html_table_cell(); $actioncell->text = html_writer::start_tag('div', array()); $actioncell->text .= html_writer::empty_tag('input', array('type'=>'submit', 'name'=>'subscribe', 'value'=>$this->page->theme->larrow.' '.get_string('add'), 'class'=>'actionbutton')); $actioncell->text .= html_writer::empty_tag('br', array()); $actioncell->text .= html_writer::empty_tag('input', array('type'=>'submit', 'name'=>'unsubscribe', 'value'=>$this->page->theme->rarrow.' '.get_string('remove'), 'class'=>'actionbutton')); $actioncell->text .= html_writer::end_tag('div', array()); $actioncell->attributes['class'] = 'actions'; $potentialcell = new html_table_cell(); $potentialcell->text = $potentialuc->display(true); $potentialcell->attributes['class'] = 'potential'; $table = new html_table(); $table->attributes['class'] = 'subscribertable boxaligncenter'; $table->data = array(new html_table_row(array($existingcell, $actioncell, $potentialcell))); $output .= html_writer::table($table); $output .= html_writer::end_tag('form'); return $output; } /** * This function generates HTML to display a subscriber overview, primarily used on * the subscribers page if editing was turned off * * @param array $users * @param object $forum * @param object $course * @return string */ public function subscriber_overview($users, $forum , $course) { $output = ''; $modinfo = get_fast_modinfo($course); if (!$users || !is_array($users) || count($users)===0) { $output .= $this->output->notification( get_string("nosubscribers", "forum"), \core\output\notification::NOTIFY_INFO, false); } else if (!isset($modinfo->instances['forum'][$forum->id])) { $output .= $this->output->heading(get_string("invalidmodule", "error")); } else { $cm = $modinfo->instances['forum'][$forum->id]; // TODO Does not support custom user profile fields (MDL-70456). $canviewemail = in_array('email', \core_user\fields::get_identity_fields(context_module::instance($cm->id), false)); $strparams = new stdclass(); $strparams->name = format_string($forum->name); $strparams->count = count($users); $output .= $this->output->heading(get_string("subscriberstowithcount", "forum", $strparams)); $table = new html_table(); $table->id = 'subscribers-table'; $table->head = []; $table->head[] = get_string('pictureofuser'); $table->head[] = get_string('fullname'); if ($canviewemail) { $table->head[] = get_string('email'); } $table->cellpadding = 5; $table->cellspacing = 5; $table->tablealign = 'center'; $table->data = array(); foreach ($users as $user) { $info = array($this->output->user_picture($user, array('courseid'=>$course->id)), fullname($user)); if ($canviewemail) { array_push($info, $user->email); } $table->data[] = $info; } $output .= html_writer::table($table); } return $output; } /** * This is used to display a control containing all of the subscribed users so that * it can be searched * * @param user_selector_base $existingusers * @return string */ public function subscribed_users(user_selector_base $existingusers) { $output = $this->output->box_start('subscriberdiv boxaligncenter'); $output .= html_writer::tag('p', get_string('forcesubscribed', 'forum')); $output .= $existingusers->display(true); $output .= $this->output->box_end(); return $output; } /** * Generate the HTML for an icon to be displayed beside the subject of a timed discussion. * * @param object $discussion * @param bool $visiblenow Indicicates that the discussion is currently * visible to all users. * @return string */ public function timed_discussion_tooltip($discussion, $visiblenow) { $dates = array(); if ($discussion->timestart) { $dates[] = get_string('displaystart', 'mod_forum').': '.userdate($discussion->timestart); } if ($discussion->timeend) { $dates[] = get_string('displayend', 'mod_forum').': '.userdate($discussion->timeend); } $str = $visiblenow ? 'timedvisible' : 'timedhidden'; $dates[] = get_string($str, 'mod_forum'); $tooltip = implode("\n", $dates); return $this->pix_icon('i/calendar', $tooltip, 'moodle', array('class' => 'smallicon timedpost')); } /** * Display a forum post in the relevant context. * * @param \mod_forum\output\forum_post $post The post to display. * @return string */ public function render_forum_post_email(\mod_forum\output\forum_post_email $post) { $data = $post->export_for_template($this, $this->target === RENDERER_TARGET_TEXTEMAIL); return $this->render_from_template('mod_forum/' . $this->forum_post_template(), $data); } /** * The template name for this renderer. * * @return string */ public function forum_post_template() { return 'forum_post'; } /** * Create the inplace_editable used to select forum digest options. * * @param stdClass $forum The forum to create the editable for. * @param int $value The current value for this user * @return inplace_editable */ public function render_digest_options($forum, $value) { $options = forum_get_user_digest_options(); $editable = new \core\output\inplace_editable( 'mod_forum', 'digestoptions', $forum->id, true, $options[$value], $value ); $editable->set_type_select($options); return $editable; } /** * Render quick search form. * * @param \mod_forum\output\quick_search_form $form The renderable. * @return string rendered HTML string from the template. */ public function render_quick_search_form(\mod_forum\output\quick_search_form $form) { if (strpos($this->page->url->get_path(), "index.php")) { return $this->render_from_template('mod_forum/quick_search_form', $form->export_for_template($this)); } return $this->render_from_template('mod_forum/forum_new_discussion_actionbar', $form->export_for_template($this)); } /** * Render the view action area. * * @param \mod_forum\output\forum_actionbar $actionbar forum_actionbar object. * @return string rendered HTML string */ public function render_activity_actionbar(\mod_forum\output\forum_actionbar $actionbar): string { return $this->render_from_template('mod_forum/forum_actionbar', $actionbar->export_for_template($this)); } /** * Render the subscription action area. * * @param \mod_forum\output\subscription_actionbar $subscriptionactionbar subscription_actionbar object. * @return bool|string rendered HTML string. */ public function subscription_actionbar(\mod_forum\output\subscription_actionbar $subscriptionactionbar): string { return $this->render_from_template('mod_forum/forum_subscription_action', $subscriptionactionbar->export_for_template($this)); } /** * Render big search form. * * @param \mod_forum\output\big_search_form $form The renderable. * @return string */ public function render_big_search_form(\mod_forum\output\big_search_form $form) { return $this->render_from_template('mod_forum/big_search_form', $form->export_for_template($this)); } } PK-B\Ƒ db/tag.phpnu[. /** * Tag areas in component mod_forum * * @package mod_forum * @copyright 2017 Andrew Hancox * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $tagareas = array( array( 'itemtype' => 'forum_posts', 'component' => 'mod_forum', 'callback' => 'mod_forum_get_tagged_posts', 'callbackfile' => '/mod/forum/locallib.php', ), ); PK-B\^_ _ db/services.phpnu[. /** * Forum external functions and service definitions. * * @package mod_forum * @copyright 2012 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $functions = array( 'mod_forum_get_forums_by_courses' => array( 'classname' => 'mod_forum_external', 'methodname' => 'get_forums_by_courses', 'classpath' => 'mod/forum/externallib.php', 'description' => 'Returns a list of forum instances in a provided set of courses, if no courses are provided then all the forum instances the user has access to will be returned.', 'type' => 'read', 'capabilities' => 'mod/forum:viewdiscussion', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_forum_get_discussion_posts' => array( 'classname' => 'mod_forum_external', 'methodname' => 'get_discussion_posts', 'classpath' => 'mod/forum/externallib.php', 'description' => 'Returns a list of forum posts for a discussion.', 'type' => 'read', 'ajax' => true, 'capabilities' => 'mod/forum:viewdiscussion, mod/forum:viewqandawithoutposting', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_forum_get_forum_discussions' => array( 'classname' => 'mod_forum_external', 'methodname' => 'get_forum_discussions', 'classpath' => 'mod/forum/externallib.php', 'description' => 'Returns a list of forum discussions optionally sorted and paginated.', 'type' => 'read', 'capabilities' => 'mod/forum:viewdiscussion, mod/forum:viewqandawithoutposting', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_forum_view_forum' => array( 'classname' => 'mod_forum_external', 'methodname' => 'view_forum', 'classpath' => 'mod/forum/externallib.php', 'description' => 'Trigger the course module viewed event and update the module completion status.', 'type' => 'write', 'capabilities' => 'mod/forum:viewdiscussion', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_forum_view_forum_discussion' => array( 'classname' => 'mod_forum_external', 'methodname' => 'view_forum_discussion', 'classpath' => 'mod/forum/externallib.php', 'description' => 'Trigger the forum discussion viewed event.', 'type' => 'write', 'capabilities' => 'mod/forum:viewdiscussion', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_forum_add_discussion_post' => array( 'classname' => 'mod_forum_external', 'methodname' => 'add_discussion_post', 'classpath' => 'mod/forum/externallib.php', 'description' => 'Create new posts into an existing discussion.', 'type' => 'write', 'ajax' => true, 'capabilities' => 'mod/forum:replypost', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_forum_add_discussion' => array( 'classname' => 'mod_forum_external', 'methodname' => 'add_discussion', 'classpath' => 'mod/forum/externallib.php', 'description' => 'Add a new discussion into an existing forum.', 'type' => 'write', 'capabilities' => 'mod/forum:startdiscussion', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_forum_can_add_discussion' => array( 'classname' => 'mod_forum_external', 'methodname' => 'can_add_discussion', 'classpath' => 'mod/forum/externallib.php', 'description' => 'Check if the current user can add discussions in the given forum (and optionally for the given group).', 'type' => 'read', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_forum_get_forum_access_information' => array( 'classname' => 'mod_forum_external', 'methodname' => 'get_forum_access_information', 'description' => 'Return capabilities information for a given forum.', 'type' => 'read', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), 'mod_forum_set_subscription_state' => array( 'classname' => 'mod_forum_external', 'methodname' => 'set_subscription_state', 'classpath' => 'mod/forum/externallib.php', 'description' => 'Set the subscription state', 'type' => 'write', 'ajax' => true, 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), 'mod_forum_set_lock_state' => array( 'classname' => 'mod_forum_external', 'methodname' => 'set_lock_state', 'classpath' => 'mod/forum/externallib.php', 'description' => 'Set the lock state for the discussion', 'type' => 'write', 'ajax' => true, 'capabilities' => 'moodle/course:manageactivities', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), 'mod_forum_toggle_favourite_state' => array( 'classname' => 'mod_forum_external', 'methodname' => 'toggle_favourite_state', 'classpath' => 'mod/forum/externallib.php', 'description' => 'Toggle the favourite state', 'type' => 'write', 'ajax' => true, 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), 'mod_forum_set_pin_state' => array( 'classname' => 'mod_forum_external', 'methodname' => 'set_pin_state', 'classpath' => 'mod/forum/externallib.php', 'description' => 'Set the pin state', 'type' => 'write', 'ajax' => true, 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), 'mod_forum_delete_post' => array( 'classname' => 'mod_forum_external', 'methodname' => 'delete_post', 'classpath' => 'mod/forum/externallib.php', 'description' => 'Deletes a post or a discussion completely when the post is the discussion topic.', '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) ), ); PK-B\gdb/messageinbound_handlers.phpnu[. /** * Message Inbound Handlers for mod_forum. * * @package mod_forum * @copyright 2014 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $handlers = array( array( 'classname' => '\mod_forum\message\inbound\reply_handler', ), ); PK-B\7db/subplugins.jsonnu[{ "subplugintypes": { "forumreport": "report" }, "plugintypes": { "forumreport": "mod/forum/report" } } PK-B\; db/log.phpnu[. /** * Definition of log events * * @package mod_forum * @category log * @copyright 2010 Petr Skoda (http://skodak.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $DB; // TODO: this is a hack, we should really do something with the SQL in SQL tables. $logs = array( array('module' => 'forum', 'action' => 'add', 'mtable' => 'forum', 'field' => 'name'), array('module' => 'forum', 'action' => 'update', 'mtable' => 'forum', 'field' => 'name'), array('module' => 'forum', 'action' => 'add discussion', 'mtable' => 'forum_discussions', 'field' => 'name'), array('module' => 'forum', 'action' => 'add post', 'mtable' => 'forum_posts', 'field' => 'subject'), array('module' => 'forum', 'action' => 'update post', 'mtable' => 'forum_posts', 'field' => 'subject'), array('module' => 'forum', 'action' => 'user report', 'mtable' => 'user', 'field' => $DB->sql_concat('firstname', "' '", 'lastname')), array('module' => 'forum', 'action' => 'move discussion', 'mtable' => 'forum_discussions', 'field' => 'name'), array('module' => 'forum', 'action' => 'view subscribers', 'mtable' => 'forum', 'field' => 'name'), array('module' => 'forum', 'action' => 'view discussion', 'mtable' => 'forum_discussions', 'field' => 'name'), array('module' => 'forum', 'action' => 'view forum', 'mtable' => 'forum', 'field' => 'name'), array('module' => 'forum', 'action' => 'subscribe', 'mtable' => 'forum', 'field' => 'name'), array('module' => 'forum', 'action' => 'unsubscribe', 'mtable' => 'forum', 'field' => 'name'), array('module' => 'forum', 'action' => 'pin discussion', 'mtable' => 'forum_discussions', 'field' => 'name'), array('module' => 'forum', 'action' => 'unpin discussion', 'mtable' => 'forum_discussions', 'field' => 'name'), );PK-B\ db/tasks.phpnu[. /** * Definition of Forum scheduled tasks. * * @package mod_forum * @category task * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $tasks = array( array( 'classname' => 'mod_forum\task\cron_task', 'blocking' => 0, 'minute' => '*', 'hour' => '*', 'day' => '*', 'month' => '*', 'dayofweek' => '*' ) ); PK-B\// db/access.phpnu[. /** * Plugin capabilities * * @package mod_forum * @copyright 2006 Martin Dougiamas * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'mod/forum:addinstance' => array( 'riskbitmask' => RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => array( 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'moodle/course:manageactivities' ), 'mod/forum:viewdiscussion' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'frontpage' => CAP_ALLOW, // needed especially for news on the frontpage 'guest' => CAP_ALLOW, 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:viewhiddentimedposts' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:startdiscussion' => array( 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:replypost' => array( 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:addnews' => array( 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:replynews' => array( 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:viewrating' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:viewanyrating' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:viewallratings' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'mod/forum:viewanyrating' ), 'mod/forum:rate' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:postprivatereply' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:readprivatereplies' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:createattachment' => array( 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:deleteownpost' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:deleteanypost' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:splitdiscussions' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:movediscussions' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:pindiscussions' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:editanypost' => array( 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:viewqandawithoutposting' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:viewsubscribers' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:managesubscriptions' => array( 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:postwithoutthrottling' => array( 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:exportdiscussion' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:exportforum' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW, ) ), 'mod/forum:exportpost' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:exportownpost' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW, 'student' => CAP_ALLOW, ) ), 'mod/forum:addquestion' => array( 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:allowforcesubscribe' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'frontpage' => CAP_ALLOW ) ), 'mod/forum:canposttomygroups' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:canoverridediscussionlock' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/forum:canoverridecutoff' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'mod/forum:canoverridediscussionlock' ), 'mod/forum:cantogglefavourite' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'user' => CAP_ALLOW ) ), 'mod/forum:grade' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW, ], ], 'mod/forum:canmailnow' => [ 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ] ], ); PK-B\uudb/upgrade.phpnu[. /** * This file keeps track of upgrades to * the forum module * * Sometimes, changes between versions involve * alterations to database structures and other * major things that may break installations. * * The upgrade function in this file will attempt * to perform all the necessary actions to upgrade * your older installation to the current version. * * If there's something it cannot do itself, it * will tell you what you need to do. * * The commands in here will all be database-neutral, * using the methods of database_manager class * * Please do not forget to use upgrade_set_timeout() * before any action that may take longer time to finish. * * @package mod_forum * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ function xmldb_forum_upgrade($oldversion) { // Automatically generated Moodle v4.2.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.3.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.4.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. return true; } PK-B\"Du=u=db/install.xmlnu[
PK-B\@ll db/events.phpnu[. /** * Forum event handler definition. * * @package mod_forum * @category event * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // List of observers. $observers = array( array( 'eventname' => '\core\event\user_enrolment_deleted', 'callback' => 'mod_forum_observer::user_enrolment_deleted', ), array( 'eventname' => '\core\event\role_assigned', 'callback' => 'mod_forum_observer::role_assigned' ), array( 'eventname' => '\core\event\course_module_created', 'callback' => 'mod_forum_observer::course_module_created', ), array( 'eventname' => '\core\event\course_created', 'callback' => 'mod_forum_observer::course_created', ), array( 'eventname' => '\core\event\course_updated', 'callback' => 'mod_forum_observer::course_updated', ), ); PK-B\(db/messages.phpnu[. /** * Defines message providers (types of messages being sent) * * @package mod_forum * @copyright 1999 onwards Martin Dougiamas http://moodle.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $messageproviders = array ( // Ordinary single forum posts. 'posts' => array( 'defaults' => [ 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, 'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, 'airnotifier' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, ], ), // Forum digest messages. 'digests' => [ 'defaults' => [ 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, 'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, ], ], ); PK-B\6@ db/caches.phpnu[. /** * Defined caches used internally by the plugin. * * @package mod_forum * @category cache * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $definitions = [ 'forum_is_tracked' => [ 'mode' => cache_store::MODE_REQUEST, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true ], 'forum_count_discussions' => [ 'mode' => cache_store::MODE_REQUEST, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, 'invalidationevents' => [ 'changesinforumdiscussions', ], ], ]; PK-B\f(lang/en/forum.phpnu[. /** * Strings for component 'mod_forum', language 'en', branch 'MOODLE_20_STABLE' * * @package mod_forum * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['activitydate:due'] = 'Due:'; $string['activityoverview'] = 'There are new forum posts'; $string['actionsforgraderinterface'] = 'Actions for the grader interface'; $string['actionsforpost'] = 'Actions for post'; $string['addanewdiscussion'] = 'Add discussion topic'; $string['addanewquestion'] = 'Add a new question'; $string['addanewtopic'] = 'Add a new topic'; $string['addreply'] = 'Add reply'; $string['addtofavourites'] = 'Star this discussion'; $string['advancedsearch'] = 'Advanced search'; $string['alldiscussions'] = 'All discussions'; $string['allforums'] = 'All forums'; $string['allowdiscussions'] = 'Can a {$a} post to this forum?'; $string['allowsallsubscribe'] = 'This forum allows everyone to choose whether to subscribe or not.'; $string['allowsdiscussions'] = 'This forum allows each person to start one discussion topic.'; $string['allsubscribe'] = 'Subscribe to all forums'; $string['allunsubscribe'] = 'Unsubscribe from all forums'; $string['allusers'] = 'All users'; $string['alreadyfirstpost'] = 'This is already the first post in the discussion'; $string['announcementsettings'] = 'Default announcement forum settings'; $string['announcementsettings_help'] = 'These settings define the defaults for all announcement forums.'; $string['anyfile'] = 'Any file'; $string['areaattachment'] = 'Attachments'; $string['areapost'] = 'Messages'; $string['attachment'] = 'Attachment'; $string['attachmentname'] = 'Attachment {$a}'; $string['attachmentnameandfilesize'] = '{$a->name} ({$a->size})'; $string['attachment_help'] = 'You can optionally attach one or more files to a forum post. If you attach an image, it will be displayed after the message.'; $string['attachmentnopost'] = 'You cannot export attachments without a post id'; $string['attachments'] = 'Attachments'; $string['attachmentswordcount'] = 'Attachments and word count'; $string['authorreplyingprivatelytoauthor'] = '{$a->respondant} replying privately to {$a->author}'; $string['authorreplyingtoauthor'] = '{$a->respondant} replying to {$a->author}'; $string['availability'] = 'Availability'; $string['blockafter'] = 'Post threshold for blocking'; $string['blockafter_help'] = 'This setting specifies the maximum number of posts which a user can post in the given time period. Users with the capability mod/forum:postwithoutthrottling are exempt from post limits.'; $string['blockperiod'] = 'Time period for blocking'; $string['blockperiod_help'] = 'Students can be blocked from posting more than a given number of posts in a given time period. Users with the capability mod/forum:postwithoutthrottling are exempt from post limits.'; $string['blockperioddisabled'] = 'Don\'t block'; $string['blogforum'] = 'Standard forum displayed in a blog-like format'; $string['browsediscussions'] = 'Browse discussions'; $string['bynameondate'] = 'by {$a->name} - {$a->date}'; $string['cachedef_forum_count_discussions'] = 'Forum discussions count'; $string['cachedef_forum_is_tracked'] = 'Forum tracking status for user'; $string['calendardue'] = '{$a} is due'; $string['cancelreply'] = 'Cancel reply'; $string['cannotadd'] = 'Could not add the discussion for this forum'; $string['cannotadddiscussion'] = 'Adding discussions to this forum requires group membership.'; $string['cannotadddiscussionall'] = 'You do not have permission to add a new discussion topic for all participants.'; $string['cannotadddiscussiongroup'] = 'You are not able to create a discussion because you are not a member of any group.'; $string['cannotaddsubscriber'] = 'Could not add subscriber with id {$a} to this forum!'; $string['cannotaddteacherforumto'] = 'Could not add converted teacher forum instance to section 0 in the course'; $string['cannotcreatediscussion'] = 'Could not create new discussion'; $string['cannotcreateinstanceforteacher'] = 'Could not create new course module instance for the teacher forum'; $string['cannotdeletepost'] = 'You can\'t delete this post!'; $string['cannoteditposts'] = 'You can\'t edit other people\'s posts!'; $string['cannotexportforum'] = 'You cannot export this forum'; $string['cannotfinddiscussion'] = 'Could not find the discussion in this forum'; $string['cannotfindfirstpost'] = 'Could not find the first post in this forum'; $string['cannotfindorcreateforum'] = 'Could not find or create a main announcements forum for the site'; $string['cannotfindparentpost'] = 'Could not find top parent of post {$a}'; $string['cannotmovefromsingleforum'] = 'Cannot move discussion from a simple single discussion forum'; $string['cannotmovenotvisible'] = 'Forum not visible'; $string['cannotmovetonotexist'] = 'You can\'t move to that forum - it doesn\'t exist!'; $string['cannotmovetonotfound'] = 'Target forum not found in this course.'; $string['cannotmovetosingleforum'] = 'Cannot move discussion to a simple single discussion forum'; $string['cannotpindiscussions'] = 'Sorry, you do not have permission to pin discussions.'; $string['cannotpurgecachedrss'] = 'Could not purge the cached RSS feeds for the source and/or destination forum(s) - check your file permissionsforums'; $string['cannotremovesubscriber'] = 'Could not remove subscriber with id {$a} from this forum!'; $string['cannotreply'] = 'You cannot reply to this post'; $string['cannotsplit'] = 'Discussions from this forum cannot be split'; $string['cannotsubscribe'] = 'Sorry, but you must be a group member to subscribe.'; $string['cannotfavourite'] = 'Sorry, but you do not have the permission to star discussions.'; $string['cannottrack'] = 'Could not stop tracking that forum'; $string['cannotunsubscribe'] = 'Could not unsubscribe you from that forum'; $string['cannotupdatepost'] = 'You can not update this post'; $string['cannotuseseperategroupsandsingletopic'] = 'Separate groups can\'t be used with a single simple discussion.'; $string['cannotusesingletopicandseperategroups'] = 'A single simple discussion can\'t be used with separate groups.'; $string['cannotviewpostyet'] = 'You can\'t read other students\' posts in this discussion yet because you haven\'t posted.'; $string['cannotviewusersposts'] = 'There are no posts made by this user that you are able to view.'; $string['cleanreadtime'] = 'Mark old posts as read hour'; $string['clicktolockdiscussion'] = 'Click to lock this discussion'; $string['clicktounlockdiscussion'] = 'Click to unlock this discussion'; $string['clicktounsubscribe'] = 'You are subscribed to this discussion. Click to unsubscribe.'; $string['clicktosubscribe'] = 'You are not subscribed to this discussion. Click to subscribe.'; $string['clicktounfavourite'] = 'You have starred this discussion. Click to unstar.'; $string['clicktofavourite'] = 'You have not starred this discussion. Click to star.'; $string['close'] = 'Close'; $string['closegrader'] = 'Close grader'; $string['completiondetail:discussions'] = 'Start discussions: {$a}'; $string['completiondetail:replies'] = 'Post replies: {$a}'; $string['completiondetail:posts'] = 'Make forum posts: {$a}'; $string['completiondiscussions'] = 'Start discussions'; $string['completiondiscussionsdesc'] = 'Student must create at least {$a} discussion(s)'; $string['completionpostsinforums'] = 'Post in forums'; $string['completionposts'] = 'Start discussions or post replies'; $string['completionpostsdesc'] = 'Student must post at least {$a} discussion(s) or reply/replies'; $string['completionpostshelp'] = 'requiring discussions or replies to complete'; $string['completionreplies'] = 'Post replies'; $string['completionrepliesdesc'] = 'Student must post at least {$a} reply/replies'; $string['configcleanreadtime'] = 'The hour of the day to clean old posts from the \'read\' table.'; $string['configdigestmailtime'] = 'People who choose to have emails sent to them in digest form will be emailed the digest daily. This setting controls which time of day the daily mail will be sent (the next cron that runs after this hour will send it).'; $string['configdisplaymode'] = 'The default display mode for discussions if one isn\'t set.'; $string['configenablerssfeeds'] = 'This switch will enable the possibility of RSS feeds for all forums. You will still need to turn feeds on manually in the settings for each forum.'; $string['configenabletimedposts'] = 'Set to \'yes\' if you want to allow setting of display periods when posting a new forum discussion.'; $string['configlongpost'] = 'Any post over this length (in characters not including HTML) is considered long. Posts displayed on the site home, social format course pages, or user profiles are shortened to a natural break somewhere between the forum_shortpost and forum_longpost values.'; $string['configmanydiscussions'] = 'Maximum number of discussions shown in a forum per page'; $string['configmaxattachments'] = 'Default maximum number of attachments allowed per post.'; $string['configmaxbytes'] = 'Default maximum size for all forum attachments on the site (subject to course limits and other local settings)'; $string['configoldpostdays'] = 'Number of days old any post is considered read.'; $string['configreplytouser'] = 'When a forum post is mailed out, should it contain the user\'s email address so that recipients can reply personally rather than via the forum? Even if set to \'Yes\' users can choose in their profile to keep their email address secret.'; $string['configrsstypedefault'] = 'If RSS feeds are enabled, sets the default activity type.'; $string['configrssarticlesdefault'] = 'If RSS feeds are enabled, sets the default number of articles (either discussions or posts).'; $string['configsubscriptiontype'] = 'Default setting for subscription mode.'; $string['configshortpost'] = 'Any post under this length (in characters not including HTML) is considered short (see below).'; $string['configtrackingtype'] = 'Default setting for read tracking.'; $string['configtrackreadposts'] = 'Set to \'yes\' if you want to track read/unread for each user.'; $string['configusermarksread'] = 'If \'yes\', the user must manually mark a post as read. If \'no\', when the post is viewed it is marked as read.'; $string['confirmsubscribediscussion'] = 'Do you really want to subscribe to discussion \'{$a->discussion}\' in forum \'{$a->forum}\'?'; $string['confirmunsubscribediscussion'] = 'Do you really want to unsubscribe from discussion \'{$a->discussion}\' in forum \'{$a->forum}\'?'; $string['confirmsubscribe'] = 'Do you really want to subscribe to forum \'{$a}\'?'; $string['confirmunsubscribe'] = 'Do you really want to unsubscribe from forum \'{$a}\'?'; $string['couldnotadd'] = 'Could not add your post due to an unknown error'; $string['couldnotdeletereplies'] = 'Sorry, that cannot be deleted as people have already responded to it'; $string['couldnotupdate'] = 'Could not update your post due to an unknown error'; $string['created'] = 'Created'; $string['crontask'] = 'Forum mailings and maintenance jobs'; $string['cutoffdate'] = 'Cut-off date'; $string['cutoffdate_help'] = 'If set, the forum will not accept posts after this date.'; $string['cutoffdatereached'] = 'The cut-off date for posting to this forum is reached so you can no longer post to it.'; $string['cutoffdatevalidation'] = 'The cut-off date cannot be earlier than the due date.'; $string['delete'] = 'Delete'; $string['deleteddiscussion'] = 'The discussion topic has been deleted'; $string['deletedpost'] = 'The post has been deleted'; $string['deletedposts'] = 'Those posts have been deleted'; $string['deleteduser'] = 'Deleted user'; $string['deletesure'] = 'Are you sure you want to delete this post?'; $string['deletesureplural'] = 'Are you sure you want to delete this post and all replies? ({$a} posts)'; $string['digestmailheader'] = 'This is your daily digest of new posts from the {$a->sitename} forums. To change your default forum email preferences, go to {$a->userprefs}.'; $string['digestmailpost'] = 'Change your forum digest preferences'; $string['digestmailpostlink'] = 'Change your forum digest preferences: {$a}'; $string['digestmailprefs'] = 'your user profile'; $string['digestmailsubject'] = '{$a}: forum digest'; $string['digestmailtime'] = 'Hour to send digest emails'; $string['digestsentusers'] = 'Email digests successfully sent to {$a} users.'; $string['disallowsubscribe'] = 'Subscriptions not allowed'; $string['disallowsubscription'] = 'Subscription'; $string['disallowsubscription_help'] = 'This forum has been configured so that you cannot subscribe to discussions.'; $string['disallowsubscribeteacher'] = 'Subscriptions not allowed (except for teachers)'; $string['discussion'] = 'Discussion'; $string['discussionlistsortbycreatedasc'] = 'Sort by creation date in ascending order'; $string['discussionlistsortbycreateddesc'] = 'Sort by creation date in descending order'; $string['discussionlistsortbydiscussionasc'] = 'Sort by discussion name in ascending order'; $string['discussionlistsortbydiscussiondesc'] = 'Sort by discussion name in descending order'; $string['discussionlistsortbygroupasc'] = 'Sort by group in ascending order'; $string['discussionlistsortbygroupdesc'] = 'Sort by group in descending order'; $string['discussionlistsortbylastpostdesc'] = 'Sort by last post creation date in descending order'; $string['discussionlistsortbylastpostasc'] = 'Sort by last post creation date in ascending order'; $string['discussionlistsortbyrepliesasc'] = 'Sort by number of replies in ascending order'; $string['discussionlistsortbyrepliesdesc'] = 'Sort by number of replies in descending order'; $string['discussionlistsortbystarterasc'] = 'Sort by discussion starter name in ascending order'; $string['discussionlistsortbystarterdesc'] = 'Sort by discussion starter name in descending order'; $string['discussionlocked'] = 'This discussion has been locked so you can no longer reply to it.'; $string['discussionlockingheader'] = 'Discussion locking'; $string['discussionlockingdisabled'] = 'Do not lock discussions'; $string['discussionmoved'] = 'This discussion has been moved to \'{$a}\'.'; $string['discussionmovedpost'] = 'This discussion has been moved to here in the forum {$a->forumname}'; $string['discussionname'] = 'Discussion name'; $string['discussionnownotsubscribed'] = '{$a->name} will NOT be notified of new posts in \'{$a->discussion}\' of \'{$a->forum}\''; $string['discussionnowsubscribed'] = 'You will be notified of new posts in \'{$a->discussion}\' in the forum \'{$a->forum}\'.'; $string['discussionpin'] = 'Pin'; $string['discussionpinned'] = 'Pinned'; $string['discussionpinned_help'] = 'Pinned discussions will appear at the top of a forum.'; $string['discussionsplit'] = 'Discussion has been split'; $string['discussionsubscribed'] = 'You are now subscribed to this discussion.'; $string['discussionsubscribestop'] = 'I don\'t want to be notified of new posts in this discussion'; $string['discussionsubscribestart'] = 'Send me notifications of new posts in this discussion'; $string['discussionsubscription'] = 'Discussion subscription'; $string['discussionsubscription_help'] = 'Subscribing to a discussion means you will receive notifications of new posts to that discussion.'; $string['discussions'] = 'Discussions'; $string['discussionsstartedby'] = 'Discussions started by {$a}'; $string['discussionstartedby'] = 'Discussion started by {$a}'; $string['discussionsstartedbyrecent'] = 'Discussions recently started by {$a}'; $string['discussionsstartedbyuserincourse'] = 'Discussions started by {$a->fullname} in {$a->coursename}'; $string['discussionsstartedincourse'] = 'Discussions started in {$a}'; $string['discussionunpin'] = 'Unpin'; $string['discussionunsubscribed'] = 'You are now unsubscribed from this discussion.'; $string['discussthistopic'] = 'Discuss this topic'; $string['discusstopicname'] = 'Discuss the topic: {$a}'; $string['displayend'] = 'Display end'; $string['displayend_help'] = 'This setting specifies whether a forum post should be hidden after a certain date. Note that administrators can always view forum posts.'; $string['displayenddate'] = 'Display end: {$a}.'; $string['displaymode'] = 'Display mode'; $string['displayperiod'] = 'Display period'; $string['displaystart'] = 'Display start'; $string['displaystart_help'] = 'This setting specifies whether a forum post should be displayed from a certain date. Note that administrators can always view forum posts.'; $string['displaystartdate'] = 'Display start: {$a}.'; $string['displaywordcount'] = 'Display word count'; $string['displaywordcount_help'] = 'This setting specifies whether the word count of each post should be displayed or not.'; $string['duedate'] = 'Due date'; $string['duedate_help'] = 'This is when posting in the forum is due. Although this date is displayed in the calendar as the due date for the forum, posting will still be allowed after this date. Set a forum cut-off date to prevent posting to the forum after a certain date.'; $string['duedatetodisplayincalendar'] = 'Due date to display in calendar'; $string['eachuserforum'] = 'Each person posts one discussion'; $string['edit'] = 'Edit'; $string['editdiscussion'] = 'Edit discussion'; $string['editdiscussiontopic'] = 'Edit discussion topic'; $string['editedby'] = 'Edited by {$a->name} - original submission {$a->date}'; $string['editedpostupdated'] = '{$a}\'s post was updated'; $string['editing'] = 'Editing'; $string['eventcoursesearched'] = 'Course searched'; $string['eventdiscussioncreated'] = 'Discussion created'; $string['eventdiscussionupdated'] = 'Discussion updated'; $string['eventdiscussiondeleted'] = 'Discussion deleted'; $string['eventdiscussionlockupdated'] = 'Discussion lock updated'; $string['eventdiscussionmoved'] = 'Discussion moved'; $string['eventdiscussionviewed'] = 'Discussion viewed'; $string['eventdiscussionsubscriptioncreated'] = 'Discussion subscription created'; $string['eventdiscussionsubscriptiondeleted'] = 'Discussion subscription deleted'; $string['eventdiscussionpinned'] = 'Discussion pinned'; $string['eventdiscussionunpinned'] = 'Discussion unpinned'; $string['eventforumsubscriptionupdated'] = 'Subscription mode updated'; $string['eventuserreportviewed'] = 'User report viewed'; $string['eventpostcreated'] = 'Post created'; $string['eventpostdeleted'] = 'Post deleted'; $string['eventpostupdated'] = 'Post updated'; $string['eventreadtrackingdisabled'] = 'Read tracking disabled'; $string['eventreadtrackingenabled'] = 'Read tracking enabled'; $string['eventsubscribersviewed'] = 'Subscribers viewed'; $string['eventsubscriptioncreated'] = 'Subscription created'; $string['eventsubscriptiondeleted'] = 'Subscription deleted'; $string['emaildigestcompleteshort'] = 'Complete posts'; $string['emaildigestdefault'] = 'Default ({$a})'; $string['emaildigestoffshort'] = 'No digest'; $string['emaildigestsubjectsshort'] = 'Subjects only'; $string['emaildigesttype'] = 'Email digest options'; $string['emaildigesttype_help'] = 'The type of notification that you will receive for each forum. * Default - follow the digest setting found in your user profile. If you update your profile, then that change will be reflected here too; * No digest - you will receive one e-mail per forum post; * Digest - complete posts - you will receive one digest e-mail per day containing the complete contents of each forum post; * Digest - subjects only - you will receive one digest e-mail per day containing just the subject of each forum post. '; $string['emptymessage'] = 'Something was wrong with your post. Perhaps you left it blank, or the attachment was too big. Your changes have NOT been saved.'; $string['errorcannotlock'] = 'You do not have the permission to lock discussions.'; $string['errordiscussionnotfound'] = 'The discussion was not found'; $string['erroremptymessage'] = 'Post message cannot be empty'; $string['erroremptysubject'] = 'Post subject cannot be empty.'; $string['errorenrolmentrequired'] = 'You must be enrolled in this course to access this content'; $string['errorforumnotfound'] = 'The forum that this discussion belongs to was not found'; $string['errorwhiledelete'] = 'An error occurred while deleting record.'; $string['eventassessableuploaded'] = 'Some content has been posted.'; $string['everyonecanchoose'] = 'Everyone can choose to be subscribed'; $string['everyonecannowchoose'] = 'Everyone can now choose to be subscribed'; $string['everyoneisnowsubscribed'] = 'Everyone is now subscribed to this forum'; $string['everyoneissubscribed'] = 'Everyone is subscribed to this forum'; $string['existingsubscribers'] = 'Existing subscribers'; $string['export'] = 'Export'; $string['exportattachmentname'] = 'Export attachment {$a} to portfolio'; $string['exportdiscussion'] = 'Export whole discussion to portfolio'; $string['exportstriphtml'] = 'Remove HTML'; $string['exportstriphtml_help'] = 'Whether HTML tags such as p and br should be removed from the forum post message.'; $string['exportoptions'] = 'Export options'; $string['exporthumandates'] = 'Human-readable dates'; $string['exporthumandates_help'] = 'Whether dates should be exported in a human-readable format or as a timestamp (sequence of numbers).'; $string['firstpost'] = 'First post'; $string['favourites'] = 'Starred'; $string['favouriteupdated'] = 'Your star option has been updated.'; $string['forcedreadtracking'] = 'Allow forced read tracking'; $string['forcedreadtracking_desc'] = 'Allows forums to be set to forced read tracking. Will result in decreased performance for some users, particularly on courses with many forums and posts. When off, any forums previously set to Forced are treated as optional.'; $string['forcesubscribed_help'] = 'This forum has been configured so that you cannot unsubscribe from discussions.'; $string['forcesubscribed'] = 'This forum forces everyone to be subscribed.'; $string['forum'] = 'Forum'; $string['forum:addinstance'] = 'Add a new forum'; $string['forum:addnews'] = 'Add announcements'; $string['forum:addquestion'] = 'Add question'; $string['forum:allowforcesubscribe'] = 'Allow force subscribe'; $string['forum:canmailnow'] = 'Can post to Q and A forum without editing-time delay'; $string['forum:canoverridecutoff'] = 'Post to forums after their cut-off date'; $string['forum:canoverridediscussionlock'] = 'Reply to locked discussions'; $string['forum:cantogglefavourite'] = 'Star discussions'; $string['forum:grade'] = 'Grade forum'; $string['forumauthorhidden'] = 'Author (hidden)'; $string['forumblockingalmosttoomanyposts'] = 'You are approaching the posting threshold. You have posted {$a->numposts} times in the last {$a->blockperiod} and the limit is {$a->blockafter} posts.'; $string['forumbodyhidden'] = 'This post is currently hidden, most likely because you need to post in the discussion first and forum post notifications must be sent. Another possibility is that the current time is not within the display period set for the post.'; $string['forum:canposttomygroups'] = 'Post to all groups you have access to'; $string['forum:createattachment'] = 'Create attachments'; $string['forum:deleteanypost'] = 'Delete any posts (anytime)'; $string['forum:deleteownpost'] = 'Delete own posts (within deadline)'; $string['forum:editanypost'] = 'Edit any post'; $string['forum:exportdiscussion'] = 'Export whole discussion'; $string['forum:exportforum'] = 'Export forum'; $string['forum:exportownpost'] = 'Export own post'; $string['forum:exportpost'] = 'Export post'; $string['forumgradingnavigation'] = 'Forum grading navigation'; $string['forumgradingpanel'] = 'Forum grading panel'; $string['forumintro'] = 'Description'; $string['forum:managesubscriptions'] = 'Manage subscribers'; $string['forum:movediscussions'] = 'Move discussions'; $string['forum:pindiscussions'] = 'Pin discussions'; $string['forum:postwithoutthrottling'] = 'Exempt from post threshold'; $string['forumname'] = 'Forum name'; $string['forumposts'] = 'Forum posts'; $string['forum:rate'] = 'Rate posts'; $string['forum:replynews'] = 'Reply to announcements'; $string['forum:replypost'] = 'Reply to posts'; $string['replypostdiscussion'] = 'Reply to discussion'; $string['forum:postprivatereply'] = 'Reply privately to posts'; $string['forum:readprivatereplies'] = 'View private replies'; $string['forums'] = 'Forums'; $string['forum:splitdiscussions'] = 'Split discussions'; $string['forum:startdiscussion'] = 'Start new discussions'; $string['forumsubjecthidden'] = 'Subject (hidden)'; $string['forumtracked'] = 'Unread posts are being tracked'; $string['forumtrackednot'] = 'Unread posts are not being tracked'; $string['forumtype'] = 'Forum type'; $string['forumtype_help'] = 'There are 5 forum types: * A single simple discussion - A single discussion topic which everyone can reply to (cannot be used with separate groups) * Each person posts one discussion - Each student can post exactly one new discussion topic, which everyone can then reply to * Q and A forum - A student must first post their reply to a question posted by the teacher, before viewing other students\' posts * Standard forum displayed in a blog-like format - An open forum where anyone can start a new discussion at any time, and in which discussion topics are displayed on one page with "Discuss this topic" links * Standard forum for general use - An open forum where anyone can start a new discussion at any time'; $string['forum:viewallratings'] = 'View all raw ratings given by individuals'; $string['forum:viewanyrating'] = 'View total ratings that anyone received'; $string['forum:viewdiscussion'] = 'View discussions'; $string['forum:viewhiddentimedposts'] = 'View hidden timed posts'; $string['forum:viewqandawithoutposting'] = 'Always see Q and A posts'; $string['forum:viewrating'] = 'View the total rating you received'; $string['forum:viewsubscribers'] = 'View subscribers'; $string['generalforum'] = 'Standard forum for general use'; $string['generalforums'] = 'General forums'; $string['gradeitem:forum'] = 'Forum'; $string['hiddenforumpost'] = 'Hidden forum post'; $string['hidegraderpanel'] = 'Hide grader panel'; $string['hidepreviousrepliescount'] = 'Hide previous replies ({$a})'; $string['hideusersearch'] = 'Hide user search'; $string['indicator:cognitivedepth'] = 'Forum cognitive'; $string['indicator:cognitivedepth_help'] = 'This indicator is based on the cognitive depth reached by the student in a Forum activity.'; $string['indicator:cognitivedepthdef'] = 'Forum cognitive'; $string['indicator:cognitivedepthdef_help'] = 'The participant has reached this percentage of the cognitive engagement offered by the Forum activities during this analysis interval (Levels = No view, View, Submit, View feedback, Comment on feedback, Resubmit after viewing feedback)'; $string['indicator:cognitivedepthdef_link'] = 'Learning_analytics_indicators#Cognitive_depth'; $string['indicator:socialbreadth'] = 'Forum social'; $string['indicator:socialbreadth_help'] = 'This indicator is based on the social breadth reached by the student in a Forum activity.'; $string['indicator:socialbreadthdef'] = 'Forum social'; $string['indicator:socialbreadthdef_help'] = 'The participant has reached this percentage of the social engagement offered by the Forum activities during this analysis interval (Levels = No participation, Participant alone, Participant with others)'; $string['indicator:socialbreadthdef_link'] = 'Learning_analytics_indicators#Social_breadth'; $string['starredonly'] = 'Search starred discussions only'; $string['indexoutoftotal'] = '{$a->index} out of {$a->total}'; $string['inforum'] = 'in {$a}'; $string['inreplyto'] = 'In reply to {$a}'; $string['introblog'] = 'The posts in this forum were copied here automatically from blogs of users in this course because those blog entries are no longer available'; $string['intronews'] = 'General news and announcements'; $string['introsocial'] = 'An open forum for chatting about anything you want to'; $string['introteacher'] = 'A forum for teacher-only notes and discussion'; $string['invalidaccess'] = 'This page was not accessed correctly'; $string['invaliddiscussionid'] = 'Discussion ID was incorrect or no longer exists'; $string['invaliddigestsetting'] = 'An invalid mail digest setting was provided'; $string['invalidforcesubscribe'] = 'Invalid force subscription mode'; $string['invalidforumid'] = 'Forum ID was incorrect'; $string['invalidparentpostid'] = 'Parent post ID was incorrect'; $string['invalidpostid'] = 'Invalid post ID - {$a}'; $string['lastpost'] = 'Last post'; $string['learningforums'] = 'Learning forums'; $string['lockdiscussionafter'] = 'Lock discussions after period of inactivity'; $string['lockdiscussionafter_help'] = 'Discussions may be automatically locked after a specified time has elapsed since the last reply. Users with the capability to reply to locked discussions can unlock a discussion by replying to it.'; $string['longpost'] = 'Long post'; $string['locked'] = 'Locked'; $string['lockdiscussion'] = 'Lock this discussion'; $string['lockupdated'] = 'The lock option has been updated.'; $string['mailnow'] = 'Send forum post notifications with no editing-time delay'; $string['manydiscussions'] = 'Discussions per page'; $string['managesubscriptionsoff'] = 'Finish managing subscriptions'; $string['managesubscriptionson'] = 'Manage subscribers'; $string['markasread'] = 'Mark as read'; $string['markalldread'] = 'Mark all posts in this discussion read.'; $string['markallread'] = 'Mark all posts in this forum read.'; $string['markasreadonnotification'] = 'When sending forum post notifications'; $string['markasreadonnotificationno'] = 'Do not mark the post as read'; $string['markasreadonnotificationyes'] = 'Mark the post as read'; $string['markasreadonnotification_help'] = 'When you are notified of a forum post, you can choose whether this should mark the post as read for the purpose of forum tracking.'; $string['markread'] = 'Mark read'; $string['markreadbutton'] = 'Mark
read'; $string['markunread'] = 'Mark unread'; $string['markunreadbutton'] = 'Mark
unread'; $string['maxattachments'] = 'Maximum number of attachments'; $string['maxattachments_help'] = 'This setting specifies the maximum number of files that can be attached to a forum post.'; $string['maxattachmentsize'] = 'Maximum attachment size'; $string['maxattachmentsize_help'] = 'This setting specifies the largest size of file that can be attached to a forum post.'; $string['maxtimehaspassed'] = 'Sorry, but the maximum time for editing this post ({$a}) has passed!'; $string['message'] = 'Message'; $string['messageinboundattachmentdisallowed'] = 'Unable to post your reply, since it includes an attachment and the forum doesn\'t allow attachments.'; $string['messageinboundfilecountexceeded'] = 'Unable to post your reply, since it includes more than the maximum number of attachments allowed for the forum ({$a->forum->maxattachments}).'; $string['messageinboundfilesizeexceeded'] = 'Unable to post your reply, since the total attachment size ({$a->filesize}) is greater than the maximum size allowed for the forum ({$a->maxbytes}).'; $string['messageinboundforumhidden'] = 'Unable to post your reply, since the forum is currently unavailable.'; $string['messageinboundnopostforum'] = 'Unable to post your reply, since you do not have permission to post in the {$a->forum->name} forum.'; $string['messageinboundthresholdhit'] = 'Unable to post your reply. You have exceeded the posting threshold set for this forum.'; $string['messageprovider:digests'] = 'Subscribed forum digests'; $string['messageprovider:posts'] = 'Subscribed forum posts'; $string['missingsearchterms'] = 'The following search terms occur only in the HTML markup of this message:'; $string['modeflatnewestfirst'] = 'Display replies flat, with newest first'; $string['modeflatoldestfirst'] = 'Display replies flat, with oldest first'; $string['modenested'] = 'Display replies in nested form'; $string['modenestedv2'] = 'Display replies in experimental nested form'; $string['modethreaded'] = 'Display replies in threaded form'; $string['modulename'] = 'Forum'; $string['modulename_help'] = 'The forum activity module enables participants to have asynchronous discussions i.e. discussions that take place over an extended period of time. There are several forum types to choose from, such as a standard forum where anyone can start a new discussion at any time; a forum where each student can post exactly one discussion; or a question and answer forum where students must first post before being able to view other students\' posts. A teacher can allow files to be attached to forum posts. Attached images are displayed in the forum post. Participants can subscribe to a forum to receive notifications of new forum posts. A teacher can set the subscription mode to optional, forced or auto, or prevent subscription completely. If required, students can be blocked from posting more than a given number of posts in a given time period; this can prevent individuals from dominating discussions. Forum posts can be rated by teachers or students (peer evaluation). Ratings can be aggregated to form a final grade which is recorded in the gradebook. Forums have many uses, such as * A social space for students to get to know each other * For course announcements (using a news forum with forced subscription) * For discussing course content or reading materials * For continuing online an issue raised previously in a face-to-face session * For teacher-only discussions (using a hidden forum) * A help centre where tutors and students can give advice * A one-on-one support area for private student-teacher communications (using a forum with separate groups and with one student per group) * For extension activities, for example ‘brain teasers’ for students to ponder and suggest solutions to'; $string['modulename_link'] = 'mod/forum/view'; $string['modulenameplural'] = 'Forums'; $string['more'] = 'more'; $string['movedmarker'] = '(Moved)'; $string['movethisdiscussionlabel'] = 'Move the current discussion to the specified forum'; $string['movethisdiscussionto'] = 'Move this discussion to ...'; $string['mustprovidediscussionorpost'] = 'You must provide either a discussion ID or post ID to export.'; $string['myprofileownpost'] = 'My forum posts'; $string['myprofileowndis'] = 'My forum discussions'; $string['myprofileotherdis'] = 'Forum discussions'; $string['namenews'] = 'Announcements'; $string['namenews_help'] = 'The course announcements forum is a special forum for announcements and is automatically created when a course is created. A course can have only one announcements forum. Only teachers and administrators can post announcements. The "Latest announcements" block will display recent announcements.'; $string['namesocial'] = 'Social forum'; $string['nameteacher'] = 'Teacher forum'; $string['nextdiscussiona'] = 'Next discussion: {$a}'; $string['nextuser'] = 'Save changes and proceed to the next user'; $string['newforumposts'] = 'New forum posts'; $string['noattachments'] = 'There are no attachments to this post'; $string['nodiscussions'] = 'There are no discussion topics yet in this forum.'; $string['nodiscussionsstartedby'] = '{$a} has not started any discussions'; $string['nodiscussionsstartedbyyou'] = 'You haven\'t started any discussions yet'; $string['noguestpost'] = 'Only logged in users can post to this forum.'; $string['noguestpost:title'] = 'Log in to continue'; $string['noguestsubscribe'] = 'Sorry, guests are not allowed to subscribe.'; $string['noguesttracking'] = 'Sorry, guests are not allowed to set tracking options.'; $string['nomorepostscontaining'] = 'No more posts containing \'{$a}\' were found'; $string['nonews'] = 'No announcements have been posted yet.'; $string['noonecansubscribenow'] = 'Subscriptions are now disallowed'; $string['nopermissiontosubscribe'] = 'You do not have the permission to view forum subscribers'; $string['nopermissiontoview'] = 'You do not have permissions to view this post'; $string['nopostforum'] = 'You are not allowed to post to this forum.'; $string['noposts'] = 'No posts'; $string['nopostsmadebyuser'] = '{$a} has made no posts'; $string['nopostsmadebyyou'] = 'You haven\'t made any posts'; $string['noquestions'] = 'There are no questions yet in this forum.'; $string['nosubscribers'] = 'There are no subscribers yet for this forum.'; $string['notsubscribed'] = 'Subscribe'; $string['notexists'] = 'Discussion no longer exists'; $string['nothingnew'] = 'Nothing new for {$a}'; $string['notingroup'] = 'You need to be part of a group to see this forum.'; $string['notinstalled'] = 'The forum module is not installed'; $string['notlocked'] = 'Lock'; $string['notpartofdiscussion'] = 'This post is not part of a discussion!'; $string['notrackforum'] = 'Don\'t track unread posts'; $string['noviewdiscussionspermission'] = 'You do not have permission to view discussions in this forum.'; $string['nowallsubscribed'] = 'You are now subscribed to all forums in {$a}.'; $string['nowallunsubscribed'] = 'You are now unsubscribed from all forums in {$a}.'; $string['nownotsubscribed'] = '{$a->name} will NOT be notified of new posts in \'{$a->forum}\''; $string['nownottracking'] = '{$a->name} is no longer tracking \'{$a->forum}\'.'; $string['nowsubscribed'] = 'You will be notified of new posts in the forum \'{$a->forum}\'.'; $string['nowtracking'] = '{$a->name} is now tracking \'{$a->forum}\'.'; $string['numposts'] = '{$a} posts'; $string['numberofreplies'] = 'Number of replies: {$a}'; $string['olderdiscussions'] = 'Older discussions'; $string['oldertopics'] = 'Older topics'; $string['oldpostdays'] = 'Read after days'; $string['page-mod-forum-x'] = 'Any forum module page'; $string['page-mod-forum-view'] = 'Forum module main page'; $string['page-mod-forum-discuss'] = 'Forum module discussion thread page'; $string['parent'] = 'Show parent'; $string['parentofthispost'] = 'Parent of this post'; $string['permalink'] = 'Permalink'; $string['permanentlinktopost'] = 'Permanent link to this post'; $string['permanentlinktoparentpost'] = 'Permanent link to the parent of this post'; $string['postisprivatereply'] = 'This is a private reply. (Teachers and other users with the capability to view private replies can also see it.)'; $string['pindiscussion'] = 'Pin this discussion'; $string['pinupdated'] = 'The pin option has been updated.'; $string['posttomygroups'] = 'Post a copy to all groups'; $string['posttomygroups_help'] = 'Posts a copy of this message to all groups you have access to. Participants in groups you do not have access to will not see this post'; $string['prevdiscussiona'] = 'Previous discussion: {$a}'; $string['pluginadministration'] = 'Forum administration'; $string['pluginname'] = 'Forum'; $string['postadded'] = '

Your post was successfully added.

You have {$a} to edit it if you want to make any changes.

'; $string['postaddedsuccess'] = 'Your post was successfully added.'; $string['postaddedtimeleft'] = 'You have {$a} to edit it if you want to make any changes.'; $string['postbymailsuccess'] = 'Your reply "{$a->subject}" was successfully posted: {$a->discussionurl}'; $string['postbymailsuccess_html'] = 'Your reply {$a->subject} was successfully posted.'; $string['postbyuser'] = '{$a->post} by {$a->user}'; $string['postincontext'] = 'See this post in context'; $string['postmailinfo'] = 'This is a copy of a message posted in {$a}.'; $string['postmailnow'] = '

This post will be mailed out immediately to all forum subscribers.

'; $string['postmailreply'] = 'To reply click on this link: {$a}'; $string['postmailsubject'] = '{$a->courseshortname}: {$a->subject}'; $string['posts'] = 'Posts'; $string['postsfrom'] = 'Posts from'; $string['postsmadebyuser'] = 'Posts made by {$a}'; $string['postsmadebyuserincourse'] = 'Posts made by {$a->fullname} in {$a->coursename}'; $string['postsmadeincourse'] = 'Posts made in {$a}'; $string['poststo'] = 'Posts to'; $string['posttoforum'] = 'Post to forum'; $string['postupdated'] = 'Your post was updated'; $string['potentialsubscribers'] = 'Potential subscribers'; $string['previoususer'] = 'Save changes and proceed to the previous user'; $string['privacy:digesttypenone'] = 'We do not hold any data relating to a preferred forum digest type for this forum.'; $string['privacy:digesttypepreference'] = 'You have chosen to receive the following forum digest type: "{$a->type}".'; $string['privacy:discussionsubscriptionpreference'] = 'You have chosen the following discussion subscription preference for this forum: "{$a->preference}"'; $string['privacy:metadata:core_tag'] = 'The forum makes use of the tag subsystem to support tagging of posts.'; $string['privacy:metadata:core_rating'] = 'The forum makes use of the rating subsystem to support the rating of posts.'; $string['privacy:metadata:forum_digests'] = 'Information about the digest preferences for each forum.'; $string['privacy:metadata:forum_digests:forum'] = 'The forum subscribed to.'; $string['privacy:metadata:forum_digests:maildigest'] = 'The digest preference.'; $string['privacy:metadata:forum_digests:userid'] = 'The ID of the user with the digest preference.'; $string['privacy:metadata:forum_discussion_subs'] = 'Information about the subscriptions to individual forum discussions'; $string['privacy:metadata:forum_discussion_subs:discussionid'] = 'The ID of the discussion that was subscribed to.'; $string['privacy:metadata:forum_discussion_subs:preference'] = 'The start time of the subscription.'; $string['privacy:metadata:forum_discussion_subs:userid'] = 'The ID of the user with the discussion subscription.'; $string['privacy:metadata:forum_discussions'] = 'Information about the individual forum discussions that a user has created'; $string['privacy:metadata:forum_discussions:assessed'] = 'TODOD - what does this field store'; $string['privacy:metadata:forum_discussions:name'] = 'The name of the discussion, as chosen by the author.'; $string['privacy:metadata:forum_discussions:timemodified'] = 'The time that the discussion was last modified.'; $string['privacy:metadata:forum_discussions:userid'] = 'The ID of the user who created the discussion'; $string['privacy:metadata:forum_discussions:usermodified'] = 'The ID of the user who last modified the discussion in some way.'; $string['privacy:metadata:forum_grades'] = 'Grade data for the forum'; $string['privacy:metadata:forum_grades:forum'] = 'The forum that was graded'; $string['privacy:metadata:forum_grades:grade'] = 'The grade awarded'; $string['privacy:metadata:forum_grades:userid'] = 'The user who was graded'; $string['privacy:metadata:forum_posts'] = 'Information about the digest preferences for each forum.'; $string['privacy:metadata:forum_posts:created'] = 'The time that the post was created.'; $string['privacy:metadata:forum_posts:discussion'] = 'The discussion that the post is in.'; $string['privacy:metadata:forum_posts:message'] = 'The message of the forum post.'; $string['privacy:metadata:forum_posts:modified'] = 'The time that the post was last modified.'; $string['privacy:metadata:forum_posts:parent'] = 'The parent post that was replied to.'; $string['privacy:metadata:forum_posts:subject'] = 'The subject of the forum post.'; $string['privacy:metadata:forum_posts:totalscore'] = 'The message of the forum post.'; $string['privacy:metadata:forum_posts:userid'] = 'The ID of the user who authored the forum post.'; $string['privacy:metadata:forum_posts:privatereplyto'] = 'The ID of the user this reply was sent to.'; $string['privacy:metadata:forum_queue'] = 'Temporary log of posts that will be mailed in digest form'; $string['privacy:metadata:forum_queue:discussionid'] = 'Forum discussion ID'; $string['privacy:metadata:forum_queue:postid'] = 'Forum post ID'; $string['privacy:metadata:forum_queue:timemodified'] = 'The modified time of the original post'; $string['privacy:metadata:forum_queue:userid'] = 'User who needs to be notified of the post'; $string['privacy:metadata:forum_read'] = 'Information about which posts have been read by the user.'; $string['privacy:metadata:forum_read:discussionid'] = 'The discussion that the post is in.'; $string['privacy:metadata:forum_read:firstread'] = 'The first time that the post was read.'; $string['privacy:metadata:forum_read:lastread'] = 'The most recent time that the post was read.'; $string['privacy:metadata:forum_read:postid'] = 'The post that was read.'; $string['privacy:metadata:forum_read:userid'] = 'The ID of the user that this record relates to.'; $string['privacy:metadata:forum_subscriptions'] = 'Information about which forums the user has subscribed to.'; $string['privacy:metadata:forum_subscriptions:forum'] = 'The forum that was subscribed to.'; $string['privacy:metadata:forum_subscriptions:userid'] = 'The ID of the user that this forum subscription relates to.'; $string['privacy:metadata:forum_track_prefs'] = 'Information about which forums the user has chosen to track post reads for.'; $string['privacy:metadata:forum_track_prefs:forumid'] = 'The forum that has read tracking enabled.'; $string['privacy:metadata:forum_track_prefs:userid'] = 'The ID of the user that this forum tracking preference relates to.'; $string['privacy:metadata:preference:autosubscribe'] = 'Whether to subscribe to discussions when replying to posts within them.'; $string['privacy:metadata:preference:forum_discussionlistsortorder'] = 'The preferred sorting order of the discussions list'; $string['privacy:metadata:preference:maildigest'] = 'The site-wide mail digest preference'; $string['privacy:metadata:preference:markasreadonnotification'] = 'Whether to mark forum posts as read when receiving them as messages.'; $string['privacy:metadata:preference:trackforums'] = 'Whether to enable read tracking.'; $string['privacy:postwasread'] = 'This post was first read on {$a->firstread} and most recently read on {$a->lastread}'; $string['privacy:readtrackingdisabled'] = 'You have chosen to not track posts you have read within this forum.'; $string['privacy:subscribedtoforum'] = 'You are subscribed to this forum.'; $string['privatereply'] = 'Reply privately'; $string['privatereply_help'] = 'When you reply privately, your post is also visible to users with the capability to view private replies, such as teachers.'; $string['processingdigest'] = 'Processing email digest for user {$a}'; $string['processingpost'] = 'Processing post {$a}'; $string['prune'] = 'Split'; $string['prunediscussion'] = 'Split discussion'; $string['prunedpost'] = 'A new discussion has been created from that post'; $string['pruneheading'] = 'Split the discussion and move this post to a new discussion'; $string['qandaforum'] = 'Q and A forum'; $string['qandanotify'] = 'This is a question and answer forum. To see other replies, you must first post your reply.'; $string['readtherest'] = 'Read the rest of this topic'; $string['removeallforumtags'] = 'All forum tags'; $string['removefromfavourites'] = 'Unstar this discussion'; $string['replies'] = 'Replies'; $string['repliesmany'] = '{$a} replies so far'; $string['repliesone'] = '{$a} reply so far'; $string['reply'] = 'Reply'; $string['replyauthorself'] = '{$a} (you)'; $string['replyingtoauthor'] = 'Replying to {$a}...'; $string['replyplaceholder'] = 'Write your reply...'; $string['replyforum'] = 'Reply to forum'; $string['replytopostbyemail'] = 'You can reply to this via email.'; $string['replytouser'] = 'Use email address in reply'; $string['reply_handler'] = 'Reply to forum posts via email'; $string['reply_handler_name'] = 'Reply to forum posts'; $string['resetforums'] = 'Delete posts from'; $string['resetforumsall'] = 'All posts'; $string['resetdigests'] = 'All per-user forum digest preferences'; $string['resetsubscriptions'] = 'All forum subscriptions'; $string['resettrackprefs'] = 'All forum tracking preferences'; $string['rsssubscriberssdiscussions'] = 'RSS feed of discussions'; $string['rsssubscriberssposts'] = 'RSS feed of posts'; $string['rssarticles'] = 'Number of RSS recent articles'; $string['rssarticles_help'] = 'This setting specifies the number of articles (either discussions or posts) to include in the RSS feed. Between 5 and 20 generally acceptable.'; $string['rsstype'] = 'RSS feed for this activity'; $string['rsstype_help'] = 'To enable the RSS feed for this activity, select either discussions or posts to be included in the feed.'; $string['rsstypedefault'] = 'RSS feed type'; $string['search'] = 'Search'; $string['search:post'] = 'Forum - posts'; $string['search:activity'] = 'Forum - activity information'; $string['searchdatefrom'] = 'Posts must be newer than this'; $string['searchdateto'] = 'Posts must be older than this'; $string['searchforumintro'] = 'Please enter search terms into one or more of the following fields:'; $string['searchforums'] = 'Search forums'; $string['searchfullwords'] = 'These words should appear as whole words'; $string['searchnotwords'] = 'These words should NOT be included'; $string['searcholderposts'] = 'Search older posts...'; $string['searchphrase'] = 'This exact phrase must appear in the post'; $string['searchresults'] = 'Search results'; $string['searchsubject'] = 'These words should be in the subject'; $string['searchtags'] = 'Is tagged with'; $string['searchuser'] = 'This name should match the author'; $string['searchusers'] = 'Search users'; $string['searchuserid'] = 'The Moodle ID of the author'; $string['searchwhichforums'] = 'Choose which forums to search'; $string['searchwords'] = 'These words can appear anywhere in the post'; $string['seeallposts'] = 'See all posts made by this user'; $string['sendstudentnotificationsno'] = 'No'; $string['sendstudentnotificationsyes'] = 'Yes, send notification to student'; $string['sendstudentnotificationsdefault'] = 'Default setting for "Notify students"'; $string['sendstudentnotificationsdefault_help'] = 'Set the default value for the "Notify students" checkbox on the grading form.'; $string['settings'] = 'Settings'; $string['shortpost'] = 'Short post'; $string['showingcountoftotaldiscussions'] = 'List of discussions. Showing {$a->count} of {$a->total} discussions'; $string['showgraderpanel'] = 'Show grader panel'; $string['showpreviousrepliescount'] = 'Show previous replies ({$a})'; $string['showsubscribers'] = 'Show/edit current subscribers'; $string['showusersearch'] = 'Show user search'; $string['singleforum'] = 'A single simple discussion'; $string['smallmessage'] = '{$a->user} posted in {$a->forumname}'; $string['smallmessagedigest'] = 'Forum digest containing {$a} messages'; $string['startedby'] = 'Started by'; $string['subject'] = 'Subject'; $string['subplugintype_forumreport'] = 'Forum report'; $string['subplugintype_forumreport_plural'] = 'Forum reports'; $string['subscribe'] = 'Subscribe to forum'; $string['subscribediscussion'] = 'Subscribe to this discussion'; $string['subscribeall'] = 'Subscribe everyone to this forum'; $string['subscribeenrolledonly'] = 'Sorry, only enrolled users are allowed to subscribe to forum post notifications.'; $string['subscribed'] = 'Subscribed'; $string['subscribenone'] = 'Unsubscribe everyone from this forum'; $string['subscribers'] = 'Subscribers'; $string['subscriberstowithcount'] = 'Subscribers to "{$a->name}" ({$a->count})'; $string['subscribestart'] = 'Send me notifications of new posts in this forum'; $string['subscribestop'] = 'I don\'t want to be notified of new posts in this forum'; $string['subscription'] = 'Subscription'; $string['subscription_help'] = 'If you are subscribed to a forum it means you will receive notification of new forum posts. Usually you can choose whether you wish to be subscribed, though sometimes subscription is forced so that everyone receives notifications.'; $string['subscriptionandtracking'] = 'Subscription and tracking'; $string['subscriptionmode'] = 'Subscription mode'; $string['subscriptionmodeconfirm'] = 'Are you sure you want to change the subscription mode to {$a}?'; $string['subscriptionmode_help'] = 'When a participant is subscribed to a forum it means they will receive forum post notifications. There are 4 subscription mode options: * Optional subscription - Participants can choose whether to be subscribed * Forced subscription - Everyone is subscribed and cannot unsubscribe * Auto subscription - Everyone is subscribed initially but can choose to unsubscribe at any time * Subscription disabled - Subscriptions are not allowed'; $string['subscriptionoptional'] = 'Optional subscription'; $string['subscriptionforced'] = 'Forced subscription'; $string['subscriptionauto'] = 'Auto subscription'; $string['subscriptiondisabled'] = 'Subscription disabled'; $string['subscriptions'] = 'Subscriptions'; $string['tagarea_forum_posts'] = 'Forum posts'; $string['thisforumisthrottled'] = 'This forum has a limit to the number of forum postings you can make in a given time period - this is currently set at {$a->blockafter} posting(s) in {$a->blockperiod}'; $string['thisforumisdue'] = 'The due date for posting to this forum was {$a}.'; $string['thisforumhasduedate'] = 'The due date for posting to this forum is {$a}.'; $string['timed'] = 'Timed'; $string['timeddiscussion'] = 'Timed discussion'; $string['timedhidden'] = 'Timed status: Hidden from students'; $string['timedposts'] = 'Timed posts'; $string['timedvisible'] = 'Timed status: Visible to all users'; $string['timestartenderror'] = 'Display end date cannot be earlier than the start date'; $string['togglediscussionmenu'] = 'Toggle the discussion menu'; $string['togglefullscreen'] = 'Toggle full screen'; $string['togglesettingsdrawer'] = 'Toggle settings drawer'; $string['trackforum'] = 'Track unread posts'; $string['trackreadposts_header'] = 'Forum tracking'; $string['tracking'] = 'Track'; $string['trackingoff'] = 'Off'; $string['trackingon'] = 'Forced'; $string['trackingoptional'] = 'Optional'; $string['trackingtype'] = 'Read tracking'; $string['trackingtype_help'] = 'Read tracking enables participants to check which posts they have not yet seen by highlighting any new posts. If set to optional, participants can choose whether to turn tracking on or off for the forum. (Users must also enable forum tracking in their forum preferences.) If \'Allow forced read tracking\' is enabled by a site administrator, then a further option is available - forced. This means that tracking is always on, regardless of users\' forum preferences.'; $string['unlockdiscussion'] = 'Unlock this discussion'; $string['unread'] = 'Unread'; $string['unreadpost'] = 'Unread post'; $string['unreadposts'] = 'Unread posts'; $string['unreadpostsnumber'] = '{$a} unread posts'; $string['unreadpostsone'] = '1 unread post'; $string['unsubscribe'] = 'Unsubscribe from forum'; $string['unsubscribelink'] = 'Unsubscribe from this forum: {$a}'; $string['unsubscribediscussion'] = 'Unsubscribe from this discussion'; $string['unsubscribediscussionlink'] = 'Unsubscribe from this discussion: {$a}'; $string['unsubscribeall'] = 'Unsubscribe from all forums'; $string['unsubscribeallconfirm'] = 'You are currently subscribed to {$a->forums} forums, and {$a->discussions} discussions. Do you really want to unsubscribe from all forums and discussions, and disable discussion auto-subscription?'; $string['unsubscribeallconfirmforums'] = 'You are currently subscribed to {$a->forums} forums. Do you really want to unsubscribe from all forums and disable discussion auto-subscription?'; $string['unsubscribeallconfirmdiscussions'] = 'You are currently subscribed to {$a->discussions} discussions. Do you really want to unsubscribe from all discussions and disable discussion auto-subscription?'; $string['unsubscribealldone'] = 'All optional forum subscriptions were removed. You will still receive notifications from forums with forced subscription. To manage forum notifications go to Messaging in My Profile Settings.'; $string['unsubscribeallempty'] = 'You are not subscribed to any forums. To disable all notifications from this server go to Messaging in My Profile Settings.'; $string['unsubscribed'] = 'Unsubscribed'; $string['unsubscribeshort'] = 'Unsubscribe'; $string['useexperimentalui'] = 'Use experimental nested discussion view'; $string['usermarksread'] = 'Manual message read marking'; $string['usernavigation'] = 'User navigation'; $string['usersforumposts'] = 'User\'s forum posts'; $string['unpindiscussion'] = 'Unpin this discussion'; $string['viewalldiscussions'] = 'View all discussions'; $string['viewparentpost'] = 'View parent post'; $string['viewthediscussion'] = 'View the discussion'; $string['warnafter'] = 'Post threshold for warning'; $string['warnafter_help'] = 'Students can be warned as they approach the maximum number of posts allowed in a given period. This setting specifies after how many posts they are warned. Users with the capability mod/forum:postwithoutthrottling are exempt from post limits.'; $string['warnformorepost'] = 'Warning! There is more than one discussion in this forum - using the most recent'; $string['yournewquestion'] = 'Your new question'; $string['yournewtopic'] = 'Your new discussion topic'; $string['yourreply'] = 'Your reply'; $string['forumsubjectdeleted'] = 'This forum post has been removed'; $string['forumbodydeleted'] = 'The content of this forum post has been removed and can no longer be accessed.'; $string['forumgrader'] = 'Forum grader'; $string['grade_forum_header'] = 'Whole forum grading'; $string['grade_forum_name'] = 'Whole forum'; $string['grade_forum_title'] = 'Grade'; $string['grade_rating_name'] = 'Rating'; $string['gradeusers'] = 'Grade users'; $string['graded'] = 'Graded'; $string['gradedby'] = 'Graded by'; $string['notgraded'] = 'Not graded'; $string['nowgradinguser'] = 'Now grading {$a}'; $string['gradeforrating'] = 'Grade for rating: {$a->str_long_grade}'; $string['gradeforratinghidden'] = 'Grade for rating hidden'; $string['gradeforwholeforum'] = 'Grade for forum: {$a->str_long_grade}'; $string['grading'] = 'Grading'; $string['gradingstatus'] = 'Grade status:'; $string['gradeforwholeforumhidden'] = 'Grade for forum hidden'; $string['gradeitemnameforwholeforum'] = '{$a->name} whole forum'; $string['gradeitemnameforrating'] = '{$a->name} rating'; $string['grades:gradesavedfor'] = 'Grade saved for {$a->fullname}'; $string['grades:gradesavefailed'] = 'Unable to save grade for {$a->fullname}: {$a->error}'; $string['nousersmatch'] = 'No user(s) found for given criteria'; $string['showmoreusers'] = 'Show more users'; $string['viewconversation'] = 'View discussion'; $string['viewgrades'] = 'View grades'; // Deprecated since Moodle 4.5. $string['privacy:request:delete:discussion:name'] = 'Delete at the request of the author'; $string['privacy:request:delete:post:message'] = 'The content of this post has been deleted at the request of its author.'; $string['privacy:request:delete:post:subject'] = 'Delete at the request of the author'; $string['tagsdeleted'] = 'Forum tags have been deleted'; // Deprecated since Moodle 5.0. $string['re'] = 'Re:'; PK-B\r;lang/en/deprecated.txtnu[privacy:request:delete:discussion:name,mod_forum privacy:request:delete:post:message,mod_forum privacy:request:delete:post:subject,mod_forum tagsdeleted,mod_forum re,mod_forum PK-B\#e11)backup/moodle2/restore_forum_stepslib.phpnu[. /** * @package mod_forum * @subpackage backup-moodle2 * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Define all the restore steps that will be used by the restore_forum_activity_task */ /** * Structure step to restore one forum activity */ class restore_forum_activity_structure_step extends restore_activity_structure_step { protected function define_structure() { $paths = array(); $userinfo = $this->get_setting_value('userinfo'); $paths[] = new restore_path_element('forum', '/activity/forum'); if ($userinfo) { $paths[] = new restore_path_element('forum_discussion', '/activity/forum/discussions/discussion'); $paths[] = new restore_path_element('forum_post', '/activity/forum/discussions/discussion/posts/post'); $paths[] = new restore_path_element('forum_tag', '/activity/forum/poststags/tag'); $paths[] = new restore_path_element('forum_discussion_sub', '/activity/forum/discussions/discussion/discussion_subs/discussion_sub'); $paths[] = new restore_path_element('forum_rating', '/activity/forum/discussions/discussion/posts/post/ratings/rating'); $paths[] = new restore_path_element('forum_subscription', '/activity/forum/subscriptions/subscription'); $paths[] = new restore_path_element('forum_digest', '/activity/forum/digests/digest'); $paths[] = new restore_path_element('forum_read', '/activity/forum/readposts/read'); $paths[] = new restore_path_element('forum_track', '/activity/forum/trackedprefs/track'); $paths[] = new restore_path_element('forum_grade', '/activity/forum/grades/grade'); } // Return the paths wrapped into standard activity structure return $this->prepare_activity_structure($paths); } protected function process_forum($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->course = $this->get_courseid(); // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. // See MDL-9367. if (!isset($data->duedate)) { $data->duedate = 0; } $data->duedate = $this->apply_date_offset($data->duedate); if (!isset($data->cutoffdate)) { $data->cutoffdate = 0; } $data->cutoffdate = $this->apply_date_offset($data->cutoffdate); $data->assesstimestart = $this->apply_date_offset($data->assesstimestart); $data->assesstimefinish = $this->apply_date_offset($data->assesstimefinish); if ($data->scale < 0) { // scale found, get mapping $data->scale = -($this->get_mappingid('scale', abs($data->scale))); } $newitemid = $DB->insert_record('forum', $data); $this->apply_activity_instance($newitemid); // Add current enrolled user subscriptions if necessary. $data->id = $newitemid; $ctx = context_module::instance($this->task->get_moduleid()); forum_instance_created($ctx, $data); } protected function process_forum_discussion($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->course = $this->get_courseid(); $data->forum = $this->get_new_parentid('forum'); $data->timestart = $this->apply_date_offset($data->timestart); $data->timeend = $this->apply_date_offset($data->timeend); $data->userid = $this->get_mappingid('user', $data->userid); $data->groupid = $this->get_mappingid('group', $data->groupid); $data->usermodified = $this->get_mappingid('user', $data->usermodified); $newitemid = $DB->insert_record('forum_discussions', $data); $this->set_mapping('forum_discussion', $oldid, $newitemid); } protected function process_forum_post($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->discussion = $this->get_new_parentid('forum_discussion'); $data->userid = $this->get_mappingid('user', $data->userid); // If post has parent, map it (it has been already restored) if (!empty($data->parent)) { $data->parent = $this->get_mappingid('forum_post', $data->parent); } \mod_forum\local\entities\post::add_message_counts($data); $newitemid = $DB->insert_record('forum_posts', $data); $this->set_mapping('forum_post', $oldid, $newitemid, true); // If !post->parent, it's the 1st post. Set it in discussion if (empty($data->parent)) { $DB->set_field('forum_discussions', 'firstpost', $newitemid, array('id' => $data->discussion)); } } protected function process_forum_tag($data) { $data = (object)$data; if (!core_tag_tag::is_enabled('mod_forum', 'forum_posts')) { // Tags disabled in server, nothing to process. return; } $tag = $data->rawname; if (!$itemid = $this->get_mappingid('forum_post', $data->itemid)) { // Some orphaned tag, we could not find the restored post for it - ignore. return; } $context = context_module::instance($this->task->get_moduleid()); core_tag_tag::add_item_tag('mod_forum', 'forum_posts', $itemid, $context, $tag); } protected function process_forum_rating($data) { global $DB; $data = (object)$data; // Cannot use ratings API, cause, it's missing the ability to specify times (modified/created) $data->contextid = $this->task->get_contextid(); $data->itemid = $this->get_new_parentid('forum_post'); if ($data->scaleid < 0) { // scale found, get mapping $data->scaleid = -($this->get_mappingid('scale', abs($data->scaleid))); } $data->rating = $data->value; $data->userid = $this->get_mappingid('user', $data->userid); // We need to check that component and ratingarea are both set here. if (empty($data->component)) { $data->component = 'mod_forum'; } if (empty($data->ratingarea)) { $data->ratingarea = 'post'; } $newitemid = $DB->insert_record('rating', $data); } protected function process_forum_subscription($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->forum = $this->get_new_parentid('forum'); $data->userid = $this->get_mappingid('user', $data->userid); // Create only a new subscription if it does not already exist (see MDL-59854). if ($subscription = $DB->get_record('forum_subscriptions', array('forum' => $data->forum, 'userid' => $data->userid))) { $this->set_mapping('forum_subscription', $oldid, $subscription->id, true); } else { $newitemid = $DB->insert_record('forum_subscriptions', $data); $this->set_mapping('forum_subscription', $oldid, $newitemid, true); } } protected function process_forum_discussion_sub($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->discussion = $this->get_new_parentid('forum_discussion'); $data->forum = $this->get_new_parentid('forum'); $data->userid = $this->get_mappingid('user', $data->userid); $newitemid = $DB->insert_record('forum_discussion_subs', $data); $this->set_mapping('forum_discussion_sub', $oldid, $newitemid, true); } protected function process_forum_digest($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->forum = $this->get_new_parentid('forum'); $data->userid = $this->get_mappingid('user', $data->userid); $newitemid = $DB->insert_record('forum_digests', $data); } protected function process_forum_grade($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->forum = $this->get_new_parentid('forum'); $data->userid = $this->get_mappingid('user', $data->userid); // We want to ensure the current user has an ID that we can associate to a grade. if ($data->userid != 0) { $newitemid = $DB->insert_record('forum_grades', $data); // Note - the old contextid is required in order to be able to restore files stored in // sub plugin file areas attached to the gradeid. $this->set_mapping('grade', $oldid, $newitemid, false, null, $this->task->get_old_contextid()); $this->set_mapping(restore_gradingform_plugin::itemid_mapping('forum'), $oldid, $newitemid); } } protected function process_forum_read($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->forumid = $this->get_new_parentid('forum'); $data->discussionid = $this->get_mappingid('forum_discussion', $data->discussionid); $data->postid = $this->get_mappingid('forum_post', $data->postid); $data->userid = $this->get_mappingid('user', $data->userid); $newitemid = $DB->insert_record('forum_read', $data); } protected function process_forum_track($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->forumid = $this->get_new_parentid('forum'); $data->userid = $this->get_mappingid('user', $data->userid); $newitemid = $DB->insert_record('forum_track_prefs', $data); } protected function after_execute() { // Add forum related files, no need to match by itemname (just internally handled context) $this->add_related_files('mod_forum', 'intro', null); // Add post related files, matching by itemname = 'forum_post' $this->add_related_files('mod_forum', 'post', 'forum_post'); $this->add_related_files('mod_forum', 'attachment', 'forum_post'); } protected function after_restore() { global $DB; // If the forum is of type 'single' and no discussion has been ignited // (non-userinfo backup/restore) create the discussion here, using forum // information as base for the initial post. $forumid = $this->task->get_activityid(); $forumrec = $DB->get_record('forum', array('id' => $forumid)); if ($forumrec->type == 'single' && !$DB->record_exists('forum_discussions', array('forum' => $forumid))) { // Create single discussion/lead post from forum data $sd = new stdClass(); $sd->course = $forumrec->course; $sd->forum = $forumrec->id; $sd->name = $forumrec->name; $sd->assessed = $forumrec->assessed; $sd->message = $forumrec->intro; $sd->messageformat = $forumrec->introformat; $sd->messagetrust = true; $sd->mailnow = false; $sdid = forum_add_discussion($sd, null, null, $this->task->get_userid()); // Mark the post as mailed $DB->set_field ('forum_posts','mailed', '1', array('discussion' => $sdid)); // Copy all the files from mod_foum/intro to mod_forum/post $fs = get_file_storage(); $files = $fs->get_area_files($this->task->get_contextid(), 'mod_forum', 'intro'); foreach ($files as $file) { $newfilerecord = new stdClass(); $newfilerecord->filearea = 'post'; $newfilerecord->itemid = $DB->get_field('forum_discussions', 'firstpost', array('id' => $sdid)); $fs->create_file_from_storedfile($newfilerecord, $file); } } } } PK-B\A#!#!(backup/moodle2/backup_forum_stepslib.phpnu[. /** * @package mod_forum * @subpackage backup-moodle2 * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Define all the backup steps that will be used by the backup_forum_activity_task */ /** * Define the complete forum structure for backup, with file and id annotations */ class backup_forum_activity_structure_step extends backup_activity_structure_step { protected function define_structure() { // To know if we are including userinfo $userinfo = $this->get_setting_value('userinfo'); // Define each element separated $forum = new backup_nested_element('forum', array('id'), array( 'type', 'name', 'intro', 'introformat', 'duedate', 'cutoffdate', 'assessed', 'assesstimestart', 'assesstimefinish', 'scale', 'maxbytes', 'maxattachments', 'forcesubscribe', 'trackingtype', 'rsstype', 'rssarticles', 'timemodified', 'warnafter', 'blockafter', 'blockperiod', 'completiondiscussions', 'completionreplies', 'completionposts', 'displaywordcount', 'lockdiscussionafter', 'grade_forum')); $discussions = new backup_nested_element('discussions'); $discussion = new backup_nested_element('discussion', array('id'), array( 'name', 'firstpost', 'userid', 'groupid', 'assessed', 'timemodified', 'usermodified', 'timestart', 'timeend', 'pinned', 'timelocked')); $posts = new backup_nested_element('posts'); $post = new backup_nested_element('post', array('id'), array( 'parent', 'userid', 'created', 'modified', 'mailed', 'subject', 'message', 'messageformat', 'messagetrust', 'attachment', 'totalscore', 'mailnow', 'privatereplyto')); $tags = new backup_nested_element('poststags'); $tag = new backup_nested_element('tag', array('id'), array('itemid', 'rawname')); $ratings = new backup_nested_element('ratings'); $rating = new backup_nested_element('rating', array('id'), array( 'component', 'ratingarea', 'scaleid', 'value', 'userid', 'timecreated', 'timemodified')); $discussionsubs = new backup_nested_element('discussion_subs'); $discussionsub = new backup_nested_element('discussion_sub', array('id'), array( 'userid', 'preference', )); $subscriptions = new backup_nested_element('subscriptions'); $subscription = new backup_nested_element('subscription', array('id'), array( 'userid')); $digests = new backup_nested_element('digests'); $digest = new backup_nested_element('digest', array('id'), array( 'userid', 'maildigest')); $readposts = new backup_nested_element('readposts'); $read = new backup_nested_element('read', array('id'), array( 'userid', 'discussionid', 'postid', 'firstread', 'lastread')); $trackedprefs = new backup_nested_element('trackedprefs'); $track = new backup_nested_element('track', array('id'), array( 'userid')); $grades = new backup_nested_element('grades'); $grade = new backup_nested_element('grade', ['id'], [ 'forum', 'itemnumber', 'userid', 'grade', 'timecreated', 'timemodified', ]); // Build the tree $forum->add_child($discussions); $discussions->add_child($discussion); $forum->add_child($subscriptions); $subscriptions->add_child($subscription); $forum->add_child($digests); $digests->add_child($digest); $forum->add_child($readposts); $readposts->add_child($read); $forum->add_child($trackedprefs); $trackedprefs->add_child($track); $forum->add_child($tags); $tags->add_child($tag); $forum->add_child($grades); $grades->add_child($grade); $discussion->add_child($posts); $posts->add_child($post); $post->add_child($ratings); $ratings->add_child($rating); $discussion->add_child($discussionsubs); $discussionsubs->add_child($discussionsub); // Define sources $forum->set_source_table('forum', array('id' => backup::VAR_ACTIVITYID)); // All these source definitions only happen if we are including user info if ($userinfo) { $discussion->set_source_sql(' SELECT * FROM {forum_discussions} WHERE forum = ?', array(backup::VAR_PARENTID)); // Need posts ordered by id so parents are always before childs on restore $post->set_source_table('forum_posts', array('discussion' => backup::VAR_PARENTID), 'id ASC'); $discussionsub->set_source_table('forum_discussion_subs', array('discussion' => backup::VAR_PARENTID)); $subscription->set_source_table('forum_subscriptions', array('forum' => backup::VAR_PARENTID)); $digest->set_source_table('forum_digests', array('forum' => backup::VAR_PARENTID)); $read->set_source_table('forum_read', array('forumid' => backup::VAR_PARENTID)); $track->set_source_table('forum_track_prefs', array('forumid' => backup::VAR_PARENTID)); $rating->set_source_table('rating', array('contextid' => backup::VAR_CONTEXTID, 'component' => backup_helper::is_sqlparam('mod_forum'), 'ratingarea' => backup_helper::is_sqlparam('post'), 'itemid' => backup::VAR_PARENTID)); $rating->set_source_alias('rating', 'value'); if (core_tag_tag::is_enabled('mod_forum', 'forum_posts')) { // Backup all tags for all forum posts in this forum. $tag->set_source_sql('SELECT t.id, ti.itemid, t.rawname FROM {tag} t JOIN {tag_instance} ti ON ti.tagid = t.id WHERE ti.itemtype = ? AND ti.component = ? AND ti.contextid = ?', array( backup_helper::is_sqlparam('forum_posts'), backup_helper::is_sqlparam('mod_forum'), backup::VAR_CONTEXTID)); } $grade->set_source_table('forum_grades', array('forum' => backup::VAR_PARENTID)); } // Define id annotations $forum->annotate_ids('scale', 'scale'); $discussion->annotate_ids('group', 'groupid'); $post->annotate_ids('user', 'userid'); $discussionsub->annotate_ids('user', 'userid'); $rating->annotate_ids('scale', 'scaleid'); $rating->annotate_ids('user', 'userid'); $subscription->annotate_ids('user', 'userid'); $digest->annotate_ids('user', 'userid'); $read->annotate_ids('user', 'userid'); $track->annotate_ids('user', 'userid'); $grade->annotate_ids('userid', 'userid'); $grade->annotate_ids('forum', 'forum'); // Define file annotations $forum->annotate_files('mod_forum', 'intro', null); // This file area hasn't itemid $post->annotate_files('mod_forum', 'post', 'id'); $post->annotate_files('mod_forum', 'attachment', 'id'); // Return the root element (forum), wrapped into standard activity structure return $this->prepare_activity_structure($forum); } } PK-B\xx+backup/moodle2/backup_forum_settingslib.phpnu[. /** * @package mod_forum * @subpackage backup-moodle2 * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // This activity has not particular settings but the inherited from the generic // backup_activity_task so here there isn't any class definition, like the ones // existing in /backup/moodle2/backup_settingslib.php (activities section) PK-B\#—4backup/moodle2/restore_forum_activity_task.class.phpnu[. /** * @package mod_forum * @subpackage backup-moodle2 * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/forum/backup/moodle2/restore_forum_stepslib.php'); // Because it exists (must) /** * forum restore task that provides all the settings and steps to perform one * complete restore of the activity */ class restore_forum_activity_task extends restore_activity_task { /** * Define (add) particular settings this activity can have */ protected function define_my_settings() { // No particular settings for this activity } /** * Define (add) particular steps this activity can have */ protected function define_my_steps() { // Choice only has one structure step $this->add_step(new restore_forum_activity_structure_step('forum_structure', 'forum.xml')); } /** * Define the contents in the activity that must be * processed by the link decoder */ public static function define_decode_contents() { $contents = array(); $contents[] = new restore_decode_content('forum', array('intro'), 'forum'); $contents[] = new restore_decode_content('forum_posts', array('message'), 'forum_post'); return $contents; } /** * Define the decoding rules for links belonging * to the activity to be executed by the link decoder */ public static function define_decode_rules() { $rules = array(); // List of forums in course $rules[] = new restore_decode_rule('FORUMINDEX', '/mod/forum/index.php?id=$1', 'course'); // Forum by cm->id and forum->id $rules[] = new restore_decode_rule('FORUMVIEWBYID', '/mod/forum/view.php?id=$1', 'course_module'); $rules[] = new restore_decode_rule('FORUMVIEWBYF', '/mod/forum/view.php?f=$1', 'forum'); // Link to forum discussion $rules[] = new restore_decode_rule('FORUMDISCUSSIONVIEW', '/mod/forum/discuss.php?d=$1', 'forum_discussion'); // Link to discussion with parent and with anchor posts $rules[] = new restore_decode_rule('FORUMDISCUSSIONVIEWPARENT', '/mod/forum/discuss.php?d=$1&parent=$2', array('forum_discussion', 'forum_post')); $rules[] = new restore_decode_rule('FORUMDISCUSSIONVIEWINSIDE', '/mod/forum/discuss.php?d=$1#$2', array('forum_discussion', 'forum_post')); return $rules; } /** * Define the restore log rules that will be applied * by the {@link restore_logs_processor} when restoring * forum logs. It must return one array * of {@link restore_log_rule} objects */ public static function define_restore_log_rules() { $rules = array(); $rules[] = new restore_log_rule('forum', 'add', 'view.php?id={course_module}', '{forum}'); $rules[] = new restore_log_rule('forum', 'update', 'view.php?id={course_module}', '{forum}'); $rules[] = new restore_log_rule('forum', 'view', 'view.php?id={course_module}', '{forum}'); $rules[] = new restore_log_rule('forum', 'view forum', 'view.php?id={course_module}', '{forum}'); $rules[] = new restore_log_rule('forum', 'mark read', 'view.php?f={forum}', '{forum}'); $rules[] = new restore_log_rule('forum', 'start tracking', 'view.php?f={forum}', '{forum}'); $rules[] = new restore_log_rule('forum', 'stop tracking', 'view.php?f={forum}', '{forum}'); $rules[] = new restore_log_rule('forum', 'subscribe', 'view.php?f={forum}', '{forum}'); $rules[] = new restore_log_rule('forum', 'unsubscribe', 'view.php?f={forum}', '{forum}'); $rules[] = new restore_log_rule('forum', 'subscriber', 'subscribers.php?id={forum}', '{forum}'); $rules[] = new restore_log_rule('forum', 'subscribers', 'subscribers.php?id={forum}', '{forum}'); $rules[] = new restore_log_rule('forum', 'view subscribers', 'subscribers.php?id={forum}', '{forum}'); $rules[] = new restore_log_rule('forum', 'add discussion', 'discuss.php?d={forum_discussion}', '{forum_discussion}'); $rules[] = new restore_log_rule('forum', 'view discussion', 'discuss.php?d={forum_discussion}', '{forum_discussion}'); $rules[] = new restore_log_rule('forum', 'move discussion', 'discuss.php?d={forum_discussion}', '{forum_discussion}'); $rules[] = new restore_log_rule('forum', 'delete discussi', 'view.php?id={course_module}', '{forum}', null, 'delete discussion'); $rules[] = new restore_log_rule('forum', 'delete discussion', 'view.php?id={course_module}', '{forum}'); $rules[] = new restore_log_rule('forum', 'add post', 'discuss.php?d={forum_discussion}&parent={forum_post}', '{forum_post}'); $rules[] = new restore_log_rule('forum', 'update post', 'discuss.php?d={forum_discussion}#p{forum_post}&parent={forum_post}', '{forum_post}'); $rules[] = new restore_log_rule('forum', 'update post', 'discuss.php?d={forum_discussion}&parent={forum_post}', '{forum_post}'); $rules[] = new restore_log_rule('forum', 'prune post', 'discuss.php?d={forum_discussion}', '{forum_post}'); $rules[] = new restore_log_rule('forum', 'delete post', 'discuss.php?d={forum_discussion}', '[post]'); return $rules; } /** * Define the restore log rules that will be applied * by the {@link restore_logs_processor} when restoring * course logs. It must return one array * of {@link restore_log_rule} objects * * Note this rules are applied when restoring course logs * by the restore final task, but are defined here at * activity level. All them are rules not linked to any module instance (cmid = 0) */ public static function define_restore_log_rules_for_course() { $rules = array(); $rules[] = new restore_log_rule('forum', 'view forums', 'index.php?id={course}', null); $rules[] = new restore_log_rule('forum', 'subscribeall', 'index.php?id={course}', '{course}'); $rules[] = new restore_log_rule('forum', 'unsubscribeall', 'index.php?id={course}', '{course}'); $rules[] = new restore_log_rule('forum', 'user report', 'user.php?course={course}&id={user}&mode=[mode]', '{user}'); $rules[] = new restore_log_rule('forum', 'search', 'search.php?id={course}&search=[searchenc]', '[search]'); return $rules; } } PK-B\2 3backup/moodle2/backup_forum_activity_task.class.phpnu[. /** * Defines backup_forum_activity_task class * * @package mod_forum * @category backup * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/forum/backup/moodle2/backup_forum_stepslib.php'); require_once($CFG->dirroot . '/mod/forum/backup/moodle2/backup_forum_settingslib.php'); /** * Provides the steps to perform one complete backup of the Forum instance */ class backup_forum_activity_task extends backup_activity_task { /** * No specific settings for this activity */ protected function define_my_settings() { } /** * Defines a backup step to store the instance data in the forum.xml file */ protected function define_my_steps() { $this->add_step(new backup_forum_activity_structure_step('forum structure', 'forum.xml')); } /** * Encodes URLs to the index.php, view.php and discuss.php scripts * * @param string $content some HTML text that eventually contains URLs to the activity instance scripts * @return string the content with the URLs encoded */ public static function encode_content_links($content) { global $CFG; $base = preg_quote($CFG->wwwroot,"/"); // Link to the list of forums $search="/(".$base."\/mod\/forum\/index.php\?id\=)([0-9]+)/"; $content= preg_replace($search, '$@FORUMINDEX*$2@$', $content); // Link to forum view by moduleid $search="/(".$base."\/mod\/forum\/view.php\?id\=)([0-9]+)/"; $content= preg_replace($search, '$@FORUMVIEWBYID*$2@$', $content); // Link to forum view by forumid $search="/(".$base."\/mod\/forum\/view.php\?f\=)([0-9]+)/"; $content= preg_replace($search, '$@FORUMVIEWBYF*$2@$', $content); // Link to forum discussion with parent syntax $search = "/(".$base."\/mod\/forum\/discuss.php\?d\=)([0-9]+)(?:\&|\&)parent\=([0-9]+)/"; $content= preg_replace($search, '$@FORUMDISCUSSIONVIEWPARENT*$2*$3@$', $content); // Link to forum discussion with relative syntax $search="/(".$base."\/mod\/forum\/discuss.php\?d\=)([0-9]+)\#([0-9]+)/"; $content= preg_replace($search, '$@FORUMDISCUSSIONVIEWINSIDE*$2*$3@$', $content); // Link to forum discussion by discussionid $search="/(".$base."\/mod\/forum\/discuss.php\?d\=)([0-9]+)/"; $content= preg_replace($search, '$@FORUMDISCUSSIONVIEW*$2@$', $content); return $content; } } PK-B\backup/moodle1/lib.phpnu[. /** * Provides support for the conversion of moodle1 backup to the moodle2 format * * @package mod_forum * @copyright 2011 Mark Nielsen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Forum conversion handler */ class moodle1_mod_forum_handler extends moodle1_mod_handler { /** @var moodle1_file_manager */ protected $fileman = null; /** @var int cmid */ protected $moduleid = null; /** * Declare the paths in moodle.xml we are able to convert * * The method returns list of {@link convert_path} instances. * For each path returned, the corresponding conversion method must be * defined. * * Note that the paths /MOODLE_BACKUP/COURSE/MODULES/MOD/FORUM do not * actually exist in the file. The last element with the module name was * appended by the moodle1_converter class. * * @return array of {@link convert_path} instances */ public function get_paths() { return array( new convert_path('forum', '/MOODLE_BACKUP/COURSE/MODULES/MOD/FORUM', array( 'renamefields' => array( 'format' => 'messageformat', ), 'newfields' => array( 'completiondiscussions' => 0, 'completionreplies' => 0, 'completionpost' => 0, 'maxattachments' => 1, 'introformat' => 0, ), ) ), ); } /** * Converts /MOODLE_BACKUP/COURSE/MODULES/MOD/FORUM data */ public function process_forum($data) { global $CFG; // get the course module id and context id $instanceid = $data['id']; $cminfo = $this->get_cminfo($instanceid); $this->moduleid = $cminfo['id']; $contextid = $this->converter->get_contextid(CONTEXT_MODULE, $this->moduleid); // get a fresh new file manager for this instance $this->fileman = $this->converter->get_file_manager($contextid, 'mod_forum'); // convert course files embedded into the intro $this->fileman->filearea = 'intro'; $this->fileman->itemid = 0; $data['intro'] = moodle1_converter::migrate_referenced_files($data['intro'], $this->fileman); // Convert the introformat if necessary. if ($CFG->texteditors !== 'textarea') { $data['intro'] = text_to_html($data['intro'], false, false, true); $data['introformat'] = FORMAT_HTML; } // start writing forum.xml $this->open_xml_writer("activities/forum_{$this->moduleid}/forum.xml"); $this->xmlwriter->begin_tag('activity', array('id' => $instanceid, 'moduleid' => $this->moduleid, 'modulename' => 'forum', 'contextid' => $contextid)); $this->xmlwriter->begin_tag('forum', array('id' => $instanceid)); foreach ($data as $field => $value) { if ($field <> 'id') { $this->xmlwriter->full_tag($field, $value); } } $this->xmlwriter->begin_tag('discussions'); return $data; } /** * This is executed when we reach the closing tag of our 'forum' path */ public function on_forum_end() { // finish writing forum.xml $this->xmlwriter->end_tag('discussions'); $this->xmlwriter->end_tag('forum'); $this->xmlwriter->end_tag('activity'); $this->close_xml_writer(); // write inforef.xml $this->open_xml_writer("activities/forum_{$this->moduleid}/inforef.xml"); $this->xmlwriter->begin_tag('inforef'); $this->xmlwriter->begin_tag('fileref'); foreach ($this->fileman->get_fileids() as $fileid) { $this->write_xml('file', array('id' => $fileid)); } $this->xmlwriter->end_tag('fileref'); $this->xmlwriter->end_tag('inforef'); $this->close_xml_writer(); } } PK-B\4))view.phpnu[. /** * Displays the list of discussions in a forum. * * @package mod_forum * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use mod_forum\grades\forum_gradeitem; require_once('../../config.php'); $managerfactory = mod_forum\local\container::get_manager_factory(); $legacydatamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory(); $vaultfactory = mod_forum\local\container::get_vault_factory(); $forumvault = $vaultfactory->get_forum_vault(); $discussionvault = $vaultfactory->get_discussion_vault(); $postvault = $vaultfactory->get_post_vault(); $discussionlistvault = $vaultfactory->get_discussions_in_forum_vault(); $cmid = optional_param('id', 0, PARAM_INT); $forumid = optional_param('f', 0, PARAM_INT); $mode = optional_param('mode', 0, PARAM_INT); $showall = optional_param('showall', '', PARAM_INT); $pageno = optional_param('page', 0, PARAM_INT); $search = optional_param('search', '', PARAM_CLEAN); $pageno = optional_param('p', $pageno, PARAM_INT); $pagesize = optional_param('s', $CFG->forum_manydiscussions ?? 0, PARAM_INT); $sortorder = optional_param('o', null, PARAM_INT); if (!$cmid && !$forumid) { throw new \moodle_exception('missingparameter'); } if ($cmid) { $forum = $forumvault->get_from_course_module_id($cmid); if (empty($forum)) { throw new \moodle_exception('Unable to find forum with cmid ' . $cmid); } } else { $forum = $forumvault->get_from_id($forumid); if (empty($forum)) { throw new \moodle_exception('Unable to find forum with id ' . $forumid); } } if (!empty($showall)) { // The user wants to see all discussions. $pageno = 0; $pagesize = 0; } $urlfactory = mod_forum\local\container::get_url_factory(); $capabilitymanager = $managerfactory->get_capability_manager($forum); $url = $urlfactory->get_forum_view_url_from_forum($forum); $PAGE->set_url($url); $course = $forum->get_course_record(); $coursemodule = $forum->get_course_module_record(); $cm = \cm_info::create($coursemodule); require_course_login($course, true, $cm); $istypesingle = $forum->get_type() === 'single'; $saveddisplaymode = get_user_preferences('forum_displaymode', $CFG->forum_displaymode); if ($mode) { $displaymode = $mode; } else { $displaymode = $saveddisplaymode; } if (get_user_preferences('forum_useexperimentalui', false)) { if ($displaymode == FORUM_MODE_NESTED) { $displaymode = FORUM_MODE_NESTED_V2; } } else { if ($displaymode == FORUM_MODE_NESTED_V2) { $displaymode = FORUM_MODE_NESTED; } } if ($displaymode != $saveddisplaymode) { set_user_preference('forum_displaymode', $displaymode); } $PAGE->set_context($forum->get_context()); $PAGE->set_title($forum->get_name()); $PAGE->add_body_class('forumtype-' . $forum->get_type()); $PAGE->set_heading($course->fullname); $PAGE->add_header_action(forum_search_form($course, $search)); if ($istypesingle && $displaymode == FORUM_MODE_NESTED_V2) { $PAGE->add_body_class('nested-v2-display-mode reset-style'); $settingstrigger = $OUTPUT->render_from_template('mod_forum/settings_drawer_trigger', null); $PAGE->add_header_action($settingstrigger); } if (empty($cm->visible) && !has_capability('moodle/course:viewhiddenactivities', $forum->get_context())) { redirect( $urlfactory->get_course_url_from_forum($forum), get_string('activityiscurrentlyhidden'), null, \core\output\notification::NOTIFY_WARNING ); } if (!$capabilitymanager->can_view_discussions($USER)) { redirect( $urlfactory->get_course_url_from_forum($forum), get_string('noviewdiscussionspermission', 'forum'), null, \core\output\notification::NOTIFY_WARNING ); } // Mark viewed and trigger the course_module_viewed event. $forumdatamapper = $legacydatamapperfactory->get_forum_data_mapper(); $forumrecord = $forumdatamapper->to_legacy_object($forum); $PAGE->set_activity_record($forumrecord); forum_view( $forumrecord, $forum->get_course_record(), $forum->get_course_module_record(), $forum->get_context() ); // Return here if we post or set subscription etc. $SESSION->fromdiscussion = qualified_me(); if (!empty($CFG->enablerssfeeds) && !empty($CFG->forum_enablerssfeeds) && $forum->get_rss_type() && $forum->get_rss_articles()) { require_once("{$CFG->libdir}/rsslib.php"); $rsstitle = format_string($course->shortname, true, [ 'context' => context_course::instance($course->id), ]) . ': ' . format_string($forum->get_name()); rss_add_http_header($forum->get_context(), 'mod_forum', $forumrecord, $rsstitle); } $activityheader = $PAGE->activityheader; $pageheader = []; if ($activityheader->is_title_allowed()) { $pageheader['title'] = format_string($forum->get_name()); } // Fetch the current groupid. $groupid = groups_get_activity_group($cm, true) ?: null; if ($istypesingle || empty($forum->get_intro())) { $pageheader['description'] = ''; } $activityheader->set_attrs($pageheader); echo $OUTPUT->header(); $rendererfactory = mod_forum\local\container::get_renderer_factory(); // The elements for view action are rendered and added to the page. echo forum_activity_actionbar($forum, $groupid, $course, $search); if ($sortorder) { set_user_preference('forum_discussionlistsortorder', $sortorder); } $sortorder = get_user_preferences('forum_discussionlistsortorder', $discussionlistvault::SORTORDER_LASTPOST_DESC); switch ($forum->get_type()) { case 'single': $forumgradeitem = forum_gradeitem::load_from_forum_entity($forum); if ($capabilitymanager->can_grade($USER)) { if ($forumgradeitem->is_grading_enabled()) { $groupid = groups_get_activity_group($cm, true) ?: null; $gradeobj = (object) [ 'contextid' => $forum->get_context()->id, 'cmid' => $forum->get_course_module_record()->id, 'name' => format_string($forum->get_name()), 'courseid' => $course->id, 'coursename' => format_string($course->shortname), 'experimentaldisplaymode' => $displaymode == FORUM_MODE_NESTED_V2, 'groupid' => $groupid, 'gradingcomponent' => $forumgradeitem->get_grading_component_name(), 'gradingcomponentsubtype' => $forumgradeitem->get_grading_component_subtype(), 'sendstudentnotifications' => $forum->should_notify_students_default_when_grade_for_forum(), 'gradeonlyactiveusers' => $forumgradeitem->should_grade_only_active_users(), ]; echo $OUTPUT->render_from_template('mod_forum/grades/grade_button', $gradeobj); } } else { if ($forumgradeitem->is_grading_enabled()) { $groupid = groups_get_activity_group($cm, true) ?: null; $gradeobj = (object) [ 'contextid' => $forum->get_context()->id, 'cmid' => $forum->get_course_module_record()->id, 'name' => format_string($forum->get_name()), 'courseid' => $course->id, 'coursename' => format_string($course->shortname), 'groupid' => $groupid, 'userid' => $USER->id, 'gradingcomponent' => $forumgradeitem->get_grading_component_name(), 'gradingcomponentsubtype' => $forumgradeitem->get_grading_component_subtype(), ]; echo $OUTPUT->render_from_template('mod_forum/grades/view_grade_button', $gradeobj); } } $discussion = $discussionvault->get_last_discussion_in_forum($forum); $discussioncount = $discussionvault->get_count_discussions_in_forum($forum); $hasmultiplediscussions = $discussioncount > 1; $discussionsrenderer = $rendererfactory->get_single_discussion_list_renderer($forum, $discussion, $hasmultiplediscussions, $displaymode); $post = $postvault->get_from_id($discussion->get_first_post_id()); $orderpostsby = $displaymode == FORUM_MODE_FLATNEWEST ? 'created DESC' : 'created ASC'; $replies = $postvault->get_replies_to_post( $USER, $post, $capabilitymanager->can_view_any_private_reply($USER), $orderpostsby ); echo $discussionsrenderer->render($USER, $post, $replies); if (!$CFG->forum_usermarksread && forum_tp_is_tracked($forumrecord, $USER)) { $postids = array_map(function($post) { return $post->get_id(); }, array_merge([$post], array_values($replies))); forum_tp_mark_posts_read($USER, $postids); } break; case 'blog': $discussionsrenderer = $rendererfactory->get_blog_discussion_list_renderer($forum); // Blog forums always show discussions newest first. echo $discussionsrenderer->render($USER, $cm, $groupid, $discussionlistvault::SORTORDER_CREATED_DESC, $pageno, $pagesize, null, false); if (!$CFG->forum_usermarksread && forum_tp_is_tracked($forumrecord, $USER)) { $discussions = mod_forum_get_discussion_summaries($forum, $USER, $groupid, null, $pageno, $pagesize); $firstpostids = array_map(function($discussion) { return $discussion->get_first_post()->get_id(); }, array_values($discussions)); forum_tp_mark_posts_read($USER, $firstpostids); } break; default: $discussionsrenderer = $rendererfactory->get_discussion_list_renderer($forum); echo $discussionsrenderer->render($USER, $cm, $groupid, $sortorder, $pageno, $pagesize, $displaymode, false); } echo $OUTPUT->footer(); PK-B\j'@'@ rsslib.phpnu[. /** * This file adds support to rss feeds generation * * @package mod_forum * @category rss * @copyright 2001 Eloy Lafuente (stronk7) http://contiento.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /* Include the core RSS lib */ require_once($CFG->libdir.'/rsslib.php'); /** * Returns the path to the cached rss feed contents. Creates/updates the cache if necessary. * @param stdClass $context the context * @param array $args the arguments received in the url * @return string the full path to the cached RSS feed directory. Null if there is a problem. */ function forum_rss_get_feed($context, $args) { global $CFG, $DB, $USER; $status = true; //are RSS feeds enabled? if (empty($CFG->forum_enablerssfeeds)) { debugging('DISABLED (module configuration)'); return null; } $forumid = clean_param($args[3], PARAM_INT); $cm = get_coursemodule_from_instance('forum', $forumid, 0, false, MUST_EXIST); $modcontext = context_module::instance($cm->id); //context id from db should match the submitted one if ($context->id != $modcontext->id || !has_capability('mod/forum:viewdiscussion', $modcontext)) { return null; } $forum = $DB->get_record('forum', array('id' => $forumid), '*', MUST_EXIST); if (!rss_enabled_for_mod('forum', $forum)) { return null; } //the sql that will retreive the data for the feed and be hashed to get the cache filename list($sql, $params) = forum_rss_get_sql($forum, $cm); // Hash the sql to get the cache file name. $filename = rss_get_file_name($forum, $sql, $params); $cachedfilepath = rss_get_file_full_name('mod_forum', $filename); //Is the cache out of date? $cachedfilelastmodified = 0; if (file_exists($cachedfilepath)) { $cachedfilelastmodified = filemtime($cachedfilepath); } // Used to determine if we need to generate a new RSS feed. $dontrecheckcutoff = time() - 60; // Sixty seconds ago. // If it hasn't been generated we need to create it. // Otherwise, if it has been > 60 seconds since we last updated, check for new items. if (($cachedfilelastmodified == 0) || (($dontrecheckcutoff > $cachedfilelastmodified) && forum_rss_newstuff($forum, $cm, $cachedfilelastmodified))) { // Need to regenerate the cached version. $result = forum_rss_feed_contents($forum, $sql, $params, $modcontext); $status = rss_save_file('mod_forum', $filename, $result); } //return the path to the cached version return $cachedfilepath; } /** * Given a forum object, deletes all cached RSS files associated with it. * * @param stdClass $forum */ function forum_rss_delete_file($forum) { rss_delete_file('mod_forum', $forum); } /////////////////////////////////////////////////////// //Utility functions /** * If there is new stuff in the forum since $time this returns true * Otherwise it returns false. * * @param stdClass $forum the forum object * @param stdClass $cm Course Module object * @param int $time check for items since this epoch timestamp * @return bool True for new items */ function forum_rss_newstuff($forum, $cm, $time) { global $DB; list($sql, $params) = forum_rss_get_sql($forum, $cm, $time); return $DB->record_exists_sql($sql, $params); } /** * Determines which type of SQL query is required, one for posts or one for discussions, and returns the appropriate query * * @param stdClass $forum the forum object * @param stdClass $cm Course Module object * @param int $time check for items since this epoch timestamp * @return string the SQL query to be used to get the Discussion/Post details from the forum table of the database */ function forum_rss_get_sql($forum, $cm, $time=0) { if ($forum->rsstype == 1) { // Discussion RSS return forum_rss_feed_discussions_sql($forum, $cm, $time); } else { // Post RSS return forum_rss_feed_posts_sql($forum, $cm, $time); } } /** * Generates the SQL query used to get the Discussion details from the forum table of the database * * @param stdClass $forum the forum object * @param stdClass $cm Course Module object * @param int $newsince check for items since this epoch timestamp * @return string the SQL query to be used to get the Discussion details from the forum table of the database */ function forum_rss_feed_discussions_sql($forum, $cm, $newsince=0) { global $CFG, $DB, $USER; $timelimit = ''; $modcontext = null; $now = floor(time() / 60) * 60; // DB Cache Friendly. $params = array(); $modcontext = context_module::instance($cm->id); if (!empty($CFG->forum_enabletimedposts)) { /// Users must fulfill timed posts if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) { $timelimit = " AND ((d.timestart <= :now1 AND (d.timeend = 0 OR d.timeend > :now2))"; $params['now1'] = $now; $params['now2'] = $now; if (isloggedin()) { $timelimit .= " OR d.userid = :userid"; $params['userid'] = $USER->id; } $timelimit .= ")"; } } // Do we only want new posts? if ($newsince) { $params['newsince'] = $newsince; $newsince = " AND p.modified > :newsince"; } else { $newsince = ''; } // Get group enforcing SQL. $groupmode = groups_get_activity_groupmode($cm); $currentgroup = groups_get_activity_group($cm); list($groupselect, $groupparams) = forum_rss_get_group_sql($cm, $groupmode, $currentgroup, $modcontext); // Add the groupparams to the params array. $params = array_merge($params, $groupparams); $forumsort = "d.timemodified DESC"; $postdata = "p.id AS postid, p.subject, p.created as postcreated, p.modified, p.discussion, p.userid, p.message as postmessage, p.messageformat AS postformat, p.messagetrust AS posttrust"; $userfieldsapi = \core_user\fields::for_userpic(); $userpicturefields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects; $sql = "SELECT $postdata, d.id as discussionid, d.name as discussionname, d.timemodified, d.usermodified, d.groupid, d.timestart, d.timeend, $userpicturefields FROM {forum_discussions} d JOIN {forum_posts} p ON p.discussion = d.id JOIN {user} u ON p.userid = u.id WHERE d.forum = {$forum->id} AND p.parent = 0 AND p.deleted <> 1 $timelimit $groupselect $newsince ORDER BY $forumsort"; return array($sql, $params); } /** * Generates the SQL query used to get the Post details from the forum table of the database * * @param stdClass $forum the forum object * @param stdClass $cm Course Module object * @param int $newsince check for items since this epoch timestamp * @return string the SQL query to be used to get the Post details from the forum table of the database */ function forum_rss_feed_posts_sql($forum, $cm, $newsince=0) { global $USER; $modcontext = context_module::instance($cm->id); // Get group enforcement SQL. $groupmode = groups_get_activity_groupmode($cm); $currentgroup = groups_get_activity_group($cm); $params = array(); list($groupselect, $groupparams) = forum_rss_get_group_sql($cm, $groupmode, $currentgroup, $modcontext); // Add the groupparams to the params array. $params = array_merge($params, $groupparams); // Do we only want new posts? if ($newsince) { $params['newsince'] = $newsince; $newsince = " AND p.modified > :newsince"; } else { $newsince = ''; } $canseeprivatereplies = has_capability('mod/forum:readprivatereplies', $modcontext); if (!$canseeprivatereplies) { $privatewhere = ' AND (p.privatereplyto = :currentuser1 OR p.userid = :currentuser2 OR p.privatereplyto = 0)'; $params['currentuser1'] = $USER->id; $params['currentuser2'] = $USER->id; } else { $privatewhere = ''; } $userfieldsapi = \core_user\fields::for_name(); $usernamefields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $sql = "SELECT p.id AS postid, d.id AS discussionid, d.name AS discussionname, d.groupid, d.timestart, d.timeend, u.id AS userid, $usernamefields, p.subject AS postsubject, p.message AS postmessage, p.created AS postcreated, p.messageformat AS postformat, p.messagetrust AS posttrust, p.parent as postparent FROM {forum_discussions} d, {forum_posts} p, {user} u WHERE d.forum = {$forum->id} AND p.discussion = d.id AND p.deleted <> 1 AND u.id = p.userid $newsince $privatewhere $groupselect ORDER BY p.created desc"; return array($sql, $params); } /** * Retrieve the correct SQL snippet for group-only forums * * @param stdClass $cm Course Module object * @param int $groupmode the mode in which the forum's groups are operating * @param bool $currentgroup true if the user is from the a group enabled on the forum * @param stdClass $modcontext The context instance of the forum module * @return string SQL Query for group details of the forum */ function forum_rss_get_group_sql($cm, $groupmode, $currentgroup, $modcontext=null) { $groupselect = ''; $params = array(); if ($groupmode) { if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) { if ($currentgroup) { $groupselect = "AND (d.groupid = :groupid OR d.groupid = -1)"; $params['groupid'] = $currentgroup; } } else { // Separate groups without access all. if ($currentgroup) { $groupselect = "AND (d.groupid = :groupid OR d.groupid = -1)"; $params['groupid'] = $currentgroup; } else { $groupselect = "AND d.groupid = -1"; } } } return array($groupselect, $params); } /** * This function return the XML rss contents about the forum * It returns false if something is wrong * * @param stdClass $forum the forum object * @param string $sql the SQL used to retrieve the contents from the database * @param array $params the SQL parameters used * @param object $context the context this forum relates to * @return bool|string false if the contents is empty, otherwise the contents of the feed is returned * * @Todo MDL-31129 implement post attachment handling */ function forum_rss_feed_contents($forum, $sql, $params, $context) { global $CFG, $DB, $USER; $recs = $DB->get_recordset_sql($sql, $params, 0, $forum->rssarticles); //set a flag. Are we displaying discussions or posts? $isdiscussion = true; if (!empty($forum->rsstype) && $forum->rsstype!=1) { $isdiscussion = false; } if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course)) { throw new \moodle_exception('invalidcoursemodule'); } $items = array(); foreach ($recs as $rec) { $item = new stdClass(); $discussion = new stdClass(); $discussion->id = $rec->discussionid; $discussion->groupid = $rec->groupid; $discussion->timestart = $rec->timestart; $discussion->timeend = $rec->timeend; $post = null; if (!$isdiscussion) { $post = new stdClass(); $post->id = $rec->postid; $post->parent = $rec->postparent; $post->userid = $rec->userid; } if ($isdiscussion && !forum_user_can_see_discussion($forum, $discussion, $context)) { // This is a discussion which the user has no permission to view $item->title = get_string('forumsubjecthidden', 'forum'); $message = get_string('forumbodyhidden', 'forum'); $item->author = get_string('forumauthorhidden', 'forum'); } else if (!$isdiscussion && !forum_user_can_see_post($forum, $discussion, $post, $USER, $cm)) { if (forum_user_can_see_post($forum, $discussion, $post, $USER, $cm, false)) { // This is a post which the user has no permission to view. $item->title = get_string('forumsubjecthidden', 'forum'); $message = get_string('forumbodyhidden', 'forum'); $item->author = get_string('forumauthorhidden', 'forum'); } else { // This is a post which has been deleted. $item->title = get_string('forumsubjectdeleted', 'mod_forum'); $message = get_string('forumbodydeleted', 'mod_forum'); $item->author = get_string('forumauthorhidden', 'forum'); } } else { // The user must have permission to view if ($isdiscussion && !empty($rec->discussionname)) { $item->title = format_string($rec->discussionname); } else if (!empty($rec->postsubject)) { $item->title = format_string($rec->postsubject); } else { //we should have an item title by now but if we dont somehow then substitute something somewhat meaningful $item->title = format_string($forum->name.' '.userdate($rec->postcreated,get_string('strftimedatetimeshort', 'langconfig'))); } $item->author = fullname($rec); $message = file_rewrite_pluginfile_urls($rec->postmessage, 'pluginfile.php', $context->id, 'mod_forum', 'post', $rec->postid); } if ($isdiscussion) { $item->link = $CFG->wwwroot."/mod/forum/discuss.php?d=".$rec->discussionid; } else { $item->link = $CFG->wwwroot."/mod/forum/discuss.php?d=".$rec->discussionid."&parent=".$rec->postid; } $item->description = format_text($message, $rec->postformat, [ 'context' => $context, 'trusted' => $rec->posttrust, ]); //TODO: MDL-31129 implement post attachment handling /*if (!$isdiscussion) { $post_file_area_name = str_replace('//', '/', "$forum->course/$CFG->moddata/forum/$forum->id/$rec->postid"); $post_files = get_directory_list("$CFG->dataroot/$post_file_area_name"); if (!empty($post_files)) { $item->attachments = array(); } }*/ $item->pubdate = $rec->postcreated; $items[] = $item; } $recs->close(); // Create the RSS header. $header = rss_standard_header(strip_tags(format_string($forum->name,true)), $CFG->wwwroot."/mod/forum/view.php?f=".$forum->id, format_string($forum->intro,true)); // TODO: fix format // Now all the RSS items, if there are any. $articles = ''; if (!empty($items)) { $articles = rss_add_items($items); } // Create the RSS footer. $footer = rss_standard_footer(); return $header . $articles . $footer; } PK-B\AdXXpix/i/pinned.svgnu[ PK-B\apix/hide-grader-panel.svgnu[ Group 3 Created with Sketch. PK-B\5qpix/show-grader-panel-rtl.svgnu[ show Created with Sketch. PK-B\pix/show-grader-panel.svgnu[ show Created with Sketch. PK-B\!sIIpix/hide-grader-panel.pngnu[PNG  IHDRB}sRGBDeXIfMM*iSIDATH 햽/AK(4 Q?A%N+:rBD Z pB|N277vw{3ϻ A;d 1 xQL2>w~3&i$EX0u\\yh^|G>ADf<2A|@>4 ̃J+S|bdfXf >ҶU!B::08Ux[* #p U!xEX@jj!6\1MIՖhl_F\uC:x]OWj-wq/bR0Di /Q}Ye:&mP k~#P~,cBvDWfz|h|Oz׊|g.9#*I:T=VғêoD`}IENDB`PK-B\s%pix/no-search-results.pngnu[PNG  IHDR:9q pHYs   IDATxiI~df]}\;;\rIAP- `SdʄMXh}ɂimH-4%Ja( †`0)%4 ,Ӽvg;3=3}tוsFETdQG֓d؞ʨ̌=Z+P?t5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS5E\PS~ I2ge['7DT-0^O̵MoU7WXSwi|Ol-q,`-L^tE}߈ݬ(ED7 v+P%bTVE *l+mueCmT4~Z]`: *m~*WX}í8qj{ZVYٍو+gOno5U:#~Wn`& '6:âD_[ndJoLA\`5e^+֊bڰjzyeoW(n~40?/q@-p qX1Vҫg~xcU*zy(I2?Uu`&^Mnd?خ9pfWX/mvmh^b Z#nԏA&U,) ") Ծ<>5rkn{AꈸRw20%"FlS4_RxB\)} Jէ?y#ףwlQ]ڡv(e@]WJxdjZQ|_wlPgĕ<{UOyG\)IߤL8JTd?JIB[g}mWFke@\)M4[{o+%ƵL)yŊdb%Z^z#`ĒYsk.Y@WGoY3VbŦ46ެm@ Wkm~sb֤6${'6+~fگwU\XCĕ ^@ݱ@MWq@MWq@MWq@MWq@MWq@MWq@MWq@MWq@MWq@MWq@MWq@MWq@MWq@MWq@MWq@MU7w%KW_Q Ly B"*7yw^?ލUb0gE̯rMmnK-͖jon#k㢟 䱴ڢBi o يZ! GDTm눸/HSaSG$hK%6 5E\.~O2)-Ƹ./-P, 0'4 )=ci P frvXH/5D\L/H/u@\Œ6|^ʩ'pCQ֫'B\bLN+KŘT)ξX @1ʬ/u/"@1it=l%ܟ.HEE)U*WucܟW^d놸s}J//;^\h}ZKzD\,Q}*O/$!"@ξ@@+P/E)e!NG\nS^s @^U5) bc}}\q$I&wRJeY6Vl6jϒK/"yeƘ<+n)"˲~qC/UH*Ϗ^K0.V9EsQJ7EENXNUmj^Aҋm>`n5\KOjmĢ9V:m\s"YeKU^D3!ͽ?}8`9:WࢬMEY2 O2cLUk,caמ?.R%˲<]R^iO'a}D粏~DQ-QƑ*4aժξXZvnbK >E>k˥M[I/J9Y!֌^B\ ?ͣJ'Q )OAOMu(c_3_٪c76QZR1c0{ݬ/e3cFIzAQ?7eZ¦j_*m#^@\˻~lS1]NsWOtpaIy>zs싵~ڤ^vɣWvX9'X ujW#(igw˫#5Y7-J"1얇- c޷ ̄v UW.+N_H4f&[k%ρKe.bYbӆ1?U BQ><}`ɮ@\k\Wω_5[h׎sYe8A̮\e _)g[}Ϫ6TJ^!p.Ot^ h4pӋl6Tkv#VIy^""3cAa"ԁy7e8[ZzqqOVKR~^Ra^.$IǝNRCÜ.g^1"bMTH/XkʯS˰j uH/# py8[%uL/qGI A(eɅR>:^ʩ{!`IWl5/K/["WED1(EA [0h̉{!`YБj[g_,fnMI/IyKq^*hVj6a@zer ζu:%DI"937y78T#H/CW,UR|3>ŭXoFrI/22",yҋLd{H/uFzڢoCOw&TZ0*Zc6]MU;5bRE(dp""fRTc}K@(VkxQzާ?\F"ҋU* a,cܹ.ȥK+6FsOc"xMhjRX1KQv>܅fjazQ,ܟW^ϥV=>K(OvcLլ=KRgE<{^%MUeeceYd.w`9(6mEJ锨$VZYRNM/XCzeҋvH/uU`:k;c䭵g_g`qj ˹ӋXkFryWUzqex=^Vu;O士T/Xz($J 1R%;=(1 ΏliW&0r7 H/b6flfEkmX\zpG bTBҋ;$~Oz) e^X !iz_غ9gz)M"^Vޙu/ p 놸3ÛnYe%Y>{EH/ wfzBzYRcZy瓟 .3 ;Zxh0<| ܑf~}nskm$ (KN{.URg$;Cmp5-T0˧F1`J "2ujEW`E^"V@!eloo]nÐR(+pQ:(Fz)hzn|%A2^|/ 7p9 I/"Bz)1CZ+02r^Vi::Pzqō+G; ^(Kl4ӋG+ Y%cʝ^dCzYY0"t}m D ?Nt8Kmfi{)ʋ+ldqh0Hl9% K M.sxEW`%׮l64FnΝ^X9* a<*I(N&Ӌ6 閭""^.dV=o;( LWϓ^dơ{E ~'KMfW*6^.\&cvvvn޼Yڷ&/8%LScj~dzId8S ^Dl^.o,>RVj cE)fٻ ÅW`J/å wsp 09WȪKkm٬˂:{STRg_#ËJ/F#ñ&Ӌ3SZQ:b^F WJOzjL/.Tؤ@= D\)Vg_ˤYCzK/Ek\Ԩ 40Xj^9u( qƭJ"{ uqM^\"ҋi C\^}H/NG=E\q]g_, 1gbqkUg_Yŝv zd8X gW+Xz#ã("3.:|! ! _C I/aAL qS(NΉξdE~a@0 IDAT8~)03xWrK2++THzW0Cz70{J,1X kVԩQJQgWS/ӋxGz *=BWx s"E=._ty7?Lnm_l]|xsk9^M)\qM_t/{7:<0ދ;j?ńRSKeer1 pƘ+ y>jQZ)_iwXzizg$I$q&I 0S]eWXu޶LDF^21^`o;_h]m(_) 6774M/ a B\o66XD&D{VM/w;~nM&s/h4FEkAVl+VcA+p:`m}ݼPzil&6r,"^$XkI/X=γgD\g9-dݑcJ+丑׽Z6H/"N9Ͼ>XU<@J/νЊx}XᘧhzQ"Rx0I/2HzAXGx@5^vޝ3ҋ+OVKErD%nETTg_Fx 3#E+g0^1Iad""fMM&Fg;ն I/2'^Gz͚ZuFxXwn pm싵x;7^koW#4̢`ш+c}^8WH`U+XH/ H/k]hH k˘Q F=03J&٣(<5ӝ^$^u(qZbMd0DQE$Q E=pIJ0 0q% Jp1]aI- D\ }"[հf_%Ve0N γ "$MSb%I2 ͲLDAajͦz Tb4W^D ^%~78/Q LpEUb /JY+4vIqt{15ȋ2ù<_~卍Ҿu P, 5׺ }ıafʱpZjJn3G9+(awj6 }`s&׺kEkkB&YbW%&9r1`سc0c|?Iׅҋs֮ysH/ H/e"Kl6Mv ;?T$;YkGn\sYav@\:QuCha=ِ^LH k3֭Ŏ93ȌLzV)+Y E kfVSy?Ӎx0DQE[Bzu(qXzJ?Q\;6ah5׫fC&Z0! jKASwGɈ+ beDyzFN׶'ZzK**^綖p8Rw4٣d`u4J)6q/"V{mYܡ+Ub^D̐^Z%#6\cO:^fC(Out6+q^>vT:{e*'(̬2ox>W ]lH/#ۏ.ynuuSxzH/@!+hFVr T1^ kYm?cW'|J$IW{k P+J_B+ Jlqx\SB|wZfI>s͒^ˣ mq]_W*7Z {,ͲLkJ$N.C^ 1^($x!G=*A\q8o\_PĮz\)>iH/0^4N^ `XLbj E\Vδud*˯ \Dz)Ah4Ǜ^`QgJWy^n\6UHV~}4q|XzqWݟVTz![b=ɋ>cш+ ~JhR=]677/?>vr2,`|\ p!Y+,BWnqŊh%+e1WB;(<6WR[e)3cJ9^NQ{Xrc<(Q6"k}_hi-ZF{ P٣r;$>%E(4mDqy)uAz-ujZ{$(q;Y#fFk+Yn5CY#?'ߪ_J V3 à (<+׬B JC\P D>+Bhy޸ӋxGz)Ftjul KTDHP"jTz?3>JQwZbA 7ڭvk$,1f-TrW3Ɣ? :3ۻ$]lm^Q9qu_ßy_yѳ]fKΈ+2/WK$(݃Ws09ӋL H/wxtnX}O alw7 zXmow7672ujatz"lo(duNWƬ$I/agۏbJ+8ƻߺw|XXk܋O[+׮l_t)d\Ax$M4Z'YY.aG gG)fY;lٯh!Ih{YQj>2omt^GO ՝vm\٣V XE:uoWQuڻTI?HQukh49KAZfϜ$I>;D+%^xKH)QShkO[͍kWo^)4k1}P2 :ݾ=Y1D9'.擤Q.=d:(/.vM^9$NZ ,ϟ<=Dkjtǿ\Ŋrݽhks浝vn]ŷS6y/ExZQ.RJ Nf^a{ )u^2s0^$㸶%i֚2rhx#Qo/RZ;W}4;=х-PJ{W7:{LzegnOkRhY+GQOYvuZU4Yud׏ЋRX3ĭVav#)zd13}0q˛^${l+V+Wssh }WnZf3M,Yərc5REV [xVqFa`=^oC=eAS]a}Kg=;/\Q`M3S'W,btz;>%~+|RJfi jVX>ͲLk͢QJyg<=hݾV-٣>xQe*vRqih?HdWRG݁{&/ 2˫*&gbj;a`d~՗n6b٣>+ɍYiR"^oV!{dqAvo^v\OŢpI/2U&L/0l[f+β?=iOSḊw<˷|zՍZ,Q`DY9,WGE{PRVq҅՝-UL/IDQo4M$?FyyVN){>{ƍ^Qu:{qXz "$|0 LMXcwlYQJey~loE2fWP2^666McLENaύ $J)"J)S'v<}WnZh1LuT+\x1R޵ Ԙ݃C[њTe^4AXTNkn9^8U^%U:O0 ->yK7WiS0y/E+rFƝ;t)8Jin4H;ݪ:ajßS+'X>zWn޸ &G%+$NAJNop8agƽtJ+E7|%X'1P%20F}`F韶{P"JI[Q~{LhMVYb?Ͳ{;=/RUX70G%+ZVeBJ(Ml֖"xdlYqZb(㯿1k%i<;<{n͙]/I(I/(Quydi*#V=}ݏ(=)h<߻1/]7DM.X}^S)%V׏?t +1zR*N$MmpvZ+͙K)y:3\d[jq=ꆧ%R8[Vq܏ݵ<,kmeY5<_oSus΅Q7`eyE,{ݞ_rRg08cf>|~< ׋RiD}x{w_><{ qXe*UaLҴ%iRXk^)k͇|x5L)Q}ej\ Ʌmե:D-+"c<?J^_.X,%~x/>#f:̮)^?Z\g5~AШ9XGyc<?{YnXioɝK*w, u <`MrtElt쭕A\AI1iHe߸s'{ZT0NxZ[kGRus^@=j`29g9LJI$mCfWx`No֗u_kǂ 'UR֟~tR^w?̧*u'XqU8.:{ϝCk?zޣZDQO$IRQO`E$i9fD_K= J*(Zz٣+2zˍ:G LD&Z:{gLDynNo= ڻ{qPg".KO)9XCMهu(ɱ*DQwA*X Óg{IWKxDQ[:]k":{qXn:{ZeL*%iތ(q||Sg"dVwR~KjhΞ7RJk~'ZQX6{Xk5SXcGTZgioɲK.:{X&S%BW2>1Xv4Mtw8Z+k{ӞR",7ZcLnMeq8TroxOhscyW~+zCQg+a>psqY'>]U\49l7Z l͠ _{AhxRb6)QgY4͒4d%II:N/ֹɿo{b7juF\ MoR"dtVnnv Y9d{ۡM7ϢDnQX(%w\:{qX&LO5Ξ\XVZ?}nYeXsb V{gnI8%c).'39;V+lZ]QJ`pw{QgF)VUa_{ïX\:{B3:Xep)~wx5*؆ll77ZWYV]\JͦDTLJ~E"MLkmr IDAT~Ǘ˾ W~? L 0:{\)$'t?|em^ټj%b6$^Wߺqk&7˲|sK,{ۿOkB=j,Ei-܉9X+'AED1A77rU:pVn1n׮lݸ}kv,!žز9'7hQJmov6{t:zVkixޭW]j7C72z֍S>i?h=Q+ck8/JFs{vvJJu߸ѷ|ȬCW+ 7q芄FUDDk%ɝVse{ֵ+[cm. 'LjV=+IʎsY?;|wVZzZ{w|׊٣&x"K.]j^{,ckDDkerZgM/]zNsc,sdT2\h1\~#RW_us{EJhOww\?6.zeN˵"Ƙnx|ܘ-Tӏ~j67٣+[`u6zs=|vx:ƴ/ݸ%߶>\p'cB9(^uO?~/vCR{w|K Ͻ;"u? Lܻ^C nY=Z{^هES]9 4\~sOXkܺrƕ>߷֪eWJ)kG.G}\. b,ؼh{OzWLg^ys[\N5Epahy7>x^GkUkw>8󓩳R dj:RgRg)iLn{ybcrƕ_]u[<9 CtƵ{v Z ϾzƳ:{, e6QgO &Yk4)[&7[ϿyU7b]c[T6f9>~$˗EkS>:{,r[iul 1h>y~,Zɍ~׿dQJ)}WJZyoo7^sc.YU~ww=LOl_Xa̮M)%jM(ke|ٳtڕ҇GǟyKTveϾZu[+7&LӞWf.HsX+g^qև'ImѠRJY;5ǿ]DW妔U%*ds\֬&IEtVeY6UR?z2Zk?Ѫ94 8DIfien ԝ?# F6AfFierGp-V_|'{)5Lkdٝ|&vτ qXyK.ժn܀}d8:SyNLWQJ?|d̒Y,iug]ny3+AB0h@0F%@$ S 3 H,-[PUTVVeͼF܈pmw+nWY  m9sHJu[x(f8t:á!"BrZ|%%C? u*,R.UʥjX)ME9O9hԷ*?8XJEoкlM}̺ra։k=F0JxRA_!jRD\K\ BYy"w߮yRޠڃ~ ^J7ޚh0tg\`V)׷۵J)2N}0·}vFEZxS=ϞYX0̚15 #ͼc„!; ݯ@ZoU[=TeM==梔B'4|qˉH_W_gop&ct0:5 #mZMՋY^}E(5[wm]t.hB)X@`,]NmlloԷfx{ `s!"v~ץy'g;7bg@Vn= D-WV6΢4Ϗσ L+F(JzɃZ9 EU\1:2 vy! Ѩo6W]b0 HR)/~ץbūN}h[0g^ + ì%[V: -J^ZVP9k]eE*eƷy\@urޒZ 2}2 DԾ6[2wwSe²%)EݓZTCD_3| >pm/77 +Wc6 + N3k*ן[~Xwr= 'k1fJGɌ?tOZn_!nFU_Eǧ'۵Z79nun\zXY@?_ݿspBKdc *% /_jK L0.۪y@FWb-]da=/ ϏϥTO#"~(jNɹBiYk/" C /i=l5&6go )p"RJU˥4Mc]K:pDß~To0o梧Rʶo`K4LDh˗↚IR>Zl~!BB/9,{3 ^jMTiDD_8CH?3U;ۉ'gB3juڝV{^p XQJ o<gq5\[ohcVs  ì쳇:8bYM ( ;"O _t]TΣj'?{℈L~)>EG}KyB vxƮAM9pd[LIdrGWf`׊}c+?#2+Bos ׻@D/_5U[OT7 %h' #׈NoG{M>2^@)u{pu!^bv?"UAn_<Qk @vݹڈD0}6>;V,BuNR+(I}Ģ`df 0kO>} VkEa za?݁pPDJ7~V2{XQ?,32V/aA+2н 'OhD̶Yafd vT,ٖeW* *CC Tw<z P  Y9B q;oS9܄V,Ru*gx|H~E_^?CVĩS"W\=`0X}py&q\yDap{B Wm 'k=w|Ϙ}೰lㄸVӕdì;$ӭO1roeT*mXZ\tTJc0 4Ibz||ҽ/^Wypwg{%;}8R<;>ЈōV#RQv*wGZTLDreEu`MWe 2 Q.~V*(`J)XEgAxr~Q,XbAeB;۵n\{pdvն䯚@~E#j|_+[YИDݒ$0FgEhgLDR;{WR:CDC V)nWJm*" Z.E!BTwvi8 zCwR .iNi.c> `R"aӇ onVc{g=W/=1oڿ-+kX0 f6gSOQ6oݝwn+u=ikV;GvXߪ "" |㓿S MZ핫Em GÑ'V$0:Ţ +EA--B: nۿ[\a+ぉ!>ܤUB۽laRJUJ'oݹ~0J)!rL]Y]Uxia+(R8U+mNi"΋JɮU vn(E+Q-͢}ٗ9ح"M$"4yt_L^a>{o?H{ 3+ s[+P\}?7_5E̵nC)2xm炎x!@>?mIE/BŶJ\i`0 Sۜr 1qj^r5o飐 ۩T;KVr^O"YR }|薠qQLK610kMżdj?ʕgZ׺DJ.zY¥ Ӈ+^5;"=bګW%aYeY \\L8F}? n#].KZVU)^h'}/!Pwݑl[w !z.(R&6dMRtӨזCVwO}&Sg8llw˕L*MӬVzX,ަR.\-ߩTA?W݁+"'wCx֝j"Ǎyڣ`I2tcbf.Y[K>Lͮ!>{yC\!I0sP;\ϥN_;a;NSnm `iJ}uumK`\V)TJEG^$^>wS*Ҋ"xϞ̚E &0+'wOza֗>f *ea$DTZ뵢{V%%L+; :\6D=R\~㢽fm۶m8EKc\?o|}|]yY:9 jAccd5xyr0 s$ahx*$WNN_w/jT++bZo :$0CG"J=\*R*)N0 VG>'V}̉H!5msy||uEp8 V ewREYA~u|a"nƺnca+ ^d'a y+94 'XE)543ZQ۵n gׇVQウ~Оn8ba2MqwC_.ʂBv:2RV|ٳ%L8!\a2C ì#S6\q۸rX..^nr^ΔMsDL,]iRŢwfAlooNB V`,XN߲(i2a({gZN{, ra$oppݛԋ"5Rg$.:T郻NrͦW"B\.+Ikv&U`Y`0H|JmՍNh D6wwaQDv BX0Y CwyagW|+{su/KJ?21Buqqw<=̕zBomm* ز-c9 ]CdTww2t7M䬕( ì'OzW]8yƕQXŞ8k"Y`0 oj@`Z>wB۶ mۦi!̧zV{{{s!"_oXg/J[wQ,6XgNڣ`+ d>|FW dЉ{OD}xֺTh?'ѨM,@ b;Н@('~5N˳zmP(,K;r^ND%Ͽ~(fk"wJe+@Dp=&\af a~Qܑ "A0uD@voЊG#"Rv F8#K)]{|~'eKѨjRJQ+.X' IDATp%EX^n.k!N{La0L!g:Ϭϋg?*UfVd~`Z0hOͼ>m,)Rji7c1V"q$ ES,xrG8&" {K(=0kk>313JgOD>;E뢟^"J,jxwI-*{O,˲6UÅ,@D;bɶP,h'JַlH0]e¤IVn 07Aoâ\g?Hg"uQNh0vh4BA@*g40EzѢ(%^0#0P,*lk JϟcEgug/;@qڼH{La0L~X}ÌggC" Rw"naӁ7-+ڷ2K "){IFNU/BTy.i'lMs3F1tFR۵jXJE`?L),W:쳇[ W"g,2 };QB6M!#@EkӥXTheZ&KP,kmчG@W+XBM/NA(g(PJ9hdCC!=&d0Bd$ &gU쫋짾 3r(F" oLZ&*, X,.XPnDgD/^O ۮUmk8(Znڣ`KfӿD>+gJDT+G ̘#`oJ ׈;Q6>79 vi/{3Z#t:YoKm\}a0~hJ DR^=QZ~Z;|{>{d'"e`1 &C`h9 Wjk#KfIzjF|o"-?^XU7DNn6m#'@l]tY0raֆ#>' Z#.#غ?/onYh":ua`0DR!J?1 aR*ޕ}GJDhիf}(Rѝè(E;R9us"T[쐢z9<<<>>hql"@Ft]L#bJ Üfv[!rH#T,;&0vJ"m{|_9(6Q<,ؠfADł]*DE\'rG^] "MbBx^pڋa^Zu:R+ZgF#)Ĭ.n_@AE X0ɓ?ì;?QD8N i"rfR"X,~ԉ,AvppzAQ.J%&D4 czL_Bal.`b)YJIѯT0LfuâRAkUB`_grak\}BS$ `GzHtRh7(Ou~~}KD{{P*pr oa贱_D\V zJE0Q*XA(`$ƈ鎁'/S1 &3+11gѵ~WAnLF}\RP(Dhoo/PWV/eٶU@ EQ#n4LH5cEԨ2XS Nڣ`r.{ɥB>ؽ+DT rv Dhg9ho7RkȫZ (J, jlR%,Jm=܋"bZ) dd'w~aօq܆Vg%2+@Jb>8 "U]FNܹsVTN@˕jƀXX.Ji1F8ːwBK[շ(B(&nbrI+ d<ݿq}jDXĵ2AѸ~Rсl/RZuttZ%>ffP.W]@ةڀC2RR.~u41]ř*(q8R?Rw-2 L}gu|Pn!W40 "!D;ޞ8XBK,:oAvrQT !`V!Dg4*I$a2 GW fIg\ ΅Q|8Q0M3MI{{{TV= C5n%Lշ* &I8a>{g]hgeôgiDS;f9\ d0~3MT(L*!DBDJ!​1ɒg?0k!n}5>RY~R\dR g J$D)=t6ng9"2-R.@n "tt D3 E8 GIȕ+ֈ0tR¸XǕ zu)*Ǯgz9 bJ! 2,W.'8{vȎ೟c\tF^j6n?{y"CQm x~NnJRI ^_wݭ4M/BSm*/_RόAġnU97I+ d@AK3$ߊ royvtE)j,Zy5 ao(O An}l$OΨE)JOb+|suGV!2!se|7bf{q=9Doo_S +~3 7s]iUDHo5qu "]O=LmKӗRFA =7Ⱥ͋[$ BB(|?H{L^`0L渾vJ`AeWܑTmoh u 0=2 ۝ž>xD]匉Jd `ab .`a5ϧgr\O 2 gaeV.WP)W!"gF8pĚTZ"ƐZ@H@E&!20cXr ku""2 cj>p8TJM$ TQ mcg DT.ڊ(]&!X0 -& 2%'pD47lnV?P&R!D+2 ckp2,WaNJ|`s]K}IDkU=ϻ) жLZPL}/figW^T.K1Wc 0bb=VR*JUp=2M rnY0 R_'ҵs lkQYշkˌqGa3-ᓌa q}Њ^^i"23䳟7G/BY0@ك!\\/UꦨwˏyuG0Av+4`$d dCWgA2Q i& `̒lWBB_J˳M"JbU0 c۞XRe A00qiRaD [4k8t. g?MF'0*e;1P`_U`!v4o^IRrxhCeFe4Q.|( GI:0a2}'`P [+tS.Q8 D(Fd[#qC~IoJhg,Ӡ fda0LgkQud8kAў]m('$*;q1ȶG5_1볏 0!@[cra>{[e@OdV{9`0"a`V0tG#?@m۵rabnO֎8! 40LӜs2,W Wi\,WW"2TFM"vڶ%igRde[u;wtZH)θ,Z$y0~ARRJu: SP%ᘦYIgo a TRQ`@S+<]bww73kzg'4S 2+ ,Wfa뉑s_s볏]!j%@Z ( [[[zbVΡN5k^r;gJwIH7]YmPJwԎ.WT0ZH$+@_N^re)F^mgx#\&ػ0L&KΟ99 "@'c0ޮT*fqqW}>彽=GmҬ>thG9c0 d>{=ˡ\JR>Z]d/C4ͣ#p' 0 MӴ,TZlc`}}!3#L.`0L }+z26}" #L,ʒB32S^M=4f5lÐ0i0 MWb; +kd<_Jc`@Jk{s( q>?,W>XX $㳯(€D2DN @Lھ,V`0LJW_ahEkﳏL%L=YCt5- Y I2'0sq7ɜ~xg¶q'iV(e)3Ḽ>3&Q|v@NTP_`CסkX6LiC&]X0 27 wS5q%99XG@?`bײ^bI+ Nly4 ë\ bg?t<+:BDT\ge3@ }$BdqQ0 Y' ؼyR}QK)i~&WT4σ6u`4zzlP/E+#;˔a ì'T4ڳqr^0I DT)=P*!J?ire]"wL3$Nd ì|rPDCo?| ϴu gjte6R/C JL^`0kmۛ'Wz6R: 1cb> `f`ҩrر{X{\wcMG3 c`6+ fIrE ɾA9v\gddgKOxPJ5ޘ"鉘I8dubzL5c* JgB F1 19x&H,Œ˂ű笁aR?TC>&\a&5Cr?L}`!]Q|6n }Csr,c\YR`ŴE0 Ly0@o['vq "!n㬣Phti[ T(DWX{G@GgR ì0aegӕ0 a+T)/f\=C@9Rv ~fs;E-_/ ai`]3ra4QJM\s(Wk ;]U=$HaxF]ȕX|נʿǎcRf 0emFdY]3ra$+cs&*U?{#CFcAM2RW~Kˮ7+Qv6DȔLb.90 }KJ鳟 {nF/`8!9R6lID6vptI>fIB& JfaHD1e hI`e|[`Shj|\oC,V|{ߘL7v2VL2y0더RX;тA\v>%ᘲ˂U* 7!u2)=?/` C<W6g?;R~P&`SO 0H̸'}wAΝՐIܭe2L8==mZRJ0B]JaF'''zӕFWbg?pbOQDʒeDQOX_*)}, "7g?'踣ԵGWaR$ C-Qna ó77yRRʘDZ(%Jg&OS<'u@nt?M}7bu|~$zVd+ ]OV4쳇KspLxL?jP|[W J9n\$|F#Y&aZ*8'rra։z0#\Ắ&4'1%| @Ňιy)W Snob%JW O7Dt@I:SJ ,W0yC)5 }~#bۍiHCat%g?eg78. 21Ҟ!`r| R"6&2/( g$@3D >{bqWOE T_{ݱ>Art,Fb$h\IaL̸{#BoDl\\e%M g":\}J!UϾbOFq^>T/|0|0rOg_,؆Wfg W$ٜI `e+쳇Rl>DU]az)!>{eTӭ%+G77'uo|'ɱݖ )0vbzߍe(i|$J˂1ra֛?}p9$WˆעC^ci(>vr'?Clx'wpk'.JR>X26)lon IDATl:Uӄ*2lrq3̦2R@zF+ZŤӆ쫷]"z1XoVTCHT%˕0^w^e~R? \P/˄|XʂZ ĴAXK-W3[* 燉뀱ŒAvw +U9$Kf jYANDmǝV1|>^ck+T/fe'پ`uxosznx֯..H$Z"ژL ^?+>{& X0fv?ۈH)`Z&6~0>!WSTq)W+b~ t rE~gBz!PJ<]\'`W bk)TJmzCD"`Ddfmܱ!90jR*1򲸟=aH\y6VQL`j3M,P:!:0Mh=A``ZwsT">XDl+a%[,)l;r7K`k\Ţ*jVK!41,<FII)eP ( ga!-a|B-3,!ٹs|+! ~yL]O;OvLd)j5Q0c.ranZJ)iBDz !>{b Wn]LGSc~LY+nw"?[gxk[.lWh\_.:}qT-+Ln0y^jn8U^Jl0쳇KJl>؍+ 53 P|NkLOd0ӷ ۯ'm'?> RG| / 6 KݵzTL;%#r3L>ыs h4F# C)eiǝ}p)Wbğ1BTY^p\=_BȊ 9+a+}w{ +SXU "#yRշ*i#[d ciBR Tt@JIg_YqEc(UЙf >fj !tфOc ު Lgq%֤ʄ9Wl^2r$E[gR 09}z5 IWU59H'0AV6[BM}\e`S!:zD$v$gϤC& >,&b:+, 6Qsj??@{>{ 꾖}oJ0 #" 8k] hPvwbv:1 n0kZ?{I7#|!boU+[O_aCw0t:A^pZ"*g`t>7?UN0iJ!ċ>Ad<>{c0RlA0{áHaJv+#0Wa0LBbgZt5ekU%L0s,0~ UѨW?#YJa(V.WWc) wa86awPhZ@&SX0d!x~Ï1VlV^}ϼIA& S@ga{NVZxﻫ%9q,"W6gp89gfC)E:V`0 sSxf쳇$,8vnR/y%ʟ O-4(E->ŭ +2 wGj)ٱ4n"P њ-~zq' F #t7AI,DN@KEJc>IT6rS'{!p87#HwW;0kQ[?s oR/Cw+ ,o  (H? ߿{G? {Ebf̹=5> ܜ &^jf'kX02\ϐIk$uDqbi r>f $(HP`@$K*IbZ49@ϾKt%Qʅ`j&g Hwvk2q10 p?Xٯ/_{=D;)LS~2 jۭ = VWB(SPPe_$k˫rzz{)}+$DX+:1 EXRAL0&eqg&k0 FQ;ǖŲ_^+`80Nl4JIv{7 =g A^K{ La0 iV+_Ůlc bgIEHUh (?#`w2X^ehK+ۘч˕:u􀍙QBfrhk|3 *:vQ]V1Γ8anj_soaN;tj THMAc "PI4( Ja Vm\ O}\D\kٯMvWd)s(+1LR}BzJdqtEً3Ρ)Jh|b҇ fBfytt6ZZ}reqqqq>GWIb*h =H to?Eq'{]& W"Rk HbRC+L6`0kD qrp8_ɞ׈q! o=Y^Dc ckD=;n=^ u=JNs}RRp8DR)ƞDOS\u]WϷL4 C;e!DBE2 ˏSwX ߫>Sva]wo(AĜI=+7BD "_VEtǭ!raNin @[̻ݮ*JbqikaDc,(A'e˲|E ];ŝvgVv ʨᏞv0 vl@y b^2452"bo\J#%ý0a0EzxW(byrWnca0jYR,jA   IR[;h62dF7-RbgAR>X+T""R^m RכAX0gl۳wl6ݻ.Zs v!b\"MXuS{]kwّ+SooǕ?Vv?Xw%!} Xv "F 5hdTtNL0,;??~蔰^wAv!ʹ\@KRTvvvzZ-c3V/9vvs `9FA%{7Ԗi$_uM|1Xn`Zٵ@/xQ {(kZE)U[fptarh4tt} YT0)ѷ<\q]Gq& Bض],hjRtŌ~nw`0m+3Xͅ՗+;+l7D>P X s bJܘyz!S/[Rv\Tr˿FHoS=\0(~"'W;k]l_zC+La0yBJ\."}ߟ6W`¯QJ\IP/BR+"޳Cù%g#1(-+zFc"᳗4MET-v0d*&#hCڣxM^6{0ь9`!M$mst%]}e\@unXhF,^/bXkŬ\"7x;'ُcٞ/ZkOcrάʪSCC7/WH}[ B $$^AH+/@bB>}T:YY9Gd{Z?Vi;"{s2 {}Z: d$%шvrʩ/ Tg_gfq]:{spI!}Fwm{~$ q`!\mX?3l#zX^ODZsbhR q ΦR*8==-w~Z%0fzrqXbf˶ӳc_rf'> Y6){>ud2~vޮB"~n*L C+^+9S9ӛzax=1,[ 3^L/Xq1XFyܩ$e>oW,??1ϧsϾ8-(KiRFRUUK">!WM=ɴNzooox#l]BӋeY]`fJz\?Ka\]Xzշ_?p_¢XO|&I_q( 6;~wxzxrqx8cA0Hpf3 <[ZqxxxM"U:R4f@RJ/ 0o/4:ORӳ=qHHθ2affqӢ/Y1YQ]q%4ƊT K&ZOOO~4#'n>g  )e/'`nn=T*D)ǧ/m\WFWlw>v I)?f ͧI73{5 L”^t稳_X酈ӋŤw>_^<^ސZTGD$Ԛ?ۙybi[d⮯a| e!8"퇎v/fұ;I7`+6LNNNo3ÌKTjZf%"^WLսl.}`t}?Jo -g+ \^8w3jM }мߤ?|n?u i˵rKH%+EQHv7a>LͽfBu/RwԯjʵwR` |AK`TzyF"DSXgtB)D~ለL+9,{#Lq ?p^G=́RJ_%%(QTtV7#{Chw_~xs50^{j?3Z8muYEI᨞ 1SJBy##44F@=GPh#{i`t>⧵v=RJX2ǧ/^]&ݜ TaFwn}* K)ifJV!"zuRnЇ <wF!DǍF# 3`n\tO1(77W{=R %ŋW_M-fnQI)nd]()мqhfKwn&Hs$<<lK~y):{3)%S03 y2^ѧk畩Opt%n+:9k>}֯xCs'+0M2dam5{ZN GBKv~?J93gf&W:NJX/|x,HN'V@A%zԜ\$ ?4J\pN˱3>s Ov0dVR+XT'g;]?*$<:~!\ @\tLyt+0RJ۶ǟk. }W?ytsf%͆anQ7~qP%;U8XS IDAT1PN 9}W`:L= W )뚚1_)/v޷Y!NN\[%ݢi2#s }f-|䬭P9jRj{c5Lq K3k$@B3?<Funtahv˙i 贱\'JeHeI,ow+I7`\+K03llu0gBu؛jM%^5%?N\{q2r7Dg( 8>k:I8Sf'I dC?R ?˲lp'3i͛k?jwL(H)Tvwyuz֖1?7vNXK_1=KYդy?,$0 RunlX*eRYo. W(zw/oi͙^kRB۲z{Zo$C\ȶ_QgR(ƟD| /3mUkT';onҍF*Zock(zt[>q+Bb8N,eݿEY"fV˲ɋ\G7u(\ hoˇ:k^̠#|M5@ L!U?f4cϵnX{~('XA$(zxC˕jR*Ͼ;7BH)Dy24oޫp R quBq٣R,X{Hʴ̴ )TNzڵJV.َJy \0O?GR*iUX0R)EOY+dL07/?@ v=nI/JaWz-$(p|QJP)y$PRJYlzqkIhR:ifFw{A\YgHBjމZ铳Fv7cB AD?luj+RADZ],EygYՑR)4BK٫s 1@a3CjN33tcc%f,[:TtR-a4<T !_g_V*a~#L/ngc/0&l۞żV*z(#!%" Rֳon\L97+Y Qgyni۶mxWN/9cRJIKY$I{[ONZ_XLĚ-%-Ku0Ua)ūaM,cf eǶ4㘉7W{hɣC.`b+mҕI)tӋ², VLkY&FfxV\NM&aЛ^UZsCuӅKv$XT).W'RKkZK{7 }lrټ1r}ڢQ~;,ICRe~f^q -%-CiΚZcP)zwoo ((Jfs6.1c;+L@I) |u?@ dI.E溮8&Є8{UKZތjTҝjnB5>a(&*zޭ7{G2EI?/&OˢBx8NӉ -&-Wח+f,#D0fZ/Uwnmyc~hV5zOeiKEʛ#KJVI-%dd~.J/fӡ%[R)+lB}Z.ʥFr@j#ؒrg])U*`t~P3jpkkޑR*n d Lfξb\@z!";NvH/YT*fݰ""ҥqql!̐NX_q^b1@}y͑)=$E*Yo|ޭـ%2\gN?l5bj*LoDC ifٶR9:mA2E`5=gk}ymew* v.Z1{R񙤼"go=}3@ dI!VziۭV~oC ]BJqZnuzh"Ė+c&&֚-)V*˅baZwnvTt:t)f9ZNXI)OBݿts W`q+IJz!"S02r ff&Mڷ7Wծ7vGЈWFbLj*EV)-/,bSt:#_KY-U!$E3Y`ͤG\Iҋ&aL/4˜άpB ARV)Q\ov4 žR,ʅj854۶-2k[dkLx}|hʉ@pt"ts Wb`1 O-8q Mz'f.Ŭ\]/W(:kuϚV'b:X3 *ZP)m48CDA |5+7Vn +qP(x֕M=R eRr,iE^mDBPRXX7zIW6LfJՒW"Rz7k.eu]Y_qd\9o^_LVGn%FXH!6Wn E\)(Me,le VO243KZm_^R.Un kxחYDn=6Y$z{m۶BE$vy\7ODT.zRAh]vFZRQRٶEQfVApL~A׏rX/ߪj) ðd?:H/0^ 3e>,*aי$Ϥ!E[$HDqqwAi&ADzc/sBVJ:yvq<$GW8|cvJ>WKcyLBbaaQ~Q!3?63+ٶ,DZmK:e[(϶R9DQL4̖Ԥh Z#"̂l;kb|7VJŤ'Fg?:{LLC[^faWgbxu-DAf>ҸG1"RR\&4q(cE8Қ5i5)f<8WJRR%m*ъ9:D48dZ+Jٜ(!l|FqvXԓ_^}~%H %%(2s|7.WH/Dd.|MJ/Ȗ}:]\+3N>_f|ٻ"rJ/qZf9!\(/.qDZI/Zq"&X"Wp,MWJhX4͍7+B|lGl K펳B !86Q*نNzn, @z!"So^_g|gT_f?  >mj^?ĦګwLyL,Rh&(+&%RV5B.`V͹SfH/dlFVJfF]r0ϳy]R|'jXZ=;;![ݿ{1)?>}7\7 @$r2+}^/?7^ %&`Ш$H%B4[9"c?Rh], ss+436Y]Y!\EŤŠp2_rh?Z5۶sc&(|ã+Y>ff˲\u L%9N~I,fsR)˻'L,RFۇn%!oT&~eT 5aM&8ǩT*Vk]Y'gdN_HJyzz݃7n \H1|Eʬ]^|7/BC CzI]4rڶBtL,̤keM|pab$<>i޽g;L eϵXlΐ^R >3J.3OXVeޔB+|L,u䝛I7q.atlL=R >^+-i:f]K!E}nfMXnm; 8E=yۇEqU+0 o]Ƕ /5\62%H/i#3ìwBFqØYJqw{(ֹL,R?٬0)no;zt3M |lG_NNgY8 !D7Ρp:^0èl33 a+Pp^ %ͮ3c>F;|EiI,w^?2D Y>~ã6>30JʀKK7BD-ulu^kBCYeYՙiF$5biil:"wkۜ&)v%gDL>n&6W>Ӈ'=gbu H/0l:n^xuKs$Rg?W2~bJ7^;-r@J~`vJt QRXr5eZkSm:tBD!\1):a|GK4eɻ7^?޴7֏'T4|]0ߗf1B8H/ ,+M%8Y7OQ :JIX-%oo~(egH)8ӝ=o?8'K#{H/i6aft+-(ה:RJR'cqmu{s)峎EFaV[t) ǕlOץ%$H/ NzW'[ LEzfSJjfsn-p,ebg/9~.O/Z $`vR(877W<+lO?K$I>1r2hN/n~7t&E1ӋzUroxȞ4`Xki.< <;IFC\Oq&cR@K^z˂-~WBbh&X b8e̤YoEW-z6-&=;8N-}D1`LAO\!bhb`RmbQջ۲WWj.OmʀbhJY~fZo-<;Ŷ/ $ q>>f 8JsʀbhDn/{Nz#,?0k(\ОT*WH,V%%W@?I7'3(q`L9 Vz=1'1+)no=b|LC'<8:]UnB|PT*۞ֺt _1W`e6p1ZDsN,&,*_ܽ1DžyUҭȆ. ^8YgvUV&kKOh~ŬN+ Ƈ) ÑkPVUFX6|8 cCbaX덕fXp+ J1-^H.Sg?Z aB  Z}{lkLVX1G@\XP&E̺b۶.a7dE;Le:~HXy,r Y%?,%l:?.((jZH/-IX6V^ r 3DZ:{A019IҭDȨFWau+ R\;a 3Q|Ҵf & BzÍϪV'''Fff[;+i*(zpg{}Y%pXPqlE-iot~+%\^^(hf%͍{'${b(wkez9`1sXP M/DCzYhhE2)Dű[[oOc,уJj:)Rʶm:O/EQ& 䄔@z\TgWgV7>998f[KO6QjY%Kp_9@J),<{Az0FI)<Xk:L}xT*\ɐ `P/SeYH/=qʤLb9==#LT+oRc|4{"J< Ѓ:,ˬn<fSս$Қw\w -É&b۶mۮ:ւWX\*x[rTec,Z3}}癅piYҋֺv::FE#oGV˲cU0\Z޸(h?T2LĚE]PSm+0CH/3q٘O%׾|v !V.m,*n3 aH/i5sF?o̴a H d uJh4I,lYVVC )+WH/cakOWP`fyyb1Yeyyyn D$0sEq?fgd b̭V 8U dCz!"eY"/ ,&JRhޓl `ֆ㊁a&0qPg0{+"prQ !Lbl`~ F3\9,kH/B۶HaB @\ Wf 42쪔Bz:fx+0CH/9pQ=cWѿM9]aHGzQJbA#Kڢ }.ضmAn\Tg9຤q' `L~zߜ.0 Fџ^8I.G&fe+' !) "]86oWK/&kLmEu]/hq&qj& d%u]f_bY3^UH/ ct`W2bYO/B˲^ .O1K+y_@uyzO[O )j1us# .J/BfX0 6 |HȿcD_p :{ 40KGS01uMI/fBhK!c.,+mG@^!'^(eBDJB8H/p(ۘ 0O82c3?酙vBzW U /j ҋm1 C^86&S`.I/檹{1/i6̟ٶh{+s `bJ#p]whF]0dfSҗ4n1ŶmSb_pM} W&VVn@&330B)0A2v:vm6,˲,:{4@\91||;^zImK4, 0 |R(K p@RJ۶Muo饗nb@Ð^r:{37BAzm;V• p@# .NJ sەRi{ dO.KŶmTG$nd\A=Gz1KK:\ hzODzI3qܖfM@^(}&su#ĭA AcyHAXȦbb@Ea"\%u~WR$ānD@BWMJiE) r ;36 rn%$X<+BDe!\Rg?GL}?!&r2 ~!Y Y"2c*,m7KjF9QW'ο(&""Z[X` L/ֺ"e3/~"8DnS韣(IѮU{d-%DX+0}&Qo?(zc/iN/ߟ n.t'.X@ yܙv8(eAVHVB∸KUquDD$muts`!@ҙ^t&-A,A+選iHzYR !d| d," ʴ fCȳ 8` 8(2FQ&`2@J RJ)ߙ[8Na`  !K _a>H)H)H)H)H)H)H)H)H)H)H)H)H)H)ۯcAQL 0+SL 0+rM1IDATSL 0+SL 0+SL 0+SL 0+SL 0+SL 0+SL 0+SL 0+SL 0+SL 0+SL 0+SL 0+SL 0+SL 0+SLIe1IENDB`PK-B\T+pix/no-posts.svgnu[no dataPK-B\Ў}++pix/no-search-results.svgnu[file searchingPK-B\]pix/t/unsubscribed.svgnu[ PK-B\$pix/t/selected.svgnu[ PK-B\ ZZpix/t/star.svgnu[ PK-B\oKKpix/t/subscribed.svgnu[ PK-B\qjSSpix/show-grader-panel-rtl.pngnu[PNG  IHDRw=sRGBDeXIfMM*i5uIDATH +DQǟILLʆ QlXl,%[ ;(Ɍe|[;==̩ϼ{={}zOEQ6:!-iZU! 8JbnR /''Fkw ~XZ, 'IKvOľ?/0™Q`eCcT_C*+tĵϤ5~7Lvn|;T@^":~1Ȋ`tec:@\;V>t)J2P,/\]%h3D/w"~i5P}q>uX?9ۃY&;ު'p[g1䤙+`Em,؆b(O>5B6Lpb(Ԙ_`c!o6K/CWu^K]qR߃ZS IHdfݥYnG1L=IENDB`PK-B\Keepix/no-posts.pngnu[PNG  IHDR$# pHYs   IDATxُd[v#+kS794lE !-Ƞ A@zl?0G?2$6<D@84ɦlPu+3#L{/?ʸQ'3"3Dĉ~]y*+cgVT^kvzNa A0A6 ` la A0A6 ` la A0A6 ` la A0A6 ` la A0A6 ` la A0A6 ` la A0A6 ` la A0A6 ` la A0A6 ` la A0A6 `V~kT?5˟F|q6 l!hInnm A0A6 O,UC.TkajީH$zX n|OklBtmD.lϽը!R_{Y)`32ry'TJp0  ff%CETՉ`=]+apD.4x*}g7^x=XN6#r\|hQooKpA0A6 ` [8*^M`~.}͸Ե^:l6;U fq8V%FXr^Uӆ Ή.X\G,@+[!l8r^^M]s^H݄4I~ Bp&v}=9U|;R|77h:䓢n`6/^M]8zRG7ϓok ~*ثm߼&OTDUo,}rW<؟,xsL3vKU(/4"lؓT4&_Ue5-6WonUECw-7׳ټu!J"|6A aLgvټd lмofQto5Qm{pbn/L̼SS bނW1=|믮~fxby{%)3H(8]9(mCm뫺i6jȪb7yܙYY٬,|ˏ>Ћq lļ:lY@e&f>KIjX$NSLDD|:j"Wiy$?* "qW*q}=u%BIw(NonfVjч k۶l6vn{maeP&jIG<ų<ކ9pJi$IX8=QeY.$QUH%ݻζm>6yv1yEqѫ__fs?z0V :  *afEQu~/^o83GmOU˲|n '`Ǯo"^<Ճ~Ͽڸf/'+ꎍG=m%܆׌G:]^W*e"rx*sOXL71(w3C+Z%ذKa-`[=8*~``PCm,YQ"rB,'E>Q31{@}וa+=\>M|*QUD *RėRsC/ +\qn1$PQLuî, ݻ89JU,IMĬ7lD̤\䓋U5EK#N89!oET46}_p7Qu.p5m V]S10QE!K8~wGK]:R}۪he 53Q|ʉ.Gٮz8L\򉈓oѥQ>`v&# ^}XTFYכv64=uN jQ:QGRX9VN28I'^Ӷ3`fbQ_\~0^N@x_K{-kH4yoIs'`gTSvOUgeb!6`r8]GmOEHm?c.Z>jlrQ\\S 6莶<{1_Y,i Siߊ{fD$$ 8- Ǯn7l- uӤ^6Nt63 mΖfsM):#*#w,}nųBSus$bSD߼y4|(YھHeEQyeY)y{սǞ>V\\nJ]˗/ {o_o;qi$~^ngCKI8Ҽ©^̲4+7yմ534ٳ u]Uq"fy<ϓ$aS8.(,˲,[O] ݗ̊糛Čs{J,qx".LeߑB`v,M\U|{O}f9jۈd2Y͵⨥݀)z$+m4MpQ+1`Ef,~NJ181 ALD郲j1|%"!Ps]-};r"6 lرU&lKq,j?z')gUT"`tYpO÷A{f&,IGN{S PUU&Q8rú(rqj>_\=SLgEv~v:UͷrI*z?4rfq6UյmlYOu⨭lDD$I=hJ,Kf63W? ޽8?'"lW[n\YVydiqŷ/m}4uݘaD$iV߆ޭ ꪘ,틧Gm ~-3{lUL{~,*ͲG<E rJ)~QW^p"1LYVQZl6PVMY5&_Qnn4ͦkwGyO)ڒAϋuxBX|U}WoZ7̬(&W776͹P˾u`(/_O]hllXYy(gfIڵͼEw*Rݰefy$OK%"//~[f|ġ9u6 %/_}UT]мsQ$Mfd+Q틮c⨭v @nifMyUX }tqqQeYUQ"0K{vycqT>GmIUg|ck]^;]^>Oi^e#B0>?oŦqñD<ϦqTQ[RS8h[}H6ȹ뫷yNa>>QS}l{>v,M yuDUqԶT6,!]\\̦ų{+?<aBӆ٥M˯O^Y$˲O>d2afjx Rgfyb04zfnO( U0ssXFmVMS׾i[\wtS!=ef9‡q::3hᜫ:Ƌ1Bi릩VvB50ۻbV@@с?T7Qr>:`t(9vͩ$w8q0dfyQLQ9 G`m,a_8iHfbT ^cl ~,MgՆ|^ vOʷ^O}sH R_\\ogCD$lxB u-f|Mt0n0~/~NH4V17WeR+q[iN:tE>NMϳĴm7MJ*3s.\"uCΩJ-r>}|΁T5ԮiP8?k߽4Il(Q 1&Ei(F6D$I ͸ʼn&yNgUao}97Z5a׶u( 7NEžY7 AqHeY>+y(C&Tu^E}VmkTu)k9FQ,"ι|^ŎF}~vkoSAq,uw3K 5̮ՋLof38m"eY6CU9QQ]Q,mEѸ*,N(p&jS73]S9՛y}jOgO>̺ćA=u˟f6Ic~Ϣ8i۲n(|>١feYjU=JDUeE.M :-pU_}U$]SUz`蹪l|=|yjZYG Q1Em۴m0(fyZ<ؐT"O/yʲ*i՛ד{mm1?a'q$M[xo9C/DE2z왅`-:Tu^73[RUm4~YX>PXUA60_^Dy#41gWD$жm|QәEqj [${T,(ڪTjBUֿM%{Y՛vчd,*`Aۿ?o_䧿AHUUtwu\ԹBY0ַmSonl:-f^!szs) NUE6LWyY˶!ӿlv2k6í_7zVYTUT$`?x'Iv?Y5Su"RFYn(E}U"+3 ![qNonzJaa2F/?kz"(oU/(9g<6M6|6BwO\/_憘=bz3n&*)>mzXDE/~w'.ot760|aN=# czG67`f|nL5N̋Ϋzt"/_MױK߈x6]|WϺ+yxiXGGE&E~U+p(;5ؖP m6Mq\UUc۷0YeұQ$EmN~}{ㄙ޺FDT 8oUVڂ=Y^- {0ywKݼ=C&%3{TDU!$a6 mxGQ1mcm}k!@ei!H7P؝t: !|k0> ($Ibf.rg8*nUDj f`'C`zL#m{춙y1f} 8]v8gYۆ8~ʖ,4=QdnfYVEQYMS9apL!m[U|>/i$I?8l*.idYd2~ѥŻ9e2$N+< a+3+˲,|>uIQ!6(2ڶydqs.dRE粔C8%pn.r6e v"33ui$Nжc<({>;ޢ8j>WU[l_O^i4MU&1}Gm0%'z U%"S4`,'GueQ]{"],v9, 6Gꊣѽ[=3j~,4u,M+(ݻ-< IDAT,׻vQ]ܻ۔b."bL<(0vDө֟`v"qGQivgm6Ms}}-ܱt:owv3y9ac/wC/ aӢ8۾۽ y`q޽:esC/c[<x(|RUuSus6U(mUO hH =G=H$mۖ篈؞ʸp )6pv_u?U:4B]o4MS56p^KӊmCEۦb*6pQUUe<{|L,—q4M]΋gL,]`,8][у/b8*ص#Z/*oX[Geٶkqtaf](*b2j]baoEsqJX/,=HEEQE ^LHs=rj*=h?էg,ne0^#*,o_84]. u<2<hl'ZZ 0 UYsu]{8z) 8U GŢ8j}qMv$Mӈo՝?ҕvyCz69EÙ#l`(GUػ8`qԃtf&EǶwT m۶uh[y7wu`r՞0SQ;]>BxƘч/`ؙc.Z~QiYF+KQbVmt!"fh!gкv~T߅ƃ@xGc8j}n̺ t(=H҅,e>+</x4/fUU9FZ8!k({,RD$d҅>3B1@*욙yx1xiڶMd,˺-",5ڶn(y w-lD9pq #<aQEוTPQyͷoX:w86\l.4Kqy~S 6EQ *iG=He(͋kQ6Q[Gu'}6e@1pT{FvCw1G=uj7"#?Q 5D 2*D#is.^7͂m˨vTefݗ(xhG!lwRtCOtޅ`mD.(HK8x<)w6+:Wֻ|&⯿G1 lSunQ|>Vm]SNA4``{ H<a]?^t.//={\m"{ؽws| Q6'ڗWLR|.9ڶ/z]9'ɲk'X#eY^^^r ]53 ?4T8!lI_)*˒-x`P`y[BV7͇0Ck$X-T*~PtR 4ֆ3GS@gN"pbl*9 OUEqI]F\OgfPAZ!l੊^ٵ^Y\z!>w]61!lੲ,ݯp-weT]p^CC Ou{WAlٌY!l`zm8k| v6a;л .R6a;y®Goa;Qm !m8 FQWJ6"aq״q6a{ ln6aiʁTUU-u63ycelnvGܹc]a l`gj9烰@hQVC"l`g8\Imz8y Ro%S-06Ky @*A. 3[G6KEQ[ UUV[6cIl|B۶ln6vGﭶx60$Iy8% GkEa{6s˛8% IT+W8% IQ3w6 p؟$Iy8 OoFEi8 OK`P ](ZB8i l`$X#^uo+9ƅꭤZǠm46W6 @^E~q}8m'L&=+mUU6ƎRMiFmӶ!"L;wz=cGl\G6oEQ[譶X6pin$fs`8kEx0Q r%sa~or #leYBXrmY{\#iD+8Z (/~K1#lHH%"т cFG~cAJdiڶ%o 8G?e;M%f|ɋw*93[\tUU!poY~/#u߬>,-@$W†[sâjLP}8(V22qo?pü@ngc&=G?>/2ȭ_8DgK;RyoWժG'o|bY{i3ʎsnbjacO~z{{2T${{[ܐwT{\Bؓ?/;7{XX6acO[D&phieY6 acO"d a"цbeYaJبzV{a2 bޓf!l$/Z7Ia=#;m#"m9ƞkهsJ*/RV5 ɤwgce86 acNSkjm}eT+="&>7ɿ/?L.p\܇H7 PFQ魤9GH2Gacr FmDU71V.88F&m6ʲЃXma#mF&˲E3㯇tw]XElaflna;m#q6靶!﷉Ӷpp OW8^e+;mz8Fwsc=W},zG-60JRHpX RTfEM8,FIUV.8ƪG\?5ƪj} C!l`XF60VY-t1]!l`zm0367|bE+=ma## C!l`z{lL*N8Fl|rPզilnaֻ+T=. " ]OťG N]aVE3h&a 7U] +R&oawW%h=. ]=+W3FmaeA3[u]C)mc#O ׊Na%M9+uBҰ6pvzFw UUq!2Sbk"`8;=Q-Iơ5p.sCٹmq"lH%k6QF|8a{%VU_9s`0d1`D"""QD %/yC%RBHAH$\gƖŀb=3{TCԩ>rvuuw}>Gk^gkOw{~Ffz.[a{6Jpb8Ίfj7m<a!j 3 $l0D9ϡٶa86xܼM_ZV67G`M(rw]%6}=h!lܛ@:Ί@M&fH)jj$H`ZF6O% |>o^ypõ@*pMֶڕr)l܃pFYUGm( 6JTKO{9Veˁ mF I`#òy0׬@,"4^a]3<1Tv6xܼXZV^'D`n# ஄ 6=!+aN n_xus= d2i#^ w"lJڰ9w"l@NͰQ;w;G\=a|>o^#ngN h?6z1˲r)lܞ[,v(( [6 m] 6 =ac\,v6nI؀H>J8B9/<8ak<7/6Bm}=՛Fu6lW&lfYk~3{p,ZF!˲j;(~+0^ L؀wM&fhVf+9>*VWBL1ĔR6ŏHΎ`ya7u ˲m%վm8Z͟aOlb !RY//?^~5rnlZmTGmmO,P}˲0IJ\,o'a7[I+|g$d{a6}66 )>{1M[yuE1/,\^pW"7uF_>Ȋ ao_YL͍^w\&3f b@ xJkh # еrS1w[6)RFеQH) ݯ6)YtjBR|ipOWCNf_"l@](ڴr<'! EqFYŏ P7͚k´ NH/ѼR(VO|fp?~n667 NHP[72}XL؀6bWWW"LǿHZ\*EX"){?3FM]`x9i1jUEqZG? ;O_~9r揲p|LG h1͚3›7|>?d_ɾWPȱТuFgC8 b~Em hĨE sğ` <1L16O)eYV#^E;ɲ,VT}~3a{nٶX,(S"l@iOc>h%l@֝tvgCh%l@|^ez@. d2mn<]\.67 k65/6爛J؀fٍ=!+aI؀Z OmdYGըͰW_VEQMZIU1.î ,6jaC@z Ua4zh?V<˾aI؀gɲ,l{w7cEQkyJ؀L___v]N؀VR5{UROY<Є Z Θ7N~[)ap d2inndYV J؀)U6j YT\=J6f=jzS6f[~ժ( y`K؀[fFUOOw&6TRp+٬zmC6VE p+Ykzw!$Xz7:mvmꀋ8^Vk%UsJ*-an~zv dRezmp dRH)5/K|>o^lm\^^ AT1j#~WP De6G܀sR(Ǖ@_ x )cuj*=f9U}N0 p7٬17T\ Oki8]Yy~cݴfJ*a@(6 ֝t%weY#BXZL؀;k#c6!6Zm4G]]]v]E؀;h?md.1al:EnmcpƄ dRh˥7` p6eT!奰 z UG\0dmzCC&l}fبmWUQ0Ll6khT:6jb*6...n#.l(U,=1pl6k^r ՝YS+pO1ᵋ(VΒڶ `sA6ZFhî (pFU˫b>H؀Nv6p, x.ɤRʲl{qw⡶ ` x.y1mܶm\mzį `h x.zk}3&lsͰ^7 ϫuFmgc[IuEO؀6B\=Z1y^ma׾ݴ-6|8~yBg/R~C},˲۷=⻛1Ƣ(VvC=41`V^nHBoe(ɤBRj6kCdib o㨼}iqbE3,˪Oq}/zu!CZ:z ٫6y# a\~˷3"ɝ4˨](aas_dl򑐊yx|?=cfvz7!68_/ҳB8|l=wBX,r68_1k!ՎRM!!!mBڄTw?NO64Gas/>P.B~8i}O|k=*X'lagmgu/5?Q{,v 2׏68gewJ٫ϧuLvnxa٬V<Ͻ &eTGCZ i=Nl6kFh?C`gse8[M907/ ` PT1OΒtWjl(2<=6[=2͍ZQY+7ON?8'-s~fng?\荝 t:-˲y6o\ t й洍jk,k~D؀fm؅,ˆ\I%a:Gv&a:G|۶̲lX96s~;yEq^ mp8ݛ691bqup C, 8ֶs"l!4O !aa>^6s"l!<]L).e\. <p TQ6s"lW*s"l;!lD؀L&FQG|\saٶ! m#UT}~A$l[9 UY6pfq`8 8ְB!\^^ s;$lBVJ6G(f5 8uT6TzX6MۨҶaym!hp~ 8oCca*؜Wr7&lVRU˨ ,ph=6#l;*sm96&ɾ:ƸX,ʲ<=hfxp A\ 8u`6* Ѓְ!I6Ѩy1T;(f#o'J؀~nnBȶꀋxHcߴ> Џ}=͍qfY,RJۀ1n>)0@=}/zcgQ;v9گ,r 8Ef:6RalnJ؀޴6܅ t Л}R6bY]__ )67ɤ5l]1.,4 l@y#cp Ч|޼Gxg Sh`8p ӾQ6S$l@FѾչڛfi6g͍R-l+.%l@wm8El_FusCN=f;6c&l1vhT=+eY.K6Nh#.lGK؀cGF3&l1#^81ad2w zq\e)oI؀#)ֶî ඄ 8Ry1˲꩸*c&lj=*`vaw0N[FznwygX: pjY~1Ƣ(%Ëjl77^|EpymFxJi</ݕm%Ǐ>.RJb}quu]njlk pfYu /bGm/RJoOf]LJ)UN]Jb}gRu#~cqV]<:p 8^,[<ϷC1Qx\Rʲl6r<#rԊ|w8jCs"lQ*hTJ_~a㘋96f+I8jZ]^^nnRw: lQ#>WծHz/ڭ9˲eF* ,{Qmy[pHx\Rʲlnmf|Odr,'3[8l^wqP( tZ$l+fM8#M؀1ϟ~x:Rekvz$lɸq[(} 8%|\VC8j6=zQ1vN|>1Iq3pJQ|gxaN|>1SJ a脰tB:!l6N@' ЉQ ~ͯme^qS$l$dyE'J8N1ľ Eϧjy$M^<Ї^p ]~o,dy1靷›_,_|5_.@ĨBaۿYdƓ!B8Lf;{6?0cG1>0a0U(by, (~ | 1=|ҟdE ޛ,a781 ("E*kI!g1?J9s-d]ʲt}֫0t(7p7X8911Ya_{ EH^'Ohx}Q@]cyu$l"lfya*1y]Ķ$'h5ܖћ2"xY1,B LZ&Kw_j1<_bMY6zݣُQx{Q1"ON>Ͱy b?>*)kE@G/~db,bE(?=Ёd/x2 8y>?FnT 0GPC\Yr a!KX)IDATa #X:^eT@!xO~e_,Fyc6H!?a0y_C/eEMH)]L3I* +_|4M?xsy?"8JN:!l6N@'  a脰tB:!l6N@'  a脰tB:!l6N@'  a脰tB:!l6N@'  a脰tB:!l6N@'  a脰tB:!l6N@'  a脰tB:!l6N@'  a脰tB:!l6N@'  a脰tB:!l6N@'  a脰tB:!l6N@'  a6hWIENDB`PK-B\Q_ddpix/show-grader-panel.pngnu[PNG  IHDRw=sRGBDeXIfMM*i5uIDATH ;KQ7eDhX[ vV`a#FB\ (jB#{Kvd;gfr澒MA Ғf9q @|;iNJ3j%Z!LZAq mkWGhpXd\hpXd|2~ЪAŚݛ{Cvq 4=d>20A3[$h40fIG+M p(Gb!GxJ,#ihU[ܪM7۽˪s_mj%z}wo]:IENDB`PK-B\.pix/monologo.svgnu[ PK-B\^lib.phpnu[. /** * @package mod_forum * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use mod_forum\local\entities\forum as forum_entity; defined('MOODLE_INTERNAL') || die(); /** Include required files */ require_once(__DIR__ . '/deprecatedlib.php'); require_once($CFG->libdir.'/filelib.php'); /// CONSTANTS /////////////////////////////////////////////////////////// define('FORUM_MODE_FLATOLDEST', 1); define('FORUM_MODE_FLATNEWEST', -1); define('FORUM_MODE_THREADED', 2); define('FORUM_MODE_NESTED', 3); define('FORUM_MODE_NESTED_V2', 4); define('FORUM_CHOOSESUBSCRIBE', 0); define('FORUM_FORCESUBSCRIBE', 1); define('FORUM_INITIALSUBSCRIBE', 2); define('FORUM_DISALLOWSUBSCRIBE',3); /** * FORUM_TRACKING_OFF - Tracking is not available for this forum. */ define('FORUM_TRACKING_OFF', 0); /** * FORUM_TRACKING_OPTIONAL - Tracking is based on user preference. */ define('FORUM_TRACKING_OPTIONAL', 1); /** * FORUM_TRACKING_FORCED - Tracking is on, regardless of user setting. * Treated as FORUM_TRACKING_OPTIONAL if $CFG->forum_allowforcedreadtracking is off. */ define('FORUM_TRACKING_FORCED', 2); define('FORUM_MAILED_PENDING', 0); define('FORUM_MAILED_SUCCESS', 1); define('FORUM_MAILED_ERROR', 2); if (!defined('FORUM_CRON_USER_CACHE')) { /** Defines how many full user records are cached in forum cron. */ define('FORUM_CRON_USER_CACHE', 5000); } /** * FORUM_POSTS_ALL_USER_GROUPS - All the posts in groups where the user is enrolled. */ define('FORUM_POSTS_ALL_USER_GROUPS', -2); define('FORUM_DISCUSSION_PINNED', 1); define('FORUM_DISCUSSION_UNPINNED', 0); /// STANDARD FUNCTIONS /////////////////////////////////////////////////////////// /** * Given an object containing all the necessary data, * (defined by the form in mod_form.php) this function * will create a new instance and return the id number * of the new instance. * * @param stdClass $forum add forum instance * @param mod_forum_mod_form $mform * @return int intance id */ function forum_add_instance($forum, $mform = null) { global $CFG, $DB; require_once($CFG->dirroot.'/mod/forum/locallib.php'); $forum->timemodified = time(); if (empty($forum->assessed)) { $forum->assessed = 0; } if (empty($forum->ratingtime) or empty($forum->assessed)) { $forum->assesstimestart = 0; $forum->assesstimefinish = 0; } $forum->id = $DB->insert_record('forum', $forum); $modcontext = context_module::instance($forum->coursemodule); if ($forum->type == 'single') { // Create related discussion. $discussion = new stdClass(); $discussion->course = $forum->course; $discussion->forum = $forum->id; $discussion->name = $forum->name; $discussion->assessed = $forum->assessed; $discussion->message = $forum->intro; $discussion->messageformat = $forum->introformat; $discussion->messagetrust = trusttext_trusted(context_course::instance($forum->course)); $discussion->mailnow = false; $discussion->groupid = -1; $message = ''; $discussion->id = forum_add_discussion($discussion, null, $message); if ($mform and $draftid = file_get_submitted_draft_itemid('introeditor')) { // Ugly hack - we need to copy the files somehow. $discussion = $DB->get_record('forum_discussions', array('id'=>$discussion->id), '*', MUST_EXIST); $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost), '*', MUST_EXIST); $options = array('subdirs'=>true); // Use the same options as intro field! $post->message = file_save_draft_area_files($draftid, $modcontext->id, 'mod_forum', 'post', $post->id, $options, $post->message); $DB->set_field('forum_posts', 'message', $post->message, array('id'=>$post->id)); } } forum_update_calendar($forum, $forum->coursemodule); forum_grade_item_update($forum); $completiontimeexpected = !empty($forum->completionexpected) ? $forum->completionexpected : null; \core_completion\api::update_completion_date_event($forum->coursemodule, 'forum', $forum->id, $completiontimeexpected); return $forum->id; } /** * Handle changes following the creation of a forum instance. * This function is typically called by the course_module_created observer. * * @param object $context the forum context * @param stdClass $forum The forum object * @return void */ function forum_instance_created($context, $forum) { if ($forum->forcesubscribe == FORUM_INITIALSUBSCRIBE) { $users = \mod_forum\subscriptions::get_potential_subscribers($context, 0, 'u.id, u.email'); foreach ($users as $user) { \mod_forum\subscriptions::subscribe_user($user->id, $forum, $context); } } } /** * Given an object containing all the necessary data, * (defined by the form in mod_form.php) this function * will update an existing instance with new data. * * @global object * @param object $forum forum instance (with magic quotes) * @return bool success */ function forum_update_instance($forum, $mform) { global $CFG, $DB, $OUTPUT, $USER; require_once($CFG->dirroot.'/mod/forum/locallib.php'); $forum->timemodified = time(); $forum->id = $forum->instance; if (empty($forum->assessed)) { $forum->assessed = 0; } if (empty($forum->ratingtime) or empty($forum->assessed)) { $forum->assesstimestart = 0; $forum->assesstimefinish = 0; } $oldforum = $DB->get_record('forum', array('id'=>$forum->id)); // MDL-3942 - if the aggregation type or scale (i.e. max grade) changes then recalculate the grades for the entire forum // if scale changes - do we need to recheck the ratings, if ratings higher than scale how do we want to respond? // for count and sum aggregation types the grade we check to make sure they do not exceed the scale (i.e. max score) when calculating the grade $updategrades = false; if ($oldforum->assessed <> $forum->assessed) { // Whether this forum is rated. $updategrades = true; } if ($oldforum->scale <> $forum->scale) { // The scale currently in use. $updategrades = true; } if (empty($oldforum->grade_forum) || $oldforum->grade_forum <> $forum->grade_forum) { // The whole forum grading. $updategrades = true; } if ($updategrades) { forum_update_grades($forum); // Recalculate grades for the forum. } if ($forum->type == 'single') { // Update related discussion and post. $discussions = $DB->get_records('forum_discussions', array('forum'=>$forum->id), 'timemodified ASC'); if (!empty($discussions)) { if (count($discussions) > 1) { echo $OUTPUT->notification(get_string('warnformorepost', 'forum')); } $discussion = array_pop($discussions); } else { // try to recover by creating initial discussion - MDL-16262 $discussion = new stdClass(); $discussion->course = $forum->course; $discussion->forum = $forum->id; $discussion->name = $forum->name; $discussion->assessed = $forum->assessed; $discussion->message = $forum->intro; $discussion->messageformat = $forum->introformat; $discussion->messagetrust = true; $discussion->mailnow = false; $discussion->groupid = -1; $message = ''; forum_add_discussion($discussion, null, $message); if (! $discussion = $DB->get_record('forum_discussions', array('forum'=>$forum->id))) { throw new \moodle_exception('cannotadd', 'forum'); } } if (! $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost))) { throw new \moodle_exception('cannotfindfirstpost', 'forum'); } $cm = get_coursemodule_from_instance('forum', $forum->id); $modcontext = context_module::instance($cm->id, MUST_EXIST); $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost), '*', MUST_EXIST); $post->subject = $forum->name; $post->message = $forum->intro; $post->messageformat = $forum->introformat; $post->messagetrust = trusttext_trusted($modcontext); $post->modified = $forum->timemodified; $post->userid = $USER->id; // MDL-18599, so that current teacher can take ownership of activities. if ($mform and $draftid = file_get_submitted_draft_itemid('introeditor')) { // Ugly hack - we need to copy the files somehow. $options = array('subdirs'=>true); // Use the same options as intro field! $post->message = file_save_draft_area_files($draftid, $modcontext->id, 'mod_forum', 'post', $post->id, $options, $post->message); } \mod_forum\local\entities\post::add_message_counts($post); $DB->update_record('forum_posts', $post); $discussion->name = $forum->name; $DB->update_record('forum_discussions', $discussion); } $DB->update_record('forum', $forum); $modcontext = context_module::instance($forum->coursemodule); if (($forum->forcesubscribe == FORUM_INITIALSUBSCRIBE) && ($oldforum->forcesubscribe <> $forum->forcesubscribe)) { $users = \mod_forum\subscriptions::get_potential_subscribers($modcontext, 0, 'u.id, u.email', ''); foreach ($users as $user) { \mod_forum\subscriptions::subscribe_user($user->id, $forum, $modcontext); } } forum_update_calendar($forum, $forum->coursemodule); forum_grade_item_update($forum); $completiontimeexpected = !empty($forum->completionexpected) ? $forum->completionexpected : null; \core_completion\api::update_completion_date_event($forum->coursemodule, 'forum', $forum->id, $completiontimeexpected); return true; } /** * Given an ID of an instance of this module, * this function will permanently delete the instance * and any data that depends on it. * * @global object * @param int $id forum instance id * @return bool success */ function forum_delete_instance($id) { global $DB; if (!$forum = $DB->get_record('forum', array('id'=>$id))) { return false; } if (!$cm = get_coursemodule_from_instance('forum', $forum->id)) { return false; } if (!$course = $DB->get_record('course', array('id'=>$cm->course))) { return false; } $context = context_module::instance($cm->id); // now get rid of all files $fs = get_file_storage(); $fs->delete_area_files($context->id); $result = true; \core_completion\api::update_completion_date_event($cm->id, 'forum', $forum->id, null); // Delete digest and subscription preferences. $DB->delete_records('forum_digests', array('forum' => $forum->id)); $DB->delete_records('forum_subscriptions', array('forum'=>$forum->id)); $DB->delete_records('forum_discussion_subs', array('forum' => $forum->id)); if ($discussions = $DB->get_records('forum_discussions', array('forum'=>$forum->id))) { foreach ($discussions as $discussion) { if (!forum_delete_discussion($discussion, true, $course, $cm, $forum)) { $result = false; } } } forum_tp_delete_read_records(-1, -1, -1, $forum->id); forum_grade_item_delete($forum); // We must delete the module record after we delete the grade item. if (!$DB->delete_records('forum', array('id'=>$forum->id))) { $result = false; } return $result; } /** * Indicates API features that the forum supports. * * @uses FEATURE_GROUPS * @uses FEATURE_GROUPINGS * @uses FEATURE_MOD_INTRO * @uses FEATURE_COMPLETION_TRACKS_VIEWS * @uses FEATURE_COMPLETION_HAS_RULES * @uses FEATURE_GRADE_HAS_GRADE * @uses FEATURE_GRADE_OUTCOMES * @param string $feature * @return mixed True if module supports feature, false if not, null if doesn't know or string for the module purpose. */ function forum_supports($feature) { switch($feature) { case FEATURE_GROUPS: return true; case FEATURE_GROUPINGS: return true; case FEATURE_MOD_INTRO: return true; case FEATURE_COMPLETION_TRACKS_VIEWS: return true; case FEATURE_COMPLETION_HAS_RULES: return true; case FEATURE_GRADE_HAS_GRADE: return true; case FEATURE_GRADE_OUTCOMES: return true; case FEATURE_RATE: return true; case FEATURE_BACKUP_MOODLE2: return true; case FEATURE_SHOW_DESCRIPTION: return true; case FEATURE_PLAGIARISM: return true; case FEATURE_ADVANCED_GRADING: return true; case FEATURE_MOD_PURPOSE: return MOD_PURPOSE_COLLABORATION; case FEATURE_CAN_UNINSTALL: return false; default: return null; } } /** * Create a message-id string to use in the custom headers of forum notification emails * * message-id is used by email clients to identify emails and to nest conversations * * @param int $postid The ID of the forum post we are notifying the user about * @param int $usertoid The ID of the user being notified * @return string A unique message-id */ function forum_get_email_message_id($postid, $usertoid) { return generate_email_messageid(hash('sha256', $postid . 'to' . $usertoid)); } /** * * @param object $course * @param object $user * @param object $mod TODO this is not used in this function, refactor * @param object $forum * @return object A standard object with 2 variables: info (number of posts for this user) and time (last modified) */ function forum_user_outline($course, $user, $mod, $forum) { global $CFG; require_once("$CFG->libdir/gradelib.php"); $gradeinfo = ''; $gradetime = 0; $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id); if (!empty($grades->items[0]->grades)) { // Item 0 is the rating. $grade = reset($grades->items[0]->grades); $gradetime = max($gradetime, grade_get_date_for_user_grade($grade, $user)); if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) { $gradeinfo .= get_string('gradeforrating', 'forum', $grade) . html_writer::empty_tag('br'); } else { $gradeinfo .= get_string('gradeforratinghidden', 'forum') . html_writer::empty_tag('br'); } } // Item 1 is the whole-forum grade. if (!empty($grades->items[1]->grades)) { $grade = reset($grades->items[1]->grades); $gradetime = max($gradetime, grade_get_date_for_user_grade($grade, $user)); if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) { $gradeinfo .= get_string('gradeforwholeforum', 'forum', $grade) . html_writer::empty_tag('br'); } else { $gradeinfo .= get_string('gradeforwholeforumhidden', 'forum') . html_writer::empty_tag('br'); } } $count = forum_count_user_posts($forum->id, $user->id); if ($count && $count->postcount > 0) { $info = get_string("numposts", "forum", $count->postcount); $time = $count->lastpost; if ($gradeinfo) { $info .= ', ' . $gradeinfo; $time = max($time, $gradetime); } return (object) [ 'info' => $info, 'time' => $time, ]; } else if ($gradeinfo) { return (object) [ 'info' => $gradeinfo, 'time' => $gradetime, ]; } return null; } /** * @global object * @global object * @param object $coure * @param object $user * @param object $mod * @param object $forum */ function forum_user_complete($course, $user, $mod, $forum) { global $CFG, $USER; require_once("$CFG->libdir/gradelib.php"); $getgradeinfo = function($grades, string $type) use ($course): string { global $OUTPUT; if (empty($grades)) { return ''; } $result = ''; $grade = reset($grades); if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) { $result .= $OUTPUT->container(get_string("gradefor{$type}", "forum", $grade)); if ($grade->str_feedback) { $result .= $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback); } } else { $result .= $OUTPUT->container(get_string("gradefor{$type}hidden", "forum")); } return $result; }; $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id); // Item 0 is the rating. if (!empty($grades->items[0]->grades)) { echo $getgradeinfo($grades->items[0]->grades, 'rating'); } // Item 1 is the whole-forum grade. if (!empty($grades->items[1]->grades)) { echo $getgradeinfo($grades->items[1]->grades, 'wholeforum'); } if ($posts = forum_get_user_posts($forum->id, $user->id)) { if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id)) { throw new \moodle_exception('invalidcoursemodule'); } $context = context_module::instance($cm->id); $discussions = forum_get_user_involved_discussions($forum->id, $user->id); $posts = array_filter($posts, function($post) use ($discussions) { return isset($discussions[$post->discussion]); }); $entityfactory = mod_forum\local\container::get_entity_factory(); $rendererfactory = mod_forum\local\container::get_renderer_factory(); $postrenderer = $rendererfactory->get_posts_renderer(); echo $postrenderer->render( $USER, [$forum->id => $entityfactory->get_forum_from_stdclass($forum, $context, $cm, $course)], array_map(function($discussion) use ($entityfactory) { return $entityfactory->get_discussion_from_stdclass($discussion); }, $discussions), array_map(function($post) use ($entityfactory) { return $entityfactory->get_post_from_stdclass($post); }, $posts) ); } else { echo "

".get_string("noposts", "forum")."

"; } } /** * Returns whether the discussion group is visible by the current user or not. * * @since Moodle 2.8, 2.7.1, 2.6.4 * @param cm_info $cm The discussion course module * @param int $discussiongroupid The discussion groupid * @return bool */ function forum_is_user_group_discussion(cm_info $cm, $discussiongroupid) { if ($discussiongroupid == -1 || $cm->effectivegroupmode != SEPARATEGROUPS) { return true; } if (isguestuser()) { return false; } if (has_capability('moodle/site:accessallgroups', context_module::instance($cm->id)) || in_array($discussiongroupid, $cm->get_modinfo()->get_groups($cm->groupingid))) { return true; } return false; } /** * Given a course and a date, prints a summary of all the new * messages posted in the course since that date * * @global object * @global object * @global object * @uses CONTEXT_MODULE * @uses VISIBLEGROUPS * @param object $course * @param bool $viewfullnames capability * @param int $timestart * @return bool success */ function forum_print_recent_activity($course, $viewfullnames, $timestart) { global $USER, $DB, $OUTPUT; // do not use log table if possible, it may be huge and is expensive to join with other tables $userfieldsapi = \core_user\fields::for_userpic(); $allnamefields = $userfieldsapi->get_sql('u', false, '', 'duserid', false)->selects; if (!$posts = $DB->get_records_sql("SELECT p.*, f.course, f.type AS forumtype, f.name AS forumname, f.intro, f.introformat, f.duedate, f.cutoffdate, f.assessed AS forumassessed, f.assesstimestart, f.assesstimefinish, f.scale, f.grade_forum, f.maxbytes, f.maxattachments, f.forcesubscribe, f.trackingtype, f.rsstype, f.rssarticles, f.timemodified, f.warnafter, f.blockafter, f.blockperiod, f.completiondiscussions, f.completionreplies, f.completionposts, f.displaywordcount, f.lockdiscussionafter, f.grade_forum_notify, d.name AS discussionname, d.firstpost, d.userid AS discussionstarter, d.assessed AS discussionassessed, d.timemodified, d.usermodified, d.forum, d.groupid, d.timestart, d.timeend, d.pinned, d.timelocked, $allnamefields FROM {forum_posts} p JOIN {forum_discussions} d ON d.id = p.discussion JOIN {forum} f ON f.id = d.forum JOIN {user} u ON u.id = p.userid WHERE p.created > ? AND f.course = ? AND p.deleted <> 1 ORDER BY p.id ASC", array($timestart, $course->id))) { // order by initial posting date return false; } $modinfo = get_fast_modinfo($course); $strftimerecent = get_string('strftimerecent'); $managerfactory = mod_forum\local\container::get_manager_factory(); $entityfactory = mod_forum\local\container::get_entity_factory(); $discussions = []; $capmanagers = []; $printposts = []; foreach ($posts as $post) { if (!isset($modinfo->instances['forum'][$post->forum])) { // not visible continue; } $cm = $modinfo->instances['forum'][$post->forum]; if (!$cm->uservisible) { continue; } // Get the discussion. Cache if not yet available. if (!isset($discussions[$post->discussion])) { // Build the discussion record object from the post data. $discussionrecord = (object)[ 'id' => $post->discussion, 'course' => $post->course, 'forum' => $post->forum, 'name' => $post->discussionname, 'firstpost' => $post->firstpost, 'userid' => $post->discussionstarter, 'groupid' => $post->groupid, 'assessed' => $post->discussionassessed, 'timemodified' => $post->timemodified, 'usermodified' => $post->usermodified, 'timestart' => $post->timestart, 'timeend' => $post->timeend, 'pinned' => $post->pinned, 'timelocked' => $post->timelocked ]; // Build the discussion entity from the factory and cache it. $discussions[$post->discussion] = $entityfactory->get_discussion_from_stdclass($discussionrecord); } $discussionentity = $discussions[$post->discussion]; // Get the capability manager. Cache if not yet available. if (!isset($capmanagers[$post->forum])) { $context = context_module::instance($cm->id); $coursemodule = $cm->get_course_module_record(); // Build the forum record object from the post data. $forumrecord = (object)[ 'id' => $post->forum, 'course' => $post->course, 'type' => $post->forumtype, 'name' => $post->forumname, 'intro' => $post->intro, 'introformat' => $post->introformat, 'duedate' => $post->duedate, 'cutoffdate' => $post->cutoffdate, 'assessed' => $post->forumassessed, 'assesstimestart' => $post->assesstimestart, 'assesstimefinish' => $post->assesstimefinish, 'scale' => $post->scale, 'grade_forum' => $post->grade_forum, 'maxbytes' => $post->maxbytes, 'maxattachments' => $post->maxattachments, 'forcesubscribe' => $post->forcesubscribe, 'trackingtype' => $post->trackingtype, 'rsstype' => $post->rsstype, 'rssarticles' => $post->rssarticles, 'timemodified' => $post->timemodified, 'warnafter' => $post->warnafter, 'blockafter' => $post->blockafter, 'blockperiod' => $post->blockperiod, 'completiondiscussions' => $post->completiondiscussions, 'completionreplies' => $post->completionreplies, 'completionposts' => $post->completionposts, 'displaywordcount' => $post->displaywordcount, 'lockdiscussionafter' => $post->lockdiscussionafter, 'grade_forum_notify' => $post->grade_forum_notify ]; // Build the forum entity from the factory. $forumentity = $entityfactory->get_forum_from_stdclass($forumrecord, $context, $coursemodule, $course); // Get the capability manager of this forum and cache it. $capmanagers[$post->forum] = $managerfactory->get_capability_manager($forumentity); } $capabilitymanager = $capmanagers[$post->forum]; // Get the post entity. $postentity = $entityfactory->get_post_from_stdclass($post); // Check if the user can view the post. if ($capabilitymanager->can_view_post($USER, $discussionentity, $postentity)) { $printposts[] = $post; } } unset($posts); if (!$printposts) { return false; } echo $OUTPUT->heading(get_string('newforumposts', 'forum') . ':', 6); $list = html_writer::start_tag('ul', ['class' => 'unlist']); foreach ($printposts as $post) { $subjectclass = empty($post->parent) ? ' bold' : ''; $authorhidden = forum_is_author_hidden($post, (object) ['type' => $post->forumtype]); $list .= html_writer::start_tag('li'); $list .= html_writer::start_div('head'); $list .= html_writer::div(userdate_htmltime($post->modified, $strftimerecent), 'date'); if (!$authorhidden) { $list .= html_writer::div(fullname($post, $viewfullnames), 'name'); } $list .= html_writer::end_div(); // Head. $list .= html_writer::start_div('info' . $subjectclass); $discussionurl = new moodle_url('/mod/forum/discuss.php', ['d' => $post->discussion]); if (!empty($post->parent)) { $discussionurl->param('parent', $post->parent); $discussionurl->set_anchor('p'. $post->id); } $post->subject = break_up_long_words(format_string($post->subject, true)); $list .= html_writer::link($discussionurl, $post->subject, ['rel' => 'bookmark']); $list .= html_writer::end_div(); // Info. $list .= html_writer::end_tag('li'); } $list .= html_writer::end_tag('ul'); echo $list; return true; } /** * Update activity grades. * * @param object $forum * @param int $userid specific user only, 0 means all */ function forum_update_grades($forum, $userid = 0): void { global $CFG, $DB; require_once($CFG->libdir.'/gradelib.php'); $ratings = null; if ($forum->assessed) { require_once($CFG->dirroot.'/rating/lib.php'); $cm = get_coursemodule_from_instance('forum', $forum->id); $rm = new rating_manager(); $ratings = $rm->get_user_grades((object) [ 'component' => 'mod_forum', 'ratingarea' => 'post', 'contextid' => \context_module::instance($cm->id)->id, 'modulename' => 'forum', 'moduleid ' => $forum->id, 'userid' => $userid, 'aggregationmethod' => $forum->assessed, 'scaleid' => $forum->scale, 'itemtable' => 'forum_posts', 'itemtableusercolumn' => 'userid', ]); } $forumgrades = null; if ($forum->grade_forum) { $sql = << $forum->id, ]; if ($userid) { $sql .= " AND g.userid = :userid"; $params['userid'] = $userid; } $forumgrades = []; if ($grades = $DB->get_recordset_sql($sql, $params)) { foreach ($grades as $userid => $grade) { if ($grade->rawgrade != -1) { $forumgrades[$userid] = $grade; } } $grades->close(); } } forum_grade_item_update($forum, $ratings, $forumgrades); } /** * Create/update grade items for given forum. * * @param stdClass $forum Forum object with extra cmidnumber * @param mixed $grades Optional array/object of grade(s); 'reset' means reset grades in gradebook */ function forum_grade_item_update($forum, $ratings = null, $forumgrades = null): void { global $CFG; require_once("{$CFG->libdir}/gradelib.php"); // Update the rating. $item = [ 'itemname' => get_string('gradeitemnameforrating', 'forum', $forum), 'idnumber' => $forum->cmidnumber, ]; if (!$forum->assessed || $forum->scale == 0) { $item['gradetype'] = GRADE_TYPE_NONE; } else if ($forum->scale > 0) { $item['gradetype'] = GRADE_TYPE_VALUE; $item['grademax'] = $forum->scale; $item['grademin'] = 0; } else if ($forum->scale < 0) { $item['gradetype'] = GRADE_TYPE_SCALE; $item['scaleid'] = -$forum->scale; } if ($ratings === 'reset') { $item['reset'] = true; $ratings = null; } // Itemnumber 0 is the rating. grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, $ratings, $item); // Whole forum grade. $item = [ 'itemname' => get_string('gradeitemnameforwholeforum', 'forum', $forum), // Note: We do not need to store the idnumber here. ]; if (!$forum->grade_forum) { $item['gradetype'] = GRADE_TYPE_NONE; } else if ($forum->grade_forum > 0) { $item['gradetype'] = GRADE_TYPE_VALUE; $item['grademax'] = $forum->grade_forum; $item['grademin'] = 0; } else if ($forum->grade_forum < 0) { $item['gradetype'] = GRADE_TYPE_SCALE; $item['scaleid'] = $forum->grade_forum * -1; } if ($forumgrades === 'reset') { $item['reset'] = true; $forumgrades = null; } // Itemnumber 1 is the whole forum grade. grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 1, $forumgrades, $item); } /** * Delete grade item for given forum. * * @param stdClass $forum Forum object */ function forum_grade_item_delete($forum) { global $CFG; require_once($CFG->libdir.'/gradelib.php'); grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, null, ['deleted' => 1]); grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 1, null, ['deleted' => 1]); } /** * Checks if scale is being used by any instance of forum. * * This is used to find out if scale used anywhere. * * @param $scaleid int * @return boolean True if the scale is used by any forum */ function forum_scale_used_anywhere(int $scaleid): bool { global $DB; if (empty($scaleid)) { return false; } return $DB->record_exists_select('forum', "scale = ? and assessed > 0", [$scaleid * -1]); } // SQL FUNCTIONS /////////////////////////////////////////////////////////// /** * Gets a post with all info ready for forum_print_post * Most of these joins are just to get the forum id * * @global object * @global object * @param int $postid * @return mixed array of posts or false */ function forum_get_post_full($postid) { global $CFG, $DB; $userfieldsapi = \core_user\fields::for_name(); $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects; return $DB->get_record_sql("SELECT p.*, d.forum, $allnames, u.email, u.picture, u.imagealt FROM {forum_posts} p JOIN {forum_discussions} d ON p.discussion = d.id LEFT JOIN {user} u ON p.userid = u.id WHERE p.id = ?", array($postid)); } /** * Gets all posts in discussion including top parent. * * @param int $discussionid The Discussion to fetch. * @param string $sort The sorting to apply. * @param bool $tracking Whether the user tracks this forum. * @return array The posts in the discussion. */ function forum_get_all_discussion_posts($discussionid, $sort, $tracking = false) { global $CFG, $DB, $USER; $tr_sel = ""; $tr_join = ""; $params = array(); if ($tracking) { $tr_sel = ", fr.id AS postread"; $tr_join = "LEFT JOIN {forum_read} fr ON (fr.postid = p.id AND fr.userid = ?)"; $params[] = $USER->id; } $userfieldsapi = \core_user\fields::for_name(); $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $params[] = $discussionid; if (!$posts = $DB->get_records_sql("SELECT p.*, $allnames, u.email, u.picture, u.imagealt $tr_sel FROM {forum_posts} p LEFT JOIN {user} u ON p.userid = u.id $tr_join WHERE p.discussion = ? ORDER BY $sort", $params)) { return array(); } foreach ($posts as $pid=>$p) { if ($tracking) { if (forum_tp_is_post_old($p)) { $posts[$pid]->postread = true; } } if (!$p->parent) { continue; } if (!isset($posts[$p->parent])) { continue; // parent does not exist?? } if (!isset($posts[$p->parent]->children)) { $posts[$p->parent]->children = array(); } $posts[$p->parent]->children[$pid] =& $posts[$pid]; } // Start with the last child of the first post. $post = &$posts[reset($posts)->id]; $lastpost = false; while (!$lastpost) { if (!isset($post->children)) { $post->lastpost = true; $lastpost = true; } else { // Go to the last child of this post. $post = &$posts[end($post->children)->id]; } } return $posts; } /** * An array of forum objects that the user is allowed to read/search through. * * @global object * @global object * @global object * @param int $userid * @param int $courseid if 0, we look for forums throughout the whole site. * @return array of forum objects, or false if no matches * Forum objects have the following attributes: * id, type, course, cmid, cmvisible, cmgroupmode, accessallgroups, * viewhiddentimedposts */ function forum_get_readable_forums($userid, $courseid=0) { global $CFG, $DB, $USER; require_once($CFG->dirroot.'/course/lib.php'); if (!$forummod = $DB->get_record('modules', array('name' => 'forum'))) { throw new \moodle_exception('notinstalled', 'forum'); } if ($courseid) { $courses = $DB->get_records('course', array('id' => $courseid)); } else { // If no course is specified, then the user can see SITE + his courses. $courses1 = $DB->get_records('course', array('id' => SITEID)); $courses2 = enrol_get_users_courses($userid, true, array('modinfo')); $courses = array_merge($courses1, $courses2); } if (!$courses) { return array(); } $readableforums = array(); foreach ($courses as $course) { $modinfo = get_fast_modinfo($course); if (empty($modinfo->instances['forum'])) { // hmm, no forums? continue; } $courseforums = $DB->get_records('forum', array('course' => $course->id)); foreach ($modinfo->instances['forum'] as $forumid => $cm) { if (!$cm->uservisible or !isset($courseforums[$forumid])) { continue; } $context = context_module::instance($cm->id); $forum = $courseforums[$forumid]; $forum->context = $context; $forum->cm = $cm; if (!has_capability('mod/forum:viewdiscussion', $context)) { continue; } /// group access if (groups_get_activity_groupmode($cm, $course) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) { $forum->onlygroups = $modinfo->get_groups($cm->groupingid); $forum->onlygroups[] = -1; } /// hidden timed discussions $forum->viewhiddentimedposts = true; if (!empty($CFG->forum_enabletimedposts)) { if (!has_capability('mod/forum:viewhiddentimedposts', $context)) { $forum->viewhiddentimedposts = false; } } /// qanda access if ($forum->type == 'qanda' && !has_capability('mod/forum:viewqandawithoutposting', $context)) { // We need to check whether the user has posted in the qanda forum. $forum->onlydiscussions = array(); // Holds discussion ids for the discussions // the user is allowed to see in this forum. if ($discussionspostedin = forum_discussions_user_has_posted_in($forum->id, $USER->id)) { foreach ($discussionspostedin as $d) { $forum->onlydiscussions[] = $d->id; } } } $readableforums[$forum->id] = $forum; } unset($modinfo); } // End foreach $courses return $readableforums; } /** * Returns a list of posts found using an array of search terms. * * @global object * @global object * @global object * @param array $searchterms array of search terms, e.g. word +word -word * @param int $courseid if 0, we search through the whole site * @param int $limitfrom * @param int $limitnum * @param int &$totalcount * @param string $extrasql * @return array|bool Array of posts found or false */ function forum_search_posts($searchterms, $courseid, $limitfrom, $limitnum, &$totalcount, $extrasql='') { global $CFG, $DB, $USER; require_once($CFG->libdir.'/searchlib.php'); $forums = forum_get_readable_forums($USER->id, $courseid); if (count($forums) == 0) { $totalcount = 0; return false; } $now = floor(time() / 60) * 60; // DB Cache Friendly. $fullaccess = array(); $where = array(); $params = array(); foreach ($forums as $forumid => $forum) { $select = array(); if (!$forum->viewhiddentimedposts) { $select[] = "(d.userid = :userid{$forumid} OR (d.timestart < :timestart{$forumid} AND (d.timeend = 0 OR d.timeend > :timeend{$forumid})))"; $params = array_merge($params, array('userid'.$forumid=>$USER->id, 'timestart'.$forumid=>$now, 'timeend'.$forumid=>$now)); } $cm = $forum->cm; $context = $forum->context; if ($forum->type == 'qanda' && !has_capability('mod/forum:viewqandawithoutposting', $context)) { if (!empty($forum->onlydiscussions)) { list($discussionid_sql, $discussionid_params) = $DB->get_in_or_equal($forum->onlydiscussions, SQL_PARAMS_NAMED, 'qanda'.$forumid.'_'); $params = array_merge($params, $discussionid_params); $select[] = "(d.id $discussionid_sql OR p.parent = 0)"; } else { $select[] = "p.parent = 0"; } } if (!empty($forum->onlygroups)) { list($groupid_sql, $groupid_params) = $DB->get_in_or_equal($forum->onlygroups, SQL_PARAMS_NAMED, 'grps'.$forumid.'_'); $params = array_merge($params, $groupid_params); $select[] = "d.groupid $groupid_sql"; } if ($select) { $selects = implode(" AND ", $select); $where[] = "(d.forum = :forum{$forumid} AND $selects)"; $params['forum'.$forumid] = $forumid; } else { $fullaccess[] = $forumid; } } if ($fullaccess) { list($fullid_sql, $fullid_params) = $DB->get_in_or_equal($fullaccess, SQL_PARAMS_NAMED, 'fula'); $params = array_merge($params, $fullid_params); $where[] = "(d.forum $fullid_sql)"; } $favjoin = ""; if (in_array('starredonly:on', $searchterms)) { $usercontext = context_user::instance($USER->id); $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext); list($favjoin, $favparams) = $ufservice->get_join_sql_by_type('mod_forum', 'discussions', "favourited", "d.id"); $searchterms = array_values(array_diff($searchterms, array('starredonly:on'))); $params = array_merge($params, $favparams); $extrasql .= " AND favourited.itemid IS NOT NULL AND favourited.itemid != 0"; } $selectdiscussion = "(".implode(" OR ", $where).")"; $messagesearch = ''; $searchstring = ''; // Need to concat these back together for parser to work. foreach($searchterms as $searchterm){ if ($searchstring != '') { $searchstring .= ' '; } $searchstring .= $searchterm; } // We need to allow quoted strings for the search. The quotes *should* be stripped // by the parser, but this should be examined carefully for security implications. $searchstring = str_replace("\\\"","\"",$searchstring); $parser = new search_parser(); $lexer = new search_lexer($parser); if ($lexer->parse($searchstring)) { $parsearray = $parser->get_parsed_array(); $tagjoins = ''; $tagfields = []; $tagfieldcount = 0; if ($parsearray) { foreach ($parsearray as $token) { if ($token->getType() == TOKEN_TAGS) { for ($i = 0; $i <= substr_count($token->getValue(), ','); $i++) { // Queries can only have a limited number of joins so set a limit sensible users won't exceed. if ($tagfieldcount > 10) { continue; } $tagjoins .= " LEFT JOIN {tag_instance} ti_$tagfieldcount ON p.id = ti_$tagfieldcount.itemid AND ti_$tagfieldcount.component = 'mod_forum' AND ti_$tagfieldcount.itemtype = 'forum_posts'"; $tagjoins .= " LEFT JOIN {tag} t_$tagfieldcount ON t_$tagfieldcount.id = ti_$tagfieldcount.tagid"; $tagfields[] = "t_$tagfieldcount.rawname"; $tagfieldcount++; } } } list($messagesearch, $msparams) = search_generate_SQL($parsearray, 'p.message', 'p.subject', 'p.userid', 'u.id', 'u.firstname', 'u.lastname', 'p.modified', 'd.forum', $tagfields); $params = ($msparams ? array_merge($params, $msparams) : $params); } } $fromsql = "{forum_posts} p INNER JOIN {forum_discussions} d ON d.id = p.discussion INNER JOIN {user} u ON u.id = p.userid $tagjoins $favjoin"; $selectsql = ($messagesearch ? $messagesearch . " AND " : ""). " p.discussion = d.id AND p.userid = u.id AND $selectdiscussion $extrasql"; $countsql = "SELECT COUNT(*) FROM $fromsql WHERE $selectsql"; $userfieldsapi = \core_user\fields::for_name(); $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $searchsql = "SELECT p.*, d.forum, $allnames, u.email, u.picture, u.imagealt FROM $fromsql WHERE $selectsql ORDER BY p.modified DESC"; $totalcount = $DB->count_records_sql($countsql, $params); return $DB->get_records_sql($searchsql, $params, $limitfrom, $limitnum); } /** * Get all the posts for a user in a forum suitable for forum_print_post * * @global object * @global object * @uses CONTEXT_MODULE * @return array */ function forum_get_user_posts($forumid, $userid) { global $CFG, $DB; $timedsql = ""; $params = array($forumid, $userid); if (!empty($CFG->forum_enabletimedposts)) { $cm = get_coursemodule_from_instance('forum', $forumid); if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) { $now = time(); $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))"; $params[] = $now; $params[] = $now; } } $userfieldsapi = \core_user\fields::for_name(); $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects; return $DB->get_records_sql("SELECT p.*, d.forum, $allnames, u.email, u.picture, u.imagealt FROM {forum} f JOIN {forum_discussions} d ON d.forum = f.id JOIN {forum_posts} p ON p.discussion = d.id JOIN {user} u ON u.id = p.userid WHERE f.id = ? AND p.userid = ? $timedsql ORDER BY p.modified ASC", $params); } /** * Get all the discussions user participated in * * @global object * @global object * @uses CONTEXT_MODULE * @param int $forumid * @param int $userid * @return array Array or false */ function forum_get_user_involved_discussions($forumid, $userid) { global $CFG, $DB; $timedsql = ""; $params = array($forumid, $userid); if (!empty($CFG->forum_enabletimedposts)) { $cm = get_coursemodule_from_instance('forum', $forumid); if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) { $now = time(); $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))"; $params[] = $now; $params[] = $now; } } return $DB->get_records_sql("SELECT DISTINCT d.* FROM {forum} f JOIN {forum_discussions} d ON d.forum = f.id JOIN {forum_posts} p ON p.discussion = d.id WHERE f.id = ? AND p.userid = ? $timedsql", $params); } /** * Get all the posts for a user in a forum suitable for forum_print_post * * @global object * @global object * @param int $forumid * @param int $userid * @return stdClass|false collection of counts or false */ function forum_count_user_posts($forumid, $userid) { global $CFG, $DB; $timedsql = ""; $params = array($forumid, $userid); if (!empty($CFG->forum_enabletimedposts)) { $cm = get_coursemodule_from_instance('forum', $forumid); if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) { $now = time(); $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))"; $params[] = $now; $params[] = $now; } } return $DB->get_record_sql("SELECT COUNT(p.id) AS postcount, MAX(p.modified) AS lastpost FROM {forum} f JOIN {forum_discussions} d ON d.forum = f.id JOIN {forum_posts} p ON p.discussion = d.id JOIN {user} u ON u.id = p.userid WHERE f.id = ? AND p.userid = ? $timedsql", $params); } /** * Given a log entry, return the forum post details for it. * * @global object * @global object * @param object $log * @return array|null */ function forum_get_post_from_log($log) { global $CFG, $DB; $userfieldsapi = \core_user\fields::for_name(); $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects; if ($log->action == "add post") { return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid, $allnames, u.email, u.picture FROM {forum_discussions} d, {forum_posts} p, {forum} f, {user} u WHERE p.id = ? AND d.id = p.discussion AND p.userid = u.id AND u.deleted <> '1' AND f.id = d.forum", array($log->info)); } else if ($log->action == "add discussion") { return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid, $allnames, u.email, u.picture FROM {forum_discussions} d, {forum_posts} p, {forum} f, {user} u WHERE d.id = ? AND d.firstpost = p.id AND p.userid = u.id AND u.deleted <> '1' AND f.id = d.forum", array($log->info)); } return NULL; } /** * Given a discussion id, return the first post from the discussion * * @global object * @global object * @param int $dicsussionid * @return array */ function forum_get_firstpost_from_discussion($discussionid) { global $CFG, $DB; return $DB->get_record_sql("SELECT p.* FROM {forum_discussions} d, {forum_posts} p WHERE d.id = ? AND d.firstpost = p.id ", array($discussionid)); } /** * Returns an array of counts of replies to each discussion * * @param int $forumid * @param string $forumsort * @param int $limit * @param int $page * @param int $perpage * @param boolean $canseeprivatereplies Whether the current user can see private replies. * @return array */ function forum_count_discussion_replies($forumid, $forumsort = "", $limit = -1, $page = -1, $perpage = 0, $canseeprivatereplies = false) { global $CFG, $DB, $USER; if ($limit > 0) { $limitfrom = 0; $limitnum = $limit; } else if ($page != -1) { $limitfrom = $page*$perpage; $limitnum = $perpage; } else { $limitfrom = 0; $limitnum = 0; } if ($forumsort == "") { $orderby = ""; $groupby = ""; } else { $orderby = "ORDER BY $forumsort"; $groupby = ", ".strtolower($forumsort); $groupby = str_replace('desc', '', $groupby); $groupby = str_replace('asc', '', $groupby); } $params = ['forumid' => $forumid]; if (!$canseeprivatereplies) { $privatewhere = ' AND (p.privatereplyto = :currentuser1 OR p.userid = :currentuser2 OR p.privatereplyto = 0)'; $params['currentuser1'] = $USER->id; $params['currentuser2'] = $USER->id; } else { $privatewhere = ''; } if (($limitfrom == 0 and $limitnum == 0) or $forumsort == "") { $sql = "SELECT p.discussion, COUNT(p.id) AS replies, MAX(p.id) AS lastpostid FROM {forum_posts} p JOIN {forum_discussions} d ON p.discussion = d.id WHERE p.parent > 0 AND d.forum = :forumid $privatewhere GROUP BY p.discussion"; return $DB->get_records_sql($sql, $params); } else { $sql = "SELECT p.discussion, (COUNT(p.id) - 1) AS replies, MAX(p.id) AS lastpostid FROM {forum_posts} p JOIN {forum_discussions} d ON p.discussion = d.id WHERE d.forum = :forumid $privatewhere GROUP BY p.discussion $groupby $orderby"; return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum); } } /** * @global object * @global object * @global object * @param object $forum * @param object $cm * @param object $course * @return mixed */ function forum_count_discussions($forum, $cm, $course) { global $CFG, $DB, $USER; $cache = cache::make('mod_forum', 'forum_count_discussions'); $cachedcounts = $cache->get($course->id); if ($cachedcounts === false) { $cachedcounts = []; } $now = floor(time() / 60) * 60; // DB Cache Friendly. $params = array($course->id); if (!isset($cachedcounts[$forum->id])) { // Initialize the cachedcounts for this forum id to 0 by default. After the // database query, if there are discussions then it should update the count. $cachedcounts[$forum->id] = 0; if (!empty($CFG->forum_enabletimedposts)) { $timedsql = "AND d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?)"; $params[] = $now; $params[] = $now; } else { $timedsql = ""; } $sql = "SELECT f.id, COUNT(d.id) as dcount FROM {forum} f JOIN {forum_discussions} d ON d.forum = f.id WHERE f.course = ? $timedsql GROUP BY f.id"; if ($counts = $DB->get_records_sql($sql, $params)) { foreach ($counts as $count) { $cachedcounts[$count->id] = $count->dcount; } $cache->set($course->id, $cachedcounts); } else { $cache->set($course->id, $cachedcounts); return $cachedcounts[$forum->id]; } } $groupmode = groups_get_activity_groupmode($cm, $course); if ($groupmode != SEPARATEGROUPS) { return $cachedcounts[$forum->id]; } if (has_capability('moodle/site:accessallgroups', context_module::instance($cm->id))) { return $cachedcounts[$forum->id]; } require_once($CFG->dirroot.'/course/lib.php'); $modinfo = get_fast_modinfo($course); $mygroups = $modinfo->get_groups($cm->groupingid); // add all groups posts $mygroups[-1] = -1; list($mygroups_sql, $params) = $DB->get_in_or_equal($mygroups); $params[] = $forum->id; if (!empty($CFG->forum_enabletimedposts)) { $timedsql = "AND d.timestart < $now AND (d.timeend = 0 OR d.timeend > $now)"; $params[] = $now; $params[] = $now; } else { $timedsql = ""; } $sql = "SELECT COUNT(d.id) FROM {forum_discussions} d WHERE d.groupid $mygroups_sql AND d.forum = ? $timedsql"; return $DB->get_field_sql($sql, $params); } /** * Get all discussions in a forum * * @global object * @global object * @global object * @uses CONTEXT_MODULE * @uses VISIBLEGROUPS * @param object $cm * @param string $forumsort * @param bool $fullpost * @param int $unused * @param int $limit * @param bool $userlastmodified * @param int $page * @param int $perpage * @param int $groupid if groups enabled, get discussions for this group overriding the current group. * Use FORUM_POSTS_ALL_USER_GROUPS for all the user groups * @param int $updatedsince retrieve only discussions updated since the given time * @return array */ function forum_get_discussions($cm, $forumsort="", $fullpost=true, $unused=-1, $limit=-1, $userlastmodified=false, $page=-1, $perpage=0, $groupid = -1, $updatedsince = 0) { global $CFG, $DB, $USER; $timelimit = ''; $now = floor(time() / 60) * 60; $params = array($cm->instance); $modcontext = context_module::instance($cm->id); if (!has_capability('mod/forum:viewdiscussion', $modcontext)) { /// User must have perms to view discussions return array(); } if (!empty($CFG->forum_enabletimedposts)) { /// Users must fulfill timed posts if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) { $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))"; $params[] = $now; $params[] = $now; if (isloggedin()) { $timelimit .= " OR d.userid = ?"; $params[] = $USER->id; } $timelimit .= ")"; } } if ($limit > 0) { $limitfrom = 0; $limitnum = $limit; } else if ($page != -1) { $limitfrom = $page*$perpage; $limitnum = $perpage; } else { $limitfrom = 0; $limitnum = 0; } $groupmode = groups_get_activity_groupmode($cm); if ($groupmode) { if (empty($modcontext)) { $modcontext = context_module::instance($cm->id); } // Special case, we received a groupid to override currentgroup. if ($groupid > 0) { $course = get_course($cm->course); if (!groups_group_visible($groupid, $course, $cm)) { // User doesn't belong to this group, return nothing. return array(); } $currentgroup = $groupid; } else if ($groupid === -1) { $currentgroup = groups_get_activity_group($cm); } else { // Get discussions for all groups current user can see. $currentgroup = null; } if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) { if ($currentgroup) { $groupselect = "AND (d.groupid = ? OR d.groupid = -1)"; $params[] = $currentgroup; } else { $groupselect = ""; } } else { // Separate groups. // Get discussions for all groups current user can see. if ($currentgroup === null) { $mygroups = array_keys(groups_get_all_groups($cm->course, $USER->id, $cm->groupingid, 'g.id')); if (empty($mygroups)) { $groupselect = "AND d.groupid = -1"; } else { list($insqlgroups, $inparamsgroups) = $DB->get_in_or_equal($mygroups); $groupselect = "AND (d.groupid = -1 OR d.groupid $insqlgroups)"; $params = array_merge($params, $inparamsgroups); } } else if ($currentgroup) { $groupselect = "AND (d.groupid = ? OR d.groupid = -1)"; $params[] = $currentgroup; } else { $groupselect = "AND d.groupid = -1"; } } } else { $groupselect = ""; } if (empty($forumsort)) { $forumsort = forum_get_default_sort_order(); } if (!str_contains($forumsort, 'id')) { $forumsort .= ', d.id DESC'; } if (empty($fullpost)) { $postdata = "p.id, p.subject, p.modified, p.discussion, p.userid, p.created"; } else { $postdata = "p.*"; } $userfieldsapi = \core_user\fields::for_name(); if (empty($userlastmodified)) { // We don't need to know this $umfields = ""; $umtable = ""; } else { $umfields = $userfieldsapi->get_sql('um', false, 'um')->selects . ', um.email AS umemail, um.picture AS umpicture, um.imagealt AS umimagealt'; $umtable = " LEFT JOIN {user} um ON (d.usermodified = um.id)"; } $updatedsincesql = ''; if (!empty($updatedsince)) { $updatedsincesql = 'AND d.timemodified > ?'; $params[] = $updatedsince; } $discussionfields = "d.id as discussionid, d.course, d.forum, d.name, d.firstpost, d.groupid, d.assessed," . " d.timemodified, d.usermodified, d.timestart, d.timeend, d.pinned, d.timelocked"; $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $sql = "SELECT $postdata, $discussionfields, $allnames, u.email, u.picture, u.imagealt, u.deleted AS userdeleted $umfields FROM {forum_discussions} d JOIN {forum_posts} p ON p.discussion = d.id JOIN {user} u ON p.userid = u.id $umtable WHERE d.forum = ? AND p.parent = 0 $timelimit $groupselect $updatedsincesql ORDER BY $forumsort"; return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum); } /** * Gets the neighbours (previous and next) of a discussion. * * The calculation is based on the timemodified when time modified or time created is identical * It will revert to using the ID to sort consistently. This is better tha skipping a discussion. * * For blog-style forums, the calculation is based on the original creation time of the * blog post. * * Please note that this does not check whether or not the discussion passed is accessible * by the user, it simply uses it as a reference to find the neighbours. On the other hand, * the returned neighbours are checked and are accessible to the current user. * * @param object $cm The CM record. * @param object $discussion The discussion record. * @param object $forum The forum instance record. * @return array That always contains the keys 'prev' and 'next'. When there is a result * they contain the record with minimal information such as 'id' and 'name'. * When the neighbour is not found the value is false. */ function forum_get_discussion_neighbours($cm, $discussion, $forum) { global $CFG, $DB, $USER; if ($cm->instance != $discussion->forum or $discussion->forum != $forum->id or $forum->id != $cm->instance) { throw new coding_exception('Discussion is not part of the same forum.'); } $neighbours = array('prev' => false, 'next' => false); $now = floor(time() / 60) * 60; $params = array(); $modcontext = context_module::instance($cm->id); $groupmode = groups_get_activity_groupmode($cm); $currentgroup = groups_get_activity_group($cm); // Users must fulfill timed posts. $timelimit = ''; if (!empty($CFG->forum_enabletimedposts)) { if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) { $timelimit = ' AND ((d.timestart <= :tltimestart AND (d.timeend = 0 OR d.timeend > :tltimeend))'; $params['tltimestart'] = $now; $params['tltimeend'] = $now; if (isloggedin()) { $timelimit .= ' OR d.userid = :tluserid'; $params['tluserid'] = $USER->id; } $timelimit .= ')'; } } // Limiting to posts accessible according to groups. $groupselect = ''; if ($groupmode) { if ($groupmode == VISIBLEGROUPS || has_capability('moodle/site:accessallgroups', $modcontext)) { if ($currentgroup) { $groupselect = 'AND (d.groupid = :groupid OR d.groupid = -1)'; $params['groupid'] = $currentgroup; } } else { if ($currentgroup) { $groupselect = 'AND (d.groupid = :groupid OR d.groupid = -1)'; $params['groupid'] = $currentgroup; } else { $groupselect = 'AND d.groupid = -1'; } } } $params['forumid'] = $cm->instance; $params['discid1'] = $discussion->id; $params['discid2'] = $discussion->id; $params['discid3'] = $discussion->id; $params['discid4'] = $discussion->id; $params['disctimecompare1'] = $discussion->timemodified; $params['disctimecompare2'] = $discussion->timemodified; $params['pinnedstate1'] = (int) $discussion->pinned; $params['pinnedstate2'] = (int) $discussion->pinned; $params['pinnedstate3'] = (int) $discussion->pinned; $params['pinnedstate4'] = (int) $discussion->pinned; $sql = "SELECT d.id, d.name, d.timemodified, d.groupid, d.timestart, d.timeend FROM {forum_discussions} d JOIN {forum_posts} p ON d.firstpost = p.id WHERE d.forum = :forumid AND d.id <> :discid1 $timelimit $groupselect"; $comparefield = "d.timemodified"; $comparevalue = ":disctimecompare1"; $comparevalue2 = ":disctimecompare2"; if (!empty($CFG->forum_enabletimedposts)) { // Here we need to take into account the release time (timestart) // if one is set, of the neighbouring posts and compare it to the // timestart or timemodified of *this* post depending on if the // release date of this post is in the future or not. // This stops discussions that appear later because of the // timestart value from being buried under discussions that were // made afterwards. $comparefield = "CASE WHEN d.timemodified < d.timestart THEN d.timestart ELSE d.timemodified END"; if ($discussion->timemodified < $discussion->timestart) { // Normally we would just use the timemodified for sorting // discussion posts. However, when timed discussions are enabled, // then posts need to be sorted base on the later of timemodified // or the release date of the post (timestart). $params['disctimecompare1'] = $discussion->timestart; $params['disctimecompare2'] = $discussion->timestart; } } $orderbydesc = forum_get_default_sort_order(true, $comparefield, 'd', false); $orderbyasc = forum_get_default_sort_order(false, $comparefield, 'd', false); if ($forum->type === 'blog') { $subselect = "SELECT pp.created FROM {forum_discussions} dd JOIN {forum_posts} pp ON dd.firstpost = pp.id "; $subselectwhere1 = " WHERE dd.id = :discid3"; $subselectwhere2 = " WHERE dd.id = :discid4"; $comparefield = "p.created"; $sub1 = $subselect.$subselectwhere1; $comparevalue = "($sub1)"; $sub2 = $subselect.$subselectwhere2; $comparevalue2 = "($sub2)"; $orderbydesc = "d.pinned, p.created DESC"; $orderbyasc = "d.pinned, p.created ASC"; } $prevsql = $sql . " AND ( (($comparefield < $comparevalue) AND :pinnedstate1 = d.pinned) OR ($comparefield = $comparevalue2 AND (d.pinned = 0 OR d.pinned = :pinnedstate4) AND d.id < :discid2) OR (d.pinned = 0 AND d.pinned <> :pinnedstate2)) ORDER BY CASE WHEN d.pinned = :pinnedstate3 THEN 1 ELSE 0 END DESC, $orderbydesc, d.id DESC"; $nextsql = $sql . " AND ( (($comparefield > $comparevalue) AND :pinnedstate1 = d.pinned) OR ($comparefield = $comparevalue2 AND (d.pinned = 1 OR d.pinned = :pinnedstate4) AND d.id > :discid2) OR (d.pinned = 1 AND d.pinned <> :pinnedstate2)) ORDER BY CASE WHEN d.pinned = :pinnedstate3 THEN 1 ELSE 0 END DESC, $orderbyasc, d.id ASC"; $neighbours['prev'] = $DB->get_record_sql($prevsql, $params, IGNORE_MULTIPLE); $neighbours['next'] = $DB->get_record_sql($nextsql, $params, IGNORE_MULTIPLE); return $neighbours; } /** * Get the sql to use in the ORDER BY clause for forum discussions. * * This has the ordering take timed discussion windows into account. * * @param bool $desc True for DESC, False for ASC. * @param string $compare The field in the SQL to compare to normally sort by. * @param string $prefix The prefix being used for the discussion table. * @param bool $pinned sort pinned posts to the top * @return string */ function forum_get_default_sort_order($desc = true, $compare = 'd.timemodified', $prefix = 'd', $pinned = true) { global $CFG; if (!empty($prefix)) { $prefix .= '.'; } $dir = $desc ? 'DESC' : 'ASC'; if ($pinned == true) { $pinned = "{$prefix}pinned DESC,"; } else { $pinned = ''; } $sort = "{$prefix}timemodified"; if (!empty($CFG->forum_enabletimedposts)) { $sort = "CASE WHEN {$compare} < {$prefix}timestart THEN {$prefix}timestart ELSE {$compare} END"; } return "$pinned $sort $dir"; } /** * * @global object * @global object * @global object * @uses CONTEXT_MODULE * @uses VISIBLEGROUPS * @param object $cm * @return array */ function forum_get_discussions_unread($cm) { global $CFG, $DB, $USER; $now = floor(time() / 60) * 60; $cutoffdate = $now - ($CFG->forum_oldpostdays*24*60*60); $params = array(); $groupmode = groups_get_activity_groupmode($cm); $currentgroup = groups_get_activity_group($cm); if ($groupmode) { $modcontext = context_module::instance($cm->id); if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) { if ($currentgroup) { $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)"; $params['currentgroup'] = $currentgroup; } else { $groupselect = ""; } } else { //separate groups without access all if ($currentgroup) { $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)"; $params['currentgroup'] = $currentgroup; } else { $groupselect = "AND d.groupid = -1"; } } } else { $groupselect = ""; } if (!empty($CFG->forum_enabletimedposts)) { $timedsql = "AND d.timestart < :now1 AND (d.timeend = 0 OR d.timeend > :now2)"; $params['now1'] = $now; $params['now2'] = $now; } else { $timedsql = ""; } $sql = "SELECT d.id, COUNT(p.id) AS unread FROM {forum_discussions} d JOIN {forum_posts} p ON p.discussion = d.id LEFT JOIN {forum_read} r ON (r.postid = p.id AND r.userid = $USER->id) WHERE d.forum = {$cm->instance} AND p.modified >= :cutoffdate AND r.id is NULL $groupselect $timedsql GROUP BY d.id"; $params['cutoffdate'] = $cutoffdate; if ($unreads = $DB->get_records_sql($sql, $params)) { foreach ($unreads as $unread) { $unreads[$unread->id] = $unread->unread; } return $unreads; } else { return array(); } } /** * @global object * @global object * @global object * @uses CONEXT_MODULE * @uses VISIBLEGROUPS * @param object $cm * @return array */ function forum_get_discussions_count($cm) { global $CFG, $DB, $USER; $now = floor(time() / 60) * 60; $params = array($cm->instance); $groupmode = groups_get_activity_groupmode($cm); $currentgroup = groups_get_activity_group($cm); if ($groupmode) { $modcontext = context_module::instance($cm->id); if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) { if ($currentgroup) { $groupselect = "AND (d.groupid = ? OR d.groupid = -1)"; $params[] = $currentgroup; } else { $groupselect = ""; } } else { //seprate groups without access all if ($currentgroup) { $groupselect = "AND (d.groupid = ? OR d.groupid = -1)"; $params[] = $currentgroup; } else { $groupselect = "AND d.groupid = -1"; } } } else { $groupselect = ""; } $timelimit = ""; if (!empty($CFG->forum_enabletimedposts)) { $modcontext = context_module::instance($cm->id); if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) { $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))"; $params[] = $now; $params[] = $now; if (isloggedin()) { $timelimit .= " OR d.userid = ?"; $params[] = $USER->id; } $timelimit .= ")"; } } $sql = "SELECT COUNT(d.id) FROM {forum_discussions} d JOIN {forum_posts} p ON p.discussion = d.id WHERE d.forum = ? AND p.parent = 0 $groupselect $timelimit"; return $DB->get_field_sql($sql, $params); } // OTHER FUNCTIONS /////////////////////////////////////////////////////////// /** * @global object * @global object * @param int $courseid * @param string $type */ function forum_get_course_forum($courseid, $type) { // How to set up special 1-per-course forums global $CFG, $DB, $OUTPUT, $USER; if ($forums = $DB->get_records_select("forum", "course = ? AND type = ?", array($courseid, $type), "id ASC")) { // There should always only be ONE, but with the right combination of // errors there might be more. In this case, just return the oldest one (lowest ID). foreach ($forums as $forum) { return $forum; // ie the first one } } // Doesn't exist, so create one now. $forum = new stdClass(); $forum->course = $courseid; $forum->type = "$type"; if (!empty($USER->htmleditor)) { $forum->introformat = $USER->htmleditor; } switch ($forum->type) { case "news": $forum->name = get_string("namenews", "forum"); $forum->intro = get_string("intronews", "forum"); $forum->introformat = FORMAT_HTML; $forum->forcesubscribe = $CFG->forum_announcementsubscription; $forum->maxattachments = $CFG->forum_announcementmaxattachments; $forum->assessed = 0; if ($courseid == SITEID) { $forum->name = get_string("sitenews"); $forum->forcesubscribe = 0; } break; case "social": $forum->name = get_string("namesocial", "forum"); $forum->intro = get_string("introsocial", "forum"); $forum->introformat = FORMAT_HTML; $forum->assessed = 0; $forum->forcesubscribe = 0; break; case "blog": $forum->name = get_string('blogforum', 'forum'); $forum->intro = get_string('introblog', 'forum'); $forum->introformat = FORMAT_HTML; $forum->assessed = 0; $forum->forcesubscribe = 0; break; default: echo $OUTPUT->notification("That forum type doesn't exist!"); return false; break; } $forum->timemodified = time(); $forum->id = $DB->insert_record("forum", $forum); if (! $module = $DB->get_record("modules", array("name" => "forum"))) { echo $OUTPUT->notification("Could not find forum module!!"); return false; } $mod = new stdClass(); $mod->course = $courseid; $mod->module = $module->id; $mod->instance = $forum->id; $mod->section = 0; include_once("$CFG->dirroot/course/lib.php"); if (! $mod->coursemodule = add_course_module($mod) ) { echo $OUTPUT->notification("Could not add a new course module to the course '" . $courseid . "'"); return false; } $sectionid = course_add_cm_to_section($courseid, $mod->coursemodule, 0, null, 'forum'); return $DB->get_record("forum", array("id" => "$forum->id")); } /** * Return rating related permissions * * @param string $options the context id * @return array an associative array of the user's rating permissions */ function forum_rating_permissions($contextid, $component, $ratingarea) { $context = context::instance_by_id($contextid, MUST_EXIST); if ($component != 'mod_forum' || $ratingarea != 'post') { // We don't know about this component/ratingarea so just return null to get the // default restrictive permissions. return null; } return array( 'view' => has_capability('mod/forum:viewrating', $context), 'viewany' => has_capability('mod/forum:viewanyrating', $context), 'viewall' => has_capability('mod/forum:viewallratings', $context), 'rate' => has_capability('mod/forum:rate', $context) ); } /** * Validates a submitted rating * @param array $params submitted data * context => object the context in which the rated items exists [required] * component => The component for this module - should always be mod_forum [required] * ratingarea => object the context in which the rated items exists [required] * * itemid => int the ID of the object being rated [required] * scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required] * rating => int the submitted rating [required] * rateduserid => int the id of the user whose items have been rated. NOT the user who submitted the ratings. 0 to update all. [required] * aggregation => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [required] * @return boolean true if the rating is valid. Will throw rating_exception if not */ function forum_rating_validate($params) { global $DB, $USER; // Check the component is mod_forum if ($params['component'] != 'mod_forum') { throw new rating_exception('invalidcomponent'); } // Check the ratingarea is post (the only rating area in forum) if ($params['ratingarea'] != 'post') { throw new rating_exception('invalidratingarea'); } // Check the rateduserid is not the current user .. you can't rate your own posts if ($params['rateduserid'] == $USER->id) { throw new rating_exception('nopermissiontorate'); } // Fetch all the related records ... we need to do this anyway to call forum_user_can_see_post $post = $DB->get_record('forum_posts', array('id' => $params['itemid'], 'userid' => $params['rateduserid']), '*', MUST_EXIST); $discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion), '*', MUST_EXIST); $forum = $DB->get_record('forum', array('id' => $discussion->forum), '*', MUST_EXIST); $course = $DB->get_record('course', array('id' => $forum->course), '*', MUST_EXIST); $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id , false, MUST_EXIST); $context = context_module::instance($cm->id); // Make sure the context provided is the context of the forum if ($context->id != $params['context']->id) { throw new rating_exception('invalidcontext'); } if ($forum->scale != $params['scaleid']) { //the scale being submitted doesnt match the one in the database throw new rating_exception('invalidscaleid'); } // check the item we're rating was created in the assessable time window if (!empty($forum->assesstimestart) && !empty($forum->assesstimefinish)) { if ($post->created < $forum->assesstimestart || $post->created > $forum->assesstimefinish) { throw new rating_exception('notavailable'); } } //check that the submitted rating is valid for the scale // lower limit if ($params['rating'] < 0 && $params['rating'] != RATING_UNSET_RATING) { throw new rating_exception('invalidnum'); } // upper limit if ($forum->scale < 0) { //its a custom scale $scalerecord = $DB->get_record('scale', array('id' => -$forum->scale)); if ($scalerecord) { $scalearray = explode(',', $scalerecord->scale); if ($params['rating'] > count($scalearray)) { throw new rating_exception('invalidnum'); } } else { throw new rating_exception('invalidscaleid'); } } else if ($params['rating'] > $forum->scale) { //if its numeric and submitted rating is above maximum throw new rating_exception('invalidnum'); } // Make sure groups allow this user to see the item they're rating if ($discussion->groupid > 0 and $groupmode = groups_get_activity_groupmode($cm, $course)) { // Groups are being used if (!groups_group_exists($discussion->groupid)) { // Can't find group throw new rating_exception('cannotfindgroup');//something is wrong } if (!groups_is_member($discussion->groupid) and !has_capability('moodle/site:accessallgroups', $context)) { // do not allow rating of posts from other groups when in SEPARATEGROUPS or VISIBLEGROUPS throw new rating_exception('notmemberofgroup'); } } // perform some final capability checks if (!forum_user_can_see_post($forum, $discussion, $post, $USER, $cm)) { throw new rating_exception('nopermissiontorate'); } return true; } /** * Can the current user see ratings for a given itemid? * * @param array $params submitted data * contextid => int contextid [required] * component => The component for this module - should always be mod_forum [required] * ratingarea => object the context in which the rated items exists [required] * itemid => int the ID of the object being rated [required] * scaleid => int scale id [optional] * @return bool * @throws coding_exception * @throws rating_exception */ function mod_forum_rating_can_see_item_ratings($params) { global $DB, $USER; // Check the component is mod_forum. if (!isset($params['component']) || $params['component'] != 'mod_forum') { throw new rating_exception('invalidcomponent'); } // Check the ratingarea is post (the only rating area in forum). if (!isset($params['ratingarea']) || $params['ratingarea'] != 'post') { throw new rating_exception('invalidratingarea'); } if (!isset($params['itemid'])) { throw new rating_exception('invaliditemid'); } $post = $DB->get_record('forum_posts', array('id' => $params['itemid']), '*', MUST_EXIST); $discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion), '*', MUST_EXIST); $forum = $DB->get_record('forum', array('id' => $discussion->forum), '*', MUST_EXIST); $course = $DB->get_record('course', array('id' => $forum->course), '*', MUST_EXIST); $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id , false, MUST_EXIST); $context = context_module::instance($cm->id); $ratingpermissions = forum_rating_permissions($context->id, 'mod_forum', 'post'); $requiredpermission = ($post->userid != $USER->id) ? 'viewall' : 'view'; if (!$ratingpermissions[$requiredpermission]) { return false; } // Perform some final capability checks. if (!forum_user_can_see_post($forum, $discussion, $post, $USER, $cm)) { return false; } return true; } /** * Return the markup for the discussion subscription toggling icon. * * @param stdClass $forum The forum object. * @param int $discussionid The discussion to create an icon for. * @return string The generated markup. */ function forum_get_discussion_subscription_icon($forum, $discussionid, $returnurl = null, $includetext = false) { global $USER, $OUTPUT, $PAGE; if ($returnurl === null && $PAGE->url) { $returnurl = $PAGE->url->out(); } $o = ''; $subscriptionstatus = \mod_forum\subscriptions::is_subscribed($USER->id, $forum, $discussionid); $subscriptionlink = new moodle_url('/mod/forum/subscribe.php', array( 'sesskey' => sesskey(), 'id' => $forum->id, 'd' => $discussionid, 'returnurl' => $returnurl, )); if ($includetext) { $o .= $subscriptionstatus ? get_string('subscribed', 'mod_forum') : get_string('notsubscribed', 'mod_forum'); } if ($subscriptionstatus) { $output = $OUTPUT->pix_icon('t/subscribed', get_string('clicktounsubscribe', 'forum'), 'mod_forum'); if ($includetext) { $output .= get_string('subscribed', 'mod_forum'); } return html_writer::link($subscriptionlink, $output, array( 'title' => get_string('clicktounsubscribe', 'forum'), 'class' => 'discussiontoggle btn btn-link', 'data-forumid' => $forum->id, 'data-discussionid' => $discussionid, 'data-includetext' => $includetext, )); } else { $output = $OUTPUT->pix_icon('t/unsubscribed', get_string('clicktosubscribe', 'forum'), 'mod_forum'); if ($includetext) { $output .= get_string('notsubscribed', 'mod_forum'); } return html_writer::link($subscriptionlink, $output, array( 'title' => get_string('clicktosubscribe', 'forum'), 'class' => 'discussiontoggle btn btn-link', 'data-forumid' => $forum->id, 'data-discussionid' => $discussionid, 'data-includetext' => $includetext, )); } } /** * Return a pair of spans containing classes to allow the subscribe and * unsubscribe icons to be pre-loaded by a browser. * * @return string The generated markup */ function forum_get_discussion_subscription_icon_preloaders() { $o = ''; $o .= html_writer::span(' ', 'preload-subscribe'); $o .= html_writer::span(' ', 'preload-unsubscribe'); return $o; } /** * Print the drop down that allows the user to select how they want to have * the discussion displayed. * * @param int $id forum id if $forumtype is 'single', * discussion id for any other forum type * @param mixed $mode forum layout mode * @param string $forumtype optional */ function forum_print_mode_form($id, $mode, $forumtype='') { global $OUTPUT; $useexperimentalui = get_user_preferences('forum_useexperimentalui', false); if ($forumtype == 'single') { $select = new single_select( new moodle_url("/mod/forum/view.php", array('f' => $id)), 'mode', forum_get_layout_modes($useexperimentalui), $mode, null, "mode" ); $select->set_label(get_string('displaymode', 'forum'), array('class' => 'accesshide')); $select->class = "forummode"; } else { $select = new single_select( new moodle_url("/mod/forum/discuss.php", array('d' => $id)), 'mode', forum_get_layout_modes($useexperimentalui), $mode, null, "mode" ); $select->set_label(get_string('displaymode', 'forum'), array('class' => 'accesshide')); } echo $OUTPUT->render($select); } /** * @global object * @param object $course * @param string $search * @return string */ function forum_search_form($course, $search='') { global $CFG, $PAGE; $forumsearch = new \mod_forum\output\quick_search_form($course->id, $search); $output = $PAGE->get_renderer('mod_forum'); return $output->render($forumsearch); } /** * Retrieve HTML for the page action * * @param forum_entity|null $forum The forum entity. * @param mixed $groupid false if groups not used, int if groups used, 0 means all groups * @param stdClass $course The course object. * @param string $search The search string. * @return string rendered HTML string. */ function forum_activity_actionbar(?forum_entity $forum, $groupid, stdClass $course, string $search=''): string { global $PAGE; $actionbar = new mod_forum\output\forum_actionbar($forum, $course, $groupid, $search); $output = $PAGE->get_renderer('mod_forum'); return $output->render($actionbar); } /** * @global object * @global object */ function forum_set_return() { global $CFG, $SESSION; if (! isset($SESSION->fromdiscussion)) { $referer = get_local_referer(false); // If the referer is NOT a login screen then save it. if (! strncasecmp("$CFG->wwwroot/login", $referer, 300)) { $SESSION->fromdiscussion = $referer; } } } /** * @global object * @param string|\moodle_url $default * @return string */ function forum_go_back_to($default) { global $SESSION; if (!empty($SESSION->fromdiscussion)) { $returnto = $SESSION->fromdiscussion; unset($SESSION->fromdiscussion); return $returnto; } else { return $default; } } /** * Given a discussion object that is being moved to $forumto, * this function checks all posts in that discussion * for attachments, and if any are found, these are * moved to the new forum directory. * * @global object * @param object $discussion * @param int $forumfrom source forum id * @param int $forumto target forum id * @return bool success */ function forum_move_attachments($discussion, $forumfrom, $forumto) { global $DB; $fs = get_file_storage(); $newcm = get_coursemodule_from_instance('forum', $forumto); $oldcm = get_coursemodule_from_instance('forum', $forumfrom); $newcontext = context_module::instance($newcm->id); $oldcontext = context_module::instance($oldcm->id); // loop through all posts, better not use attachment flag ;-) if ($posts = $DB->get_records('forum_posts', array('discussion'=>$discussion->id), '', 'id, attachment')) { foreach ($posts as $post) { $fs->move_area_files_to_new_context($oldcontext->id, $newcontext->id, 'mod_forum', 'post', $post->id); $attachmentsmoved = $fs->move_area_files_to_new_context($oldcontext->id, $newcontext->id, 'mod_forum', 'attachment', $post->id); if ($attachmentsmoved > 0 && $post->attachment != '1') { // Weird - let's fix it $post->attachment = '1'; $DB->update_record('forum_posts', $post); } else if ($attachmentsmoved == 0 && $post->attachment != '') { // Weird - let's fix it $post->attachment = ''; $DB->update_record('forum_posts', $post); } } } return true; } /** * Returns attachments as formated text/html optionally with separate images * * @global object * @global object * @global object * @param object $post * @param object $cm * @param string $type html/text/separateimages * @return mixed string or array of (html text withouth images and image HTML) */ function forum_print_attachments($post, $cm, $type) { global $CFG, $DB, $USER, $OUTPUT; if (empty($post->attachment)) { return $type !== 'separateimages' ? '' : array('', ''); } if (!in_array($type, array('separateimages', 'html', 'text'))) { return $type !== 'separateimages' ? '' : array('', ''); } if (!$context = context_module::instance($cm->id)) { return $type !== 'separateimages' ? '' : array('', ''); } $strattachment = get_string('attachment', 'forum'); $fs = get_file_storage(); $imagereturn = ''; $output = ''; $canexport = !empty($CFG->enableportfolios) && (has_capability('mod/forum:exportpost', $context) || ($post->userid == $USER->id && has_capability('mod/forum:exportownpost', $context))); if ($canexport) { require_once($CFG->libdir.'/portfoliolib.php'); } // We retrieve all files according to the time that they were created. In the case that several files were uploaded // at the sametime (e.g. in the case of drag/drop upload) we revert to using the filename. $files = $fs->get_area_files($context->id, 'mod_forum', 'attachment', $post->id, "filename", false); if ($files) { if ($canexport) { $button = new portfolio_add_button(); } foreach ($files as $file) { $filename = $file->get_filename(); $mimetype = $file->get_mimetype(); $iconimage = $OUTPUT->pix_icon(file_file_icon($file), get_mimetype_description($file), 'moodle', [ 'class' => 'icon', 'style' => 'max-width: 24px; max-height: 24px; vertical-align: middle;', ] ); $path = file_encode_url($CFG->wwwroot.'/pluginfile.php', '/'.$context->id.'/mod_forum/attachment/'.$post->id.'/'.$filename); if ($type == 'html') { $output .= "$iconimage "; $output .= "".s($filename).""; if ($canexport) { $button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id, 'attachment' => $file->get_id()), 'mod_forum'); $button->set_format_by_file($file); $output .= $button->to_html(PORTFOLIO_ADD_ICON_LINK); } $output .= "
"; } else if ($type == 'text') { $output .= "$strattachment ".s($filename).":\n$path\n"; } else { //'returnimages' if (in_array($mimetype, array('image/gif', 'image/jpeg', 'image/png'))) { // Image attachments don't get printed as links $imagereturn .= "
\"\""; if ($canexport) { $button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id, 'attachment' => $file->get_id()), 'mod_forum'); $button->set_format_by_file($file); $imagereturn .= $button->to_html(PORTFOLIO_ADD_ICON_LINK); } } else { $output .= "$iconimage "; $output .= format_text("".s($filename)."", FORMAT_HTML, array('context'=>$context)); if ($canexport) { $button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id, 'attachment' => $file->get_id()), 'mod_forum'); $button->set_format_by_file($file); $output .= $button->to_html(PORTFOLIO_ADD_ICON_LINK); } $output .= '
'; } } if (!empty($CFG->enableplagiarism)) { require_once($CFG->libdir.'/plagiarismlib.php'); $output .= plagiarism_get_links(array('userid' => $post->userid, 'file' => $file, 'cmid' => $cm->id, 'course' => $cm->course, 'forum' => $cm->instance)); $output .= '
'; } } } if ($type !== 'separateimages') { return $output; } else { return array($output, $imagereturn); } } //////////////////////////////////////////////////////////////////////////////// // File API // //////////////////////////////////////////////////////////////////////////////// /** * Lists all browsable file areas * * @package mod_forum * @category files * @param stdClass $course course object * @param stdClass $cm course module object * @param stdClass $context context object * @return array */ function forum_get_file_areas($course, $cm, $context) { return array( 'attachment' => get_string('areaattachment', 'mod_forum'), 'post' => get_string('areapost', 'mod_forum'), ); } /** * File browsing support for forum module. * * @package mod_forum * @category files * @param stdClass $browser file browser object * @param stdClass $areas file areas * @param stdClass $course course object * @param stdClass $cm course module * @param stdClass $context context module * @param string $filearea file area * @param int $itemid item ID * @param string $filepath file path * @param string $filename file name * @return file_info instance or null if not found */ function forum_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) { global $CFG, $DB, $USER; if ($context->contextlevel != CONTEXT_MODULE) { return null; } // filearea must contain a real area if (!isset($areas[$filearea])) { return null; } // Note that forum_user_can_see_post() additionally allows access for parent roles // and it explicitly checks qanda forum type, too. One day, when we stop requiring // course:managefiles, we will need to extend this. if (!has_capability('mod/forum:viewdiscussion', $context)) { return null; } if (is_null($itemid)) { require_once($CFG->dirroot.'/mod/forum/locallib.php'); return new forum_file_info_container($browser, $course, $cm, $context, $areas, $filearea); } static $cached = array(); // $cached will store last retrieved post, discussion and forum. To make sure that the cache // is cleared between unit tests we check if this is the same session if (!isset($cached['sesskey']) || $cached['sesskey'] != sesskey()) { $cached = array('sesskey' => sesskey()); } if (isset($cached['post']) && $cached['post']->id == $itemid) { $post = $cached['post']; } else if ($post = $DB->get_record('forum_posts', array('id' => $itemid))) { $cached['post'] = $post; } else { return null; } if (isset($cached['discussion']) && $cached['discussion']->id == $post->discussion) { $discussion = $cached['discussion']; } else if ($discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion))) { $cached['discussion'] = $discussion; } else { return null; } if (isset($cached['forum']) && $cached['forum']->id == $cm->instance) { $forum = $cached['forum']; } else if ($forum = $DB->get_record('forum', array('id' => $cm->instance))) { $cached['forum'] = $forum; } else { return null; } $fs = get_file_storage(); $filepath = is_null($filepath) ? '/' : $filepath; $filename = is_null($filename) ? '.' : $filename; if (!($storedfile = $fs->get_file($context->id, 'mod_forum', $filearea, $itemid, $filepath, $filename))) { return null; } // Checks to see if the user can manage files or is the owner. // TODO MDL-33805 - Do not use userid here and move the capability check above. if (!has_capability('moodle/course:managefiles', $context) && $storedfile->get_userid() != $USER->id) { return null; } // Make sure groups allow this user to see this file if ($discussion->groupid > 0 && !has_capability('moodle/site:accessallgroups', $context)) { $groupmode = groups_get_activity_groupmode($cm, $course); if ($groupmode == SEPARATEGROUPS && !groups_is_member($discussion->groupid)) { return null; } } // Make sure we're allowed to see it... if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) { return null; } $urlbase = $CFG->wwwroot.'/pluginfile.php'; return new file_info_stored($browser, $context, $storedfile, $urlbase, $itemid, true, true, false, false); } /** * Serves the forum attachments. Implements needed access control ;-) * * @package mod_forum * @category files * @param stdClass $course course object * @param stdClass $cm course module object * @param stdClass $context context object * @param string $filearea file area * @param array $args extra arguments * @param bool $forcedownload whether or not force download * @param array $options additional options affecting the file serving * @return bool false if file not found, does not return if found - justsend the file */ function forum_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) { global $CFG, $DB; if ($context->contextlevel != CONTEXT_MODULE) { return false; } require_course_login($course, true, $cm); $areas = forum_get_file_areas($course, $cm, $context); // filearea must contain a real area if (!isset($areas[$filearea])) { return false; } $postid = (int)array_shift($args); if (!$post = $DB->get_record('forum_posts', array('id'=>$postid))) { return false; } if (!$discussion = $DB->get_record('forum_discussions', array('id'=>$post->discussion))) { return false; } if (!$forum = $DB->get_record('forum', array('id'=>$cm->instance))) { return false; } $fs = get_file_storage(); $relativepath = implode('/', $args); $fullpath = "/$context->id/mod_forum/$filearea/$postid/$relativepath"; if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { return false; } // Make sure groups allow this user to see this file if ($discussion->groupid > 0) { $groupmode = groups_get_activity_groupmode($cm, $course); if ($groupmode == SEPARATEGROUPS) { if (!groups_is_member($discussion->groupid) and !has_capability('moodle/site:accessallgroups', $context)) { return false; } } } // Make sure we're allowed to see it... if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) { return false; } // finally send the file send_stored_file($file, 0, 0, true, $options); // download MUST be forced - security! } /** * If successful, this function returns the name of the file * * @global object * @param object $post is a full post record, including course and forum * @param object $forum * @param object $cm * @param mixed $mform * @param string $unused * @return bool */ function forum_add_attachment($post, $forum, $cm, $mform=null, $unused=null) { global $DB; if (empty($mform)) { return false; } if (empty($post->attachments)) { return true; // Nothing to do } $context = context_module::instance($cm->id); $info = file_get_draft_area_info($post->attachments); $present = ($info['filecount']>0) ? '1' : ''; file_save_draft_area_files($post->attachments, $context->id, 'mod_forum', 'attachment', $post->id, mod_forum_post_form::attachment_options($forum)); $DB->set_field('forum_posts', 'attachment', $present, array('id'=>$post->id)); return true; } /** * Add a new post in an existing discussion. * * @param stdClass $post The post data * @param mixed $mform The submitted form * @param string $unused * @return int */ function forum_add_new_post($post, $mform, $unused = null) { global $USER, $DB; $discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion)); $forum = $DB->get_record('forum', array('id' => $discussion->forum)); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = context_module::instance($cm->id); $privatereplyto = 0; // Check whether private replies should be enabled for this post. if ($post->parent) { $parent = $DB->get_record('forum_posts', array('id' => $post->parent)); if (!empty($parent->privatereplyto)) { throw new \coding_exception('It should not be possible to reply to a private reply'); } if (!empty($post->isprivatereply) && forum_user_can_reply_privately($context, $parent)) { $privatereplyto = $parent->userid; } } $post->created = $post->modified = time(); $post->mailed = FORUM_MAILED_PENDING; $post->userid = $USER->id; $post->privatereplyto = $privatereplyto; $post->attachment = ""; if (!isset($post->totalscore)) { $post->totalscore = 0; } if (!isset($post->mailnow)) { $post->mailnow = 0; } \mod_forum\local\entities\post::add_message_counts($post); $post->id = $DB->insert_record("forum_posts", $post); $post->message = file_save_draft_area_files($post->itemid, $context->id, 'mod_forum', 'post', $post->id, mod_forum_post_form::editor_options($context, null), $post->message); $DB->set_field('forum_posts', 'message', $post->message, array('id'=>$post->id)); forum_add_attachment($post, $forum, $cm, $mform); // Update discussion modified date $DB->set_field("forum_discussions", "timemodified", $post->modified, array("id" => $post->discussion)); $DB->set_field("forum_discussions", "usermodified", $post->userid, array("id" => $post->discussion)); if (forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) { forum_tp_mark_post_read($post->userid, $post); } if (isset($post->tags)) { core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, $post->tags); } // Let Moodle know that assessable content is uploaded (eg for plagiarism detection) forum_trigger_content_uploaded_event($post, $cm, 'forum_add_new_post'); return $post->id; } /** * Trigger post updated event. * * @param object $post forum post object * @param object $discussion discussion object * @param object $context forum context object * @param object $forum forum object * @since Moodle 3.8 * @return void */ function forum_trigger_post_updated_event($post, $discussion, $context, $forum) { global $USER; $params = array( 'context' => $context, 'objectid' => $post->id, 'other' => array( 'discussionid' => $discussion->id, 'forumid' => $forum->id, 'forumtype' => $forum->type, ) ); if ($USER->id !== $post->userid) { $params['relateduserid'] = $post->userid; } $event = \mod_forum\event\post_updated::create($params); $event->add_record_snapshot('forum_discussions', $discussion); $event->trigger(); } /** * Update a post. * * @param stdClass $newpost The post to update * @param mixed $mform The submitted form * @param string $unused * @return bool */ function forum_update_post($newpost, $mform, $unused = null) { global $DB, $USER; $post = $DB->get_record('forum_posts', array('id' => $newpost->id)); $discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion)); $forum = $DB->get_record('forum', array('id' => $discussion->forum)); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = context_module::instance($cm->id); // Allowed modifiable fields. $modifiablefields = [ 'subject', 'message', 'messageformat', 'messagetrust', 'timestart', 'timeend', 'pinned', 'attachments', ]; foreach ($modifiablefields as $field) { if (isset($newpost->{$field})) { $post->{$field} = $newpost->{$field}; } } $post->modified = time(); if (!$post->parent) { // Post is a discussion starter - update discussion title and times too $discussion->name = $post->subject; $discussion->timestart = $post->timestart; $discussion->timeend = $post->timeend; if (isset($post->pinned)) { $discussion->pinned = $post->pinned; } } $post->message = file_save_draft_area_files($newpost->itemid, $context->id, 'mod_forum', 'post', $post->id, mod_forum_post_form::editor_options($context, $post->id), $post->message); \mod_forum\local\entities\post::add_message_counts($post); $DB->update_record('forum_posts', $post); // Note: Discussion modified time/user are intentionally not updated, to enable them to track the latest new post. $DB->update_record('forum_discussions', $discussion); forum_add_attachment($post, $forum, $cm, $mform); if ($forum->type == 'single' && $post->parent == '0') { // Updating first post of single discussion type -> updating forum intro. $forum->intro = $post->message; $forum->timemodified = time(); $DB->update_record("forum", $forum); } if (isset($newpost->tags)) { core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, $newpost->tags); } if (forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) { forum_tp_mark_post_read($USER->id, $post); } // Let Moodle know that assessable content is uploaded (eg for plagiarism detection) forum_trigger_content_uploaded_event($post, $cm, 'forum_update_post'); return true; } /** * Given an object containing all the necessary data, * create a new discussion and return the id * * @param object $post * @param mixed $mform * @param string $unused * @param int $userid * @return object */ function forum_add_discussion($discussion, $mform=null, $unused=null, $userid=null) { global $USER, $CFG, $DB; $timenow = isset($discussion->timenow) ? $discussion->timenow : time(); if (is_null($userid)) { $userid = $USER->id; } // The first post is stored as a real post, and linked // to from the discuss entry. $forum = $DB->get_record('forum', array('id'=>$discussion->forum)); $cm = get_coursemodule_from_instance('forum', $forum->id); $post = new stdClass(); $post->discussion = 0; $post->parent = 0; $post->privatereplyto = 0; $post->userid = $userid; $post->created = $timenow; $post->modified = $timenow; $post->mailed = FORUM_MAILED_PENDING; $post->subject = $discussion->name; $post->message = $discussion->message; $post->messageformat = $discussion->messageformat; $post->messagetrust = $discussion->messagetrust; $post->attachments = isset($discussion->attachments) ? $discussion->attachments : null; $post->forum = $forum->id; // speedup $post->course = $forum->course; // speedup $post->mailnow = $discussion->mailnow; \mod_forum\local\entities\post::add_message_counts($post); $post->id = $DB->insert_record("forum_posts", $post); // TODO: Fix the calling code so that there always is a $cm when this function is called if (!empty($cm->id) && !empty($discussion->itemid)) { // In "single simple discussions" this may not exist yet $context = context_module::instance($cm->id); $text = file_save_draft_area_files($discussion->itemid, $context->id, 'mod_forum', 'post', $post->id, mod_forum_post_form::editor_options($context, null), $post->message); $DB->set_field('forum_posts', 'message', $text, array('id'=>$post->id)); } // Now do the main entry for the discussion, linking to this first post $discussion->firstpost = $post->id; $discussion->timemodified = $timenow; $discussion->usermodified = $post->userid; $discussion->userid = $userid; $discussion->assessed = 0; $post->discussion = $DB->insert_record("forum_discussions", $discussion); // Finally, set the pointer on the post. $DB->set_field("forum_posts", "discussion", $post->discussion, array("id"=>$post->id)); if (!empty($cm->id)) { forum_add_attachment($post, $forum, $cm, $mform, $unused); } if (isset($discussion->tags)) { $tags = is_array($discussion->tags) ? $discussion->tags : explode(',', $discussion->tags); core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, context_module::instance($cm->id), $tags); } if (forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) { forum_tp_mark_post_read($post->userid, $post); } // Let Moodle know that assessable content is uploaded (eg for plagiarism detection) if (!empty($cm->id)) { forum_trigger_content_uploaded_event($post, $cm, 'forum_add_discussion'); } // Clear the discussion count cache just in case it's in the same request. \cache_helper::purge_by_event('changesinforumdiscussions'); return $post->discussion; } /** * Deletes a discussion and handles all associated cleanup. * * @global object * @param object $discussion Discussion to delete * @param bool $fulldelete True when deleting entire forum * @param object $course Course * @param object $cm Course-module * @param object $forum Forum * @return bool */ function forum_delete_discussion($discussion, $fulldelete, $course, $cm, $forum) { global $DB, $CFG; require_once($CFG->libdir.'/completionlib.php'); $result = true; if ($posts = $DB->get_records("forum_posts", array("discussion" => $discussion->id))) { foreach ($posts as $post) { $post->course = $discussion->course; $post->forum = $discussion->forum; if (!forum_delete_post($post, 'ignore', $course, $cm, $forum, $fulldelete)) { $result = false; } } } forum_tp_delete_read_records(-1, -1, $discussion->id); // Discussion subscriptions must be removed before discussions because of key constraints. $DB->delete_records('forum_discussion_subs', array('discussion' => $discussion->id)); if (!$DB->delete_records("forum_discussions", array("id" => $discussion->id))) { $result = false; } // Update completion state if we are tracking completion based on number of posts // But don't bother when deleting whole thing if (!$fulldelete) { $completion = new completion_info($course); if ($completion->is_enabled($cm) == COMPLETION_TRACKING_AUTOMATIC && ($forum->completiondiscussions || $forum->completionreplies || $forum->completionposts)) { $completion->update_state($cm, COMPLETION_INCOMPLETE, $discussion->userid); } } $params = array( 'objectid' => $discussion->id, 'context' => context_module::instance($cm->id), 'other' => array( 'forumid' => $forum->id, ) ); $event = \mod_forum\event\discussion_deleted::create($params); $event->add_record_snapshot('forum_discussions', $discussion); $event->trigger(); // Clear the discussion count cache just in case it's in the same request. \cache_helper::purge_by_event('changesinforumdiscussions'); return $result; } /** * Deletes a single forum post. * * @global object * @param object $post Forum post object * @param mixed $children Whether to delete children. If false, returns false * if there are any children (without deleting the post). If true, * recursively deletes all children. If set to special value 'ignore', deletes * post regardless of children (this is for use only when deleting all posts * in a disussion). * @param object $course Course * @param object $cm Course-module * @param object $forum Forum * @param bool $skipcompletion True to skip updating completion state if it * would otherwise be updated, i.e. when deleting entire forum anyway. * @return bool */ function forum_delete_post($post, $children, $course, $cm, $forum, $skipcompletion=false) { global $DB, $CFG, $USER; require_once($CFG->libdir.'/completionlib.php'); $context = context_module::instance($cm->id); if ($children !== 'ignore' && ($childposts = $DB->get_records('forum_posts', array('parent'=>$post->id)))) { if ($children) { foreach ($childposts as $childpost) { forum_delete_post($childpost, true, $course, $cm, $forum, $skipcompletion); } } else { return false; } } // Delete ratings. require_once($CFG->dirroot.'/rating/lib.php'); $delopt = new stdClass; $delopt->contextid = $context->id; $delopt->component = 'mod_forum'; $delopt->ratingarea = 'post'; $delopt->itemid = $post->id; $rm = new rating_manager(); $rm->delete_ratings($delopt); // Delete attachments. $fs = get_file_storage(); $fs->delete_area_files($context->id, 'mod_forum', 'attachment', $post->id); $fs->delete_area_files($context->id, 'mod_forum', 'post', $post->id); // Delete cached RSS feeds. if (!empty($CFG->enablerssfeeds)) { require_once($CFG->dirroot.'/mod/forum/rsslib.php'); forum_rss_delete_file($forum); } if ($DB->delete_records("forum_posts", array("id" => $post->id))) { forum_tp_delete_read_records(-1, $post->id); // Just in case we are deleting the last post forum_discussion_update_last_post($post->discussion); // Update completion state if we are tracking completion based on number of posts // But don't bother when deleting whole thing if (!$skipcompletion) { $completion = new completion_info($course); if ($completion->is_enabled($cm) == COMPLETION_TRACKING_AUTOMATIC && ($forum->completiondiscussions || $forum->completionreplies || $forum->completionposts)) { $completion->update_state($cm, COMPLETION_INCOMPLETE, $post->userid); } } $params = array( 'context' => $context, 'objectid' => $post->id, 'other' => array( 'discussionid' => $post->discussion, 'forumid' => $forum->id, 'forumtype' => $forum->type, ) ); $post->deleted = 1; if ($post->userid !== $USER->id) { $params['relateduserid'] = $post->userid; } $event = \mod_forum\event\post_deleted::create($params); $event->add_record_snapshot('forum_posts', $post); $event->trigger(); return true; } return false; } /** * Sends post content to plagiarism plugin * @param object $post Forum post object * @param object $cm Course-module * @param string $name * @return bool */ function forum_trigger_content_uploaded_event($post, $cm, $name) { $context = context_module::instance($cm->id); $fs = get_file_storage(); $files = $fs->get_area_files($context->id, 'mod_forum', 'attachment', $post->id, "timemodified", false); $params = array( 'context' => $context, 'objectid' => $post->id, 'other' => array( 'content' => $post->message, 'pathnamehashes' => array_keys($files), 'discussionid' => $post->discussion, 'triggeredfrom' => $name, ) ); $event = \mod_forum\event\assessable_uploaded::create($params); $event->trigger(); return true; } /** * Given a new post, subscribes or unsubscribes as appropriate. * Returns some text which describes what happened. * * @param object $fromform The submitted form * @param stdClass $forum The forum record * @param stdClass $discussion The forum discussion record * @return string */ function forum_post_subscription($fromform, $forum, $discussion) { global $USER; if (\mod_forum\subscriptions::is_forcesubscribed($forum)) { return ""; } else if (\mod_forum\subscriptions::subscription_disabled($forum)) { $subscribed = \mod_forum\subscriptions::is_subscribed($USER->id, $forum); if ($subscribed && !has_capability('moodle/course:manageactivities', context_course::instance($forum->course), $USER->id)) { // This user should not be subscribed to the forum. \mod_forum\subscriptions::unsubscribe_user($USER->id, $forum); } return ""; } $info = new stdClass(); $info->name = fullname($USER); $info->discussion = format_string($discussion->name); $info->forum = format_string($forum->name); if (isset($fromform->discussionsubscribe) && $fromform->discussionsubscribe) { if ($result = \mod_forum\subscriptions::subscribe_user_to_discussion($USER->id, $discussion)) { return html_writer::tag('p', get_string('discussionnowsubscribed', 'forum', $info)); } } else { if ($result = \mod_forum\subscriptions::unsubscribe_user_from_discussion($USER->id, $discussion)) { return html_writer::tag('p', get_string('discussionnownotsubscribed', 'forum', $info)); } } return ''; } /** * Generate and return the subscribe or unsubscribe link for a forum. * * @param object $forum the forum. Fields used are $forum->id and $forum->forcesubscribe. * @param object $context the context object for this forum. * @param array $messages text used for the link in its various states * (subscribed, unsubscribed, forcesubscribed or cantsubscribe). * Any strings not passed in are taken from the $defaultmessages array * at the top of the function. * @param bool $cantaccessagroup * @param bool $unused1 * @param bool $backtoindex * @param array $unused2 * @return string */ function forum_get_subscribe_link($forum, $context, $messages = array(), $cantaccessagroup = false, $unused1 = true, $backtoindex = false, $unused2 = null) { global $CFG, $USER, $PAGE, $OUTPUT; $defaultmessages = array( 'subscribed' => get_string('unsubscribe', 'forum'), 'unsubscribed' => get_string('subscribe', 'forum'), 'cantaccessgroup' => get_string('no'), 'forcesubscribed' => get_string('everyoneissubscribed', 'forum'), 'cantsubscribe' => get_string('disallowsubscribe','forum') ); $messages = $messages + $defaultmessages; if (\mod_forum\subscriptions::is_forcesubscribed($forum)) { return $messages['forcesubscribed']; } else if (\mod_forum\subscriptions::subscription_disabled($forum) && !has_capability('mod/forum:managesubscriptions', $context)) { return $messages['cantsubscribe']; } else if ($cantaccessagroup) { return $messages['cantaccessgroup']; } else { if (!is_enrolled($context, $USER, '', true)) { return ''; } $subscribed = \mod_forum\subscriptions::is_subscribed($USER->id, $forum); if ($subscribed) { $linktext = $messages['subscribed']; $linktitle = get_string('subscribestop', 'forum'); } else { $linktext = $messages['unsubscribed']; $linktitle = get_string('subscribestart', 'forum'); } $options = array(); if ($backtoindex) { $backtoindexlink = '&backtoindex=1'; $options['backtoindex'] = 1; } else { $backtoindexlink = ''; } $options['id'] = $forum->id; $options['sesskey'] = sesskey(); $url = new moodle_url('/mod/forum/subscribe.php', $options); return $OUTPUT->single_button($url, $linktext, 'get', array('title' => $linktitle)); } } /** * Returns true if user created new discussion already. * * @param int $forumid The forum to check for postings * @param int $userid The user to check for postings * @param int $groupid The group to restrict the check to * @return bool */ function forum_user_has_posted_discussion($forumid, $userid, $groupid = null) { global $CFG, $DB; $sql = "SELECT 'x' FROM {forum_discussions} d, {forum_posts} p WHERE d.forum = ? AND p.discussion = d.id AND p.parent = 0 AND p.userid = ?"; $params = [$forumid, $userid]; if ($groupid) { $sql .= " AND d.groupid = ?"; $params[] = $groupid; } return $DB->record_exists_sql($sql, $params); } /** * @global object * @global object * @param int $forumid * @param int $userid * @return array */ function forum_discussions_user_has_posted_in($forumid, $userid) { global $CFG, $DB; $haspostedsql = "SELECT d.id AS id, d.* FROM {forum_posts} p, {forum_discussions} d WHERE p.discussion = d.id AND d.forum = ? AND p.userid = ?"; return $DB->get_records_sql($haspostedsql, array($forumid, $userid)); } /** * @global object * @global object * @param int $forumid * @param int $did * @param int $userid * @return bool */ function forum_user_has_posted($forumid, $did, $userid) { global $DB; if (empty($did)) { // posted in any forum discussion? $sql = "SELECT 'x' FROM {forum_posts} p JOIN {forum_discussions} d ON d.id = p.discussion WHERE p.userid = :userid AND d.forum = :forumid"; return $DB->record_exists_sql($sql, array('forumid'=>$forumid,'userid'=>$userid)); } else { return $DB->record_exists('forum_posts', array('discussion'=>$did,'userid'=>$userid)); } } /** * Returns true if user posted with mailnow in given discussion * @param int $did Discussion id * @param int $userid User id * @return bool */ function forum_get_user_posted_mailnow(int $did, int $userid): bool { global $DB; $postmailnow = $DB->get_field('forum_posts', 'MAX(mailnow)', ['userid' => $userid, 'discussion' => $did]); return !empty($postmailnow); } /** * Returns creation time of the first user's post in given discussion * @global object $DB * @param int $did Discussion id * @param int $userid User id * @return int|bool post creation time stamp or return false */ function forum_get_user_posted_time($did, $userid) { global $DB; $posttime = $DB->get_field('forum_posts', 'MIN(created)', array('userid'=>$userid, 'discussion'=>$did)); if (empty($posttime)) { return false; } return $posttime; } /** * @global object * @param object $forum * @param object $currentgroup * @param int $unused * @param object $cm * @param object $context * @return bool */ function forum_user_can_post_discussion($forum, $currentgroup=null, $unused=-1, $cm=NULL, $context=NULL) { // $forum is an object global $USER; // shortcut - guest and not-logged-in users can not post if (isguestuser() or !isloggedin()) { return false; } if (!$cm) { debugging('missing cm', DEBUG_DEVELOPER); if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course)) { throw new \moodle_exception('invalidcoursemodule'); } } if (!$context) { $context = context_module::instance($cm->id); } if (forum_is_cutoff_date_reached($forum)) { if (!has_capability('mod/forum:canoverridecutoff', $context)) { return false; } } if ($currentgroup === null) { $currentgroup = groups_get_activity_group($cm); } $groupmode = groups_get_activity_groupmode($cm); if ($forum->type == 'news') { $capname = 'mod/forum:addnews'; } else if ($forum->type == 'qanda') { $capname = 'mod/forum:addquestion'; } else { $capname = 'mod/forum:startdiscussion'; } if (!has_capability($capname, $context)) { return false; } if ($forum->type == 'single') { return false; } if ($forum->type == 'eachuser') { if (forum_user_has_posted_discussion($forum->id, $USER->id, $currentgroup)) { return false; } } if (!$groupmode or has_capability('moodle/site:accessallgroups', $context)) { return true; } if ($currentgroup) { return groups_is_member($currentgroup); } else { // no group membership and no accessallgroups means no new discussions // reverted to 1.7 behaviour in 1.9+, buggy in 1.8.0-1.9.0 return false; } } /** * This function checks whether the user can reply to posts in a forum * discussion. Use forum_user_can_post_discussion() to check whether the user * can start discussions. * * @global object * @global object * @uses DEBUG_DEVELOPER * @uses CONTEXT_MODULE * @uses VISIBLEGROUPS * @param object $forum forum object * @param object $discussion * @param object $user * @param object $cm * @param object $course * @param object $context * @return bool */ function forum_user_can_post($forum, $discussion, $user=NULL, $cm=NULL, $course=NULL, $context=NULL) { global $USER, $DB; if (empty($user)) { $user = $USER; } // shortcut - guest and not-logged-in users can not post if (isguestuser($user) or empty($user->id)) { return false; } if (!isset($discussion->groupid)) { debugging('incorrect discussion parameter', DEBUG_DEVELOPER); return false; } if (!$cm) { debugging('missing cm', DEBUG_DEVELOPER); if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course)) { throw new \moodle_exception('invalidcoursemodule'); } } if (!$course) { debugging('missing course', DEBUG_DEVELOPER); if (!$course = $DB->get_record('course', array('id' => $forum->course))) { throw new \moodle_exception('invalidcourseid'); } } if (!$context) { $context = context_module::instance($cm->id); } if (forum_is_cutoff_date_reached($forum)) { if (!has_capability('mod/forum:canoverridecutoff', $context)) { return false; } } // Check whether the discussion is locked. if (forum_discussion_is_locked($forum, $discussion)) { if (!has_capability('mod/forum:canoverridediscussionlock', $context)) { return false; } } // normal users with temporary guest access can not post, suspended users can not post either if (!is_viewing($context, $user->id) and !is_enrolled($context, $user->id, '', true)) { return false; } if ($forum->type == 'news') { $capname = 'mod/forum:replynews'; } else { $capname = 'mod/forum:replypost'; } if (!has_capability($capname, $context, $user->id)) { return false; } if (!$groupmode = groups_get_activity_groupmode($cm, $course)) { return true; } if (has_capability('moodle/site:accessallgroups', $context)) { return true; } if ($groupmode == VISIBLEGROUPS) { if ($discussion->groupid == -1) { // allow students to reply to all participants discussions - this was not possible in Moodle <1.8 return true; } return groups_is_member($discussion->groupid); } else { //separate groups if ($discussion->groupid == -1) { return false; } return groups_is_member($discussion->groupid); } } /** * Check to ensure a user can view a timed discussion. * * @param object $discussion * @param object $user * @param object $context * @return boolean returns true if they can view post, false otherwise */ function forum_user_can_see_timed_discussion($discussion, $user, $context) { global $CFG; // Check that the user can view a discussion that is normally hidden due to access times. if (!empty($CFG->forum_enabletimedposts)) { $time = time(); if (($discussion->timestart != 0 && $discussion->timestart > $time) || ($discussion->timeend != 0 && $discussion->timeend < $time)) { if (!has_capability('mod/forum:viewhiddentimedposts', $context, $user->id)) { return false; } } } return true; } /** * Check to ensure a user can view a group discussion. * * @param object $discussion * @param object $cm * @param object $context * @return boolean returns true if they can view post, false otherwise */ function forum_user_can_see_group_discussion($discussion, $cm, $context) { // If it's a grouped discussion, make sure the user is a member. if ($discussion->groupid > 0) { $groupmode = groups_get_activity_groupmode($cm); if ($groupmode == SEPARATEGROUPS) { return groups_is_member($discussion->groupid) || has_capability('moodle/site:accessallgroups', $context); } } return true; } /** * @global object * @global object * @uses DEBUG_DEVELOPER * @param object $forum * @param object $discussion * @param object $context * @param object $user * @return bool */ function forum_user_can_see_discussion($forum, $discussion, $context, $user=NULL) { global $USER, $DB; if (empty($user) || empty($user->id)) { $user = $USER; } // retrieve objects (yuk) if (is_numeric($forum)) { debugging('missing full forum', DEBUG_DEVELOPER); if (!$forum = $DB->get_record('forum',array('id'=>$forum))) { return false; } } if (is_numeric($discussion)) { debugging('missing full discussion', DEBUG_DEVELOPER); if (!$discussion = $DB->get_record('forum_discussions',array('id'=>$discussion))) { return false; } } if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course)) { throw new \moodle_exception('invalidcoursemodule'); } if (!has_capability('mod/forum:viewdiscussion', $context)) { return false; } if (!forum_user_can_see_timed_discussion($discussion, $user, $context)) { return false; } if (!forum_user_can_see_group_discussion($discussion, $cm, $context)) { return false; } return true; } /** * Check whether a user can see the specified post. * * @param \stdClass $forum The forum to chcek * @param \stdClass $discussion The discussion the post is in * @param \stdClass $post The post in question * @param \stdClass $user The user to test - if not specified, the current user is checked. * @param \stdClass $cm The Course Module that the forum is in (required). * @param bool $checkdeleted Whether to check the deleted flag on the post. * @return bool */ function forum_user_can_see_post($forum, $discussion, $post, $user = null, $cm = null, $checkdeleted = true) { global $CFG, $USER, $DB; // retrieve objects (yuk) if (is_numeric($forum)) { debugging('missing full forum', DEBUG_DEVELOPER); if (!$forum = $DB->get_record('forum',array('id'=>$forum))) { return false; } } if (is_numeric($discussion)) { debugging('missing full discussion', DEBUG_DEVELOPER); if (!$discussion = $DB->get_record('forum_discussions',array('id'=>$discussion))) { return false; } } if (is_numeric($post)) { debugging('missing full post', DEBUG_DEVELOPER); if (!$post = $DB->get_record('forum_posts',array('id'=>$post))) { return false; } } if (!isset($post->id) && isset($post->parent)) { $post->id = $post->parent; } if ($checkdeleted && !empty($post->deleted)) { return false; } if (!$cm) { debugging('missing cm', DEBUG_DEVELOPER); if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course)) { throw new \moodle_exception('invalidcoursemodule'); } } // Context used throughout function. $modcontext = context_module::instance($cm->id); if (empty($user) || empty($user->id)) { $user = $USER; } $canviewdiscussion = (isset($cm->cache) && !empty($cm->cache->caps['mod/forum:viewdiscussion'])) || has_capability('mod/forum:viewdiscussion', $modcontext, $user->id); if (!$canviewdiscussion && !has_all_capabilities(array('moodle/user:viewdetails', 'moodle/user:readuserposts'), context_user::instance($post->userid))) { return false; } if (!forum_post_is_visible_privately($post, $cm)) { return false; } if (isset($cm->uservisible)) { if (!$cm->uservisible) { return false; } } else { if (!\core_availability\info_module::is_user_visible($cm, $user->id, false)) { return false; } } if (!forum_user_can_see_timed_discussion($discussion, $user, $modcontext)) { return false; } if (!forum_user_can_see_group_discussion($discussion, $cm, $modcontext)) { return false; } if ($forum->type == 'qanda') { if (has_capability('mod/forum:viewqandawithoutposting', $modcontext, $user->id) || $post->userid == $user->id || (isset($discussion->firstpost) && $discussion->firstpost == $post->id)) { return true; } $firstpost = forum_get_firstpost_from_discussion($discussion->id); if ($firstpost->userid == $user->id) { return true; } $userpostmailnow = forum_get_user_posted_mailnow($discussion->id, $user->id); if ($userpostmailnow) { return true; } $userfirstpost = forum_get_user_posted_time($discussion->id, $user->id); return (($userfirstpost !== false && (time() - $userfirstpost >= $CFG->maxeditingtime))); } return true; } /** * Returns all forum posts since a given time in specified forum. * * @todo Document this functions args * @global object * @global object * @global object * @global object */ function forum_get_recent_mod_activity(&$activities, &$index, $timestart, $courseid, $cmid, $userid=0, $groupid=0) { global $CFG, $COURSE, $USER, $DB; if ($COURSE->id == $courseid) { $course = $COURSE; } else { $course = $DB->get_record('course', array('id' => $courseid)); } $modinfo = get_fast_modinfo($course); $cm = $modinfo->cms[$cmid]; $params = array($timestart, $cm->instance); if ($userid) { $userselect = "AND u.id = ?"; $params[] = $userid; } else { $userselect = ""; } if ($groupid) { $groupselect = "AND d.groupid = ?"; $params[] = $groupid; } else { $groupselect = ""; } $userfieldsapi = \core_user\fields::for_name(); $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects; if (!$posts = $DB->get_records_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid, d.timestart, d.timeend, d.userid AS duserid, $allnames, u.email, u.picture, u.imagealt, u.email FROM {forum_posts} p JOIN {forum_discussions} d ON d.id = p.discussion JOIN {forum} f ON f.id = d.forum JOIN {user} u ON u.id = p.userid WHERE p.created > ? AND f.id = ? $userselect $groupselect ORDER BY p.id ASC", $params)) { // order by initial posting date return; } $groupmode = groups_get_activity_groupmode($cm, $course); $cm_context = context_module::instance($cm->id); $viewhiddentimed = has_capability('mod/forum:viewhiddentimedposts', $cm_context); $accessallgroups = has_capability('moodle/site:accessallgroups', $cm_context); $printposts = array(); foreach ($posts as $post) { if (!empty($CFG->forum_enabletimedposts) and $USER->id != $post->duserid and (($post->timestart > 0 and $post->timestart > time()) or ($post->timeend > 0 and $post->timeend < time()))) { if (!$viewhiddentimed) { continue; } } if ($groupmode) { if ($post->groupid == -1 or $groupmode == VISIBLEGROUPS or $accessallgroups) { // oki (Open discussions have groupid -1) } else { // separate mode if (isguestuser()) { // shortcut continue; } if (!in_array($post->groupid, $modinfo->get_groups($cm->groupingid))) { continue; } } } $printposts[] = $post; } if (!$printposts) { return; } $aname = format_string($cm->name,true); foreach ($printposts as $post) { $tmpactivity = new stdClass(); $tmpactivity->type = 'forum'; $tmpactivity->cmid = $cm->id; $tmpactivity->name = $aname; $tmpactivity->sectionnum = $cm->sectionnum; $tmpactivity->timestamp = $post->modified; $tmpactivity->content = new stdClass(); $tmpactivity->content->id = $post->id; $tmpactivity->content->discussion = $post->discussion; $tmpactivity->content->subject = format_string($post->subject); $tmpactivity->content->parent = $post->parent; $tmpactivity->content->forumtype = $post->forumtype; $tmpactivity->user = new stdClass(); $additionalfields = array('id' => 'userid', 'picture', 'imagealt', 'email'); $additionalfields = explode(',', implode(',', \core_user\fields::get_picture_fields())); $tmpactivity->user = username_load_fields_from_object($tmpactivity->user, $post, null, $additionalfields); $tmpactivity->user->id = $post->userid; $activities[$index++] = $tmpactivity; } return; } /** * Outputs the forum post indicated by $activity. * * @param object $activity the activity object the forum resides in * @param int $courseid the id of the course the forum resides in * @param bool $detail not used, but required for compatibilty with other modules * @param int $modnames not used, but required for compatibilty with other modules * @param bool $viewfullnames not used, but required for compatibilty with other modules */ function forum_print_recent_mod_activity($activity, $courseid, $detail, $modnames, $viewfullnames) { global $OUTPUT; $content = $activity->content; if ($content->parent) { $class = 'reply'; } else { $class = 'discussion'; } $tableoptions = [ 'border' => '0', 'cellpadding' => '3', 'cellspacing' => '0', 'class' => 'forum-recent table-reboot', ]; $output = html_writer::start_tag('table', $tableoptions); $output .= html_writer::start_tag('tr'); $post = (object) ['parent' => $content->parent]; $forum = (object) ['type' => $content->forumtype]; $authorhidden = forum_is_author_hidden($post, $forum); // Show user picture if author should not be hidden. if (!$authorhidden) { $pictureoptions = [ 'courseid' => $courseid, 'link' => $authorhidden, 'alttext' => $authorhidden, ]; $picture = $OUTPUT->user_picture($activity->user, $pictureoptions); $output .= html_writer::tag('td', $picture, ['class' => 'userpicture', 'valign' => 'top']); } // Discussion title and author. $output .= html_writer::start_tag('td', ['class' => $class]); if ($content->parent) { $class = 'title'; } else { // Bold the title of new discussions so they stand out. $class = 'title bold'; } $output .= html_writer::start_div($class); if ($detail) { $aname = s($activity->name); $output .= $OUTPUT->image_icon('monologo', $aname, $activity->type); } $discussionurl = new moodle_url('/mod/forum/discuss.php', ['d' => $content->discussion]); $discussionurl->set_anchor('p' . $activity->content->id); $output .= html_writer::link($discussionurl, $content->subject); $output .= html_writer::end_div(); $timestamp = userdate_htmltime($activity->timestamp); if ($authorhidden) { $authornamedate = $timestamp; } else { $fullname = fullname($activity->user, $viewfullnames); $userurl = new moodle_url('/user/view.php'); $userurl->params(['id' => $activity->user->id, 'course' => $courseid]); $by = new stdClass(); $by->name = html_writer::link($userurl, $fullname); $by->date = $timestamp; $authornamedate = get_string('bynameondate', 'forum', $by); } $output .= html_writer::div($authornamedate, 'user'); $output .= html_writer::end_tag('td'); $output .= html_writer::end_tag('tr'); $output .= html_writer::end_tag('table'); echo $output; } /** * recursively sets the discussion field to $discussionid on $postid and all its children * used when pruning a post * * @global object * @param int $postid * @param int $discussionid * @return bool */ function forum_change_discussionid($postid, $discussionid) { global $DB; $DB->set_field('forum_posts', 'discussion', $discussionid, array('id' => $postid)); if ($posts = $DB->get_records('forum_posts', array('parent' => $postid))) { foreach ($posts as $post) { forum_change_discussionid($post->id, $discussionid); } } return true; } // Functions to do with read tracking. /** * Mark posts as read. * * @global object * @global object * @param object $user object * @param array $postids array of post ids * @return boolean success */ function forum_tp_mark_posts_read($user, $postids) { global $CFG, $DB; if (!forum_tp_can_track_forums(false, $user)) { return true; } $status = true; $now = time(); $cutoffdate = $now - ($CFG->forum_oldpostdays * 24 * 3600); if (empty($postids)) { return true; } else if (count($postids) > 200) { while ($part = array_splice($postids, 0, 200)) { $status = forum_tp_mark_posts_read($user, $part) && $status; } return $status; } list($usql, $postidparams) = $DB->get_in_or_equal($postids, SQL_PARAMS_NAMED, 'postid'); $insertparams = array( 'userid1' => $user->id, 'userid2' => $user->id, 'userid3' => $user->id, 'firstread' => $now, 'lastread' => $now, 'cutoffdate' => $cutoffdate, ); $params = array_merge($postidparams, $insertparams); if ($CFG->forum_allowforcedreadtracking) { $trackingsql = "AND (f.trackingtype = ".FORUM_TRACKING_FORCED." OR (f.trackingtype = ".FORUM_TRACKING_OPTIONAL." AND tf.id IS NULL))"; } else { $trackingsql = "AND ((f.trackingtype = ".FORUM_TRACKING_OPTIONAL." OR f.trackingtype = ".FORUM_TRACKING_FORCED.") AND tf.id IS NULL)"; } // First insert any new entries. $sql = "INSERT INTO {forum_read} (userid, postid, discussionid, forumid, firstread, lastread) SELECT :userid1, p.id, p.discussion, d.forum, :firstread, :lastread FROM {forum_posts} p JOIN {forum_discussions} d ON d.id = p.discussion JOIN {forum} f ON f.id = d.forum LEFT JOIN {forum_track_prefs} tf ON (tf.userid = :userid2 AND tf.forumid = f.id) LEFT JOIN {forum_read} fr ON ( fr.userid = :userid3 AND fr.postid = p.id AND fr.discussionid = d.id AND fr.forumid = f.id ) WHERE p.id $usql AND p.modified >= :cutoffdate $trackingsql AND fr.id IS NULL"; $status = $DB->execute($sql, $params) && $status; // Then update all records. $updateparams = array( 'userid' => $user->id, 'lastread' => $now, ); $params = array_merge($postidparams, $updateparams); $status = $DB->set_field_select('forum_read', 'lastread', $now, ' userid = :userid AND lastread <> :lastread AND postid ' . $usql, $params) && $status; return $status; } /** * Mark post as read. * @global object * @global object * @param int $userid * @param int $postid */ function forum_tp_add_read_record($userid, $postid) { global $CFG, $DB; $now = time(); $cutoffdate = $now - ($CFG->forum_oldpostdays * 24 * 3600); if (!$DB->record_exists('forum_read', array('userid' => $userid, 'postid' => $postid))) { $sql = "INSERT INTO {forum_read} (userid, postid, discussionid, forumid, firstread, lastread) SELECT ?, p.id, p.discussion, d.forum, ?, ? FROM {forum_posts} p JOIN {forum_discussions} d ON d.id = p.discussion WHERE p.id = ? AND p.modified >= ?"; return $DB->execute($sql, array($userid, $now, $now, $postid, $cutoffdate)); } else { $sql = "UPDATE {forum_read} SET lastread = ? WHERE userid = ? AND postid = ?"; return $DB->execute($sql, array($now, $userid, $userid)); } } /** * If its an old post, do nothing. If the record exists, the maintenance will clear it up later. * * @param int $userid The ID of the user to mark posts read for. * @param object $post The post record for the post to mark as read. * @param mixed $unused * @return bool */ function forum_tp_mark_post_read($userid, $post, $unused = null) { if (!forum_tp_is_post_old($post)) { return forum_tp_add_read_record($userid, $post->id); } else { return true; } } /** * Marks a whole forum as read, for a given user * * @global object * @global object * @param object $user * @param int $forumid * @param int|bool $groupid * @return bool */ function forum_tp_mark_forum_read($user, $forumid, $groupid=false) { global $CFG, $DB; $cutoffdate = time() - ($CFG->forum_oldpostdays*24*60*60); $groupsel = ""; $params = array($user->id, $forumid, $cutoffdate); if ($groupid !== false) { $groupsel = " AND (d.groupid = ? OR d.groupid = -1)"; $params[] = $groupid; } $sql = "SELECT p.id FROM {forum_posts} p LEFT JOIN {forum_discussions} d ON d.id = p.discussion LEFT JOIN {forum_read} r ON (r.postid = p.id AND r.userid = ?) WHERE d.forum = ? AND p.modified >= ? AND r.id is NULL $groupsel"; if ($posts = $DB->get_records_sql($sql, $params)) { $postids = array_keys($posts); return forum_tp_mark_posts_read($user, $postids); } return true; } /** * Marks a whole discussion as read, for a given user * * @global object * @global object * @param object $user * @param int $discussionid * @return bool */ function forum_tp_mark_discussion_read($user, $discussionid) { global $CFG, $DB; $cutoffdate = time() - ($CFG->forum_oldpostdays*24*60*60); $sql = "SELECT p.id FROM {forum_posts} p LEFT JOIN {forum_read} r ON (r.postid = p.id AND r.userid = ?) WHERE p.discussion = ? AND p.modified >= ? AND r.id is NULL"; if ($posts = $DB->get_records_sql($sql, array($user->id, $discussionid, $cutoffdate))) { $postids = array_keys($posts); return forum_tp_mark_posts_read($user, $postids); } return true; } /** * @global object * @param int $userid * @param object $post */ function forum_tp_is_post_read($userid, $post) { global $DB; return (forum_tp_is_post_old($post) || $DB->record_exists('forum_read', array('userid' => $userid, 'postid' => $post->id))); } /** * @global object * @param object $post * @param int $time Defautls to time() */ function forum_tp_is_post_old($post, $time=null) { global $CFG; if (is_null($time)) { $time = time(); } return ($post->modified < ($time - ($CFG->forum_oldpostdays * 24 * 3600))); } /** * Returns the count of records for the provided user and course. * Please note that group access is ignored! * * @global object * @global object * @param int $userid * @param int $courseid * @return array */ function forum_tp_get_course_unread_posts($userid, $courseid) { global $CFG, $DB; $modinfo = get_fast_modinfo($courseid); $forumcms = $modinfo->get_instances_of('forum'); if (empty($forumcms)) { // Return early if the course doesn't have any forum. Will save us a DB query. return []; } $now = floor(time() / MINSECS) * MINSECS; // DB cache friendliness. $cutoffdate = $now - ($CFG->forum_oldpostdays * DAYSECS); $params = [ 'privatereplyto' => $userid, 'modified' => $cutoffdate, 'readuserid' => $userid, 'trackprefsuser' => $userid, 'courseid' => $courseid, 'trackforumuser' => $userid, ]; if (!empty($CFG->forum_enabletimedposts)) { $timedsql = "AND d.timestart < :timestart AND (d.timeend = 0 OR d.timeend > :timeend)"; $params['timestart'] = $now; $params['timeend'] = $now; } else { $timedsql = ""; } if ($CFG->forum_allowforcedreadtracking) { $trackingsql = "AND (f.trackingtype = ".FORUM_TRACKING_FORCED." OR (f.trackingtype = ".FORUM_TRACKING_OPTIONAL." AND tf.id IS NULL AND (SELECT trackforums FROM {user} WHERE id = :trackforumuser) = 1))"; } else { $trackingsql = "AND ((f.trackingtype = ".FORUM_TRACKING_OPTIONAL." OR f.trackingtype = ".FORUM_TRACKING_FORCED.") AND tf.id IS NULL AND (SELECT trackforums FROM {user} WHERE id = :trackforumuser) = 1)"; } $sql = "SELECT f.id, COUNT(p.id) AS unread, COUNT(p.privatereply) as privatereplies, COUNT(p.privatereplytouser) as privaterepliestouser FROM ( SELECT id, discussion, CASE WHEN privatereplyto <> 0 THEN 1 END privatereply, CASE WHEN privatereplyto = :privatereplyto THEN 1 END privatereplytouser FROM {forum_posts} WHERE modified >= :modified ) p JOIN {forum_discussions} d ON d.id = p.discussion JOIN {forum} f ON f.id = d.forum JOIN {course} c ON c.id = f.course LEFT JOIN {forum_read} r ON (r.postid = p.id AND r.userid = :readuserid) LEFT JOIN {forum_track_prefs} tf ON (tf.userid = :trackprefsuser AND tf.forumid = f.id) WHERE f.course = :courseid AND r.id is NULL $trackingsql $timedsql GROUP BY f.id"; $results = []; if ($records = $DB->get_records_sql($sql, $params)) { // Loop through each forum instance to check for capability and count the number of unread posts. foreach ($forumcms as $cm) { // Check that the forum instance exists in the query results. if (!isset($records[$cm->instance])) { continue; } $record = $records[$cm->instance]; $unread = $record->unread; // Check if the user has the capability to read private replies for this forum instance. $forumcontext = context_module::instance($cm->id); if (!has_capability('mod/forum:readprivatereplies', $forumcontext, $userid)) { // The real unread count would be the total of unread count minus the number of unread private replies plus // the total unread private replies to the user. $unread = $record->unread - $record->privatereplies + $record->privaterepliestouser; } // Build and add the object to the array of results to be returned. $results[$record->id] = (object)[ 'id' => $record->id, 'unread' => $unread, ]; } } return $results; } /** * Returns the count of records for the provided user and forum and [optionally] group. * * @global object * @global object * @global object * @param object $cm * @param object $course * @param bool $resetreadcache optional, true to reset the function static $readcache var * @return int */ function forum_tp_count_forum_unread_posts($cm, $course, $resetreadcache = false) { global $CFG, $USER, $DB; static $readcache = array(); if ($resetreadcache) { $readcache = array(); } $forumid = $cm->instance; if (!isset($readcache[$course->id])) { $readcache[$course->id] = array(); if ($counts = forum_tp_get_course_unread_posts($USER->id, $course->id)) { foreach ($counts as $count) { $readcache[$course->id][$count->id] = $count->unread; } } } if (empty($readcache[$course->id][$forumid])) { // no need to check group mode ;-) return 0; } $groupmode = groups_get_activity_groupmode($cm, $course); if ($groupmode != SEPARATEGROUPS) { return $readcache[$course->id][$forumid]; } $forumcontext = context_module::instance($cm->id); if (has_any_capability(['moodle/site:accessallgroups', 'mod/forum:readprivatereplies'], $forumcontext)) { return $readcache[$course->id][$forumid]; } require_once($CFG->dirroot.'/course/lib.php'); $modinfo = get_fast_modinfo($course); $mygroups = $modinfo->get_groups($cm->groupingid); // add all groups posts $mygroups[-1] = -1; list ($groupssql, $groupsparams) = $DB->get_in_or_equal($mygroups, SQL_PARAMS_NAMED); $now = floor(time() / MINSECS) * MINSECS; // DB Cache friendliness. $cutoffdate = $now - ($CFG->forum_oldpostdays * DAYSECS); $params = [ 'readuser' => $USER->id, 'forum' => $forumid, 'cutoffdate' => $cutoffdate, 'privatereplyto' => $USER->id, ]; if (!empty($CFG->forum_enabletimedposts)) { $timedsql = "AND d.timestart < :timestart AND (d.timeend = 0 OR d.timeend > :timeend)"; $params['timestart'] = $now; $params['timeend'] = $now; } else { $timedsql = ""; } $params = array_merge($params, $groupsparams); $sql = "SELECT COUNT(p.id) FROM {forum_posts} p JOIN {forum_discussions} d ON p.discussion = d.id LEFT JOIN {forum_read} r ON (r.postid = p.id AND r.userid = :readuser) WHERE d.forum = :forum AND p.modified >= :cutoffdate AND r.id is NULL $timedsql AND d.groupid $groupssql AND (p.privatereplyto = 0 OR p.privatereplyto = :privatereplyto)"; return $DB->get_field_sql($sql, $params); } /** * Deletes read records for the specified index. At least one parameter must be specified. * * @global object * @param int $userid * @param int $postid * @param int $discussionid * @param int $forumid * @return bool */ function forum_tp_delete_read_records($userid=-1, $postid=-1, $discussionid=-1, $forumid=-1) { global $DB; $params = array(); $select = ''; if ($userid > -1) { if ($select != '') $select .= ' AND '; $select .= 'userid = ?'; $params[] = $userid; } if ($postid > -1) { if ($select != '') $select .= ' AND '; $select .= 'postid = ?'; $params[] = $postid; } if ($discussionid > -1) { if ($select != '') $select .= ' AND '; $select .= 'discussionid = ?'; $params[] = $discussionid; } if ($forumid > -1) { if ($select != '') $select .= ' AND '; $select .= 'forumid = ?'; $params[] = $forumid; } if ($select == '') { return false; } else { return $DB->delete_records_select('forum_read', $select, $params); } } /** * Get a list of forums not tracked by the user. * * @global object * @global object * @param int $userid The id of the user to use. * @param int $courseid The id of the course being checked. * @return mixed An array indexed by forum id, or false. */ function forum_tp_get_untracked_forums($userid, $courseid) { global $CFG, $DB; if ($CFG->forum_allowforcedreadtracking) { $trackingsql = "AND (f.trackingtype = ".FORUM_TRACKING_OFF." OR (f.trackingtype = ".FORUM_TRACKING_OPTIONAL." AND (ft.id IS NOT NULL OR (SELECT trackforums FROM {user} WHERE id = ?) = 0)))"; } else { $trackingsql = "AND (f.trackingtype = ".FORUM_TRACKING_OFF." OR ((f.trackingtype = ".FORUM_TRACKING_OPTIONAL." OR f.trackingtype = ".FORUM_TRACKING_FORCED.") AND (ft.id IS NOT NULL OR (SELECT trackforums FROM {user} WHERE id = ?) = 0)))"; } $sql = "SELECT f.id FROM {forum} f LEFT JOIN {forum_track_prefs} ft ON (ft.forumid = f.id AND ft.userid = ?) WHERE f.course = ? $trackingsql"; if ($forums = $DB->get_records_sql($sql, array($userid, $courseid, $userid))) { foreach ($forums as $forum) { $forums[$forum->id] = $forum; } return $forums; } else { return array(); } } /** * Determine if a user can track forums and optionally a particular forum. * Checks the site settings, the user settings and the forum settings (if * requested). * * @global object * @global object * @global object * @param mixed $forum The forum object to test, or the int id (optional). * @param mixed $userid The user object to check for (optional). * @return boolean */ function forum_tp_can_track_forums($forum=false, $user=false) { global $USER, $CFG, $DB; // if possible, avoid expensive // queries if (empty($CFG->forum_trackreadposts)) { return false; } if ($user === false) { $user = $USER; } if (isguestuser($user) or empty($user->id)) { return false; } if ($forum === false) { if ($CFG->forum_allowforcedreadtracking) { // Since we can force tracking, assume yes without a specific forum. return true; } else { return (bool)$user->trackforums; } } // Work toward always passing an object... if (is_numeric($forum)) { debugging('Better use proper forum object.', DEBUG_DEVELOPER); $forum = $DB->get_record('forum', array('id' => $forum), '', 'id,trackingtype'); } $forumallows = ($forum->trackingtype == FORUM_TRACKING_OPTIONAL); $forumforced = ($forum->trackingtype == FORUM_TRACKING_FORCED); if ($CFG->forum_allowforcedreadtracking) { // If we allow forcing, then forced forums takes procidence over user setting. return ($forumforced || ($forumallows && (!empty($user->trackforums) && (bool)$user->trackforums))); } else { // If we don't allow forcing, user setting trumps. return ($forumforced || $forumallows) && !empty($user->trackforums); } } /** * Tells whether a specific forum is tracked by the user. A user can optionally * be specified. If not specified, the current user is assumed. * * @global object * @global object * @global object * @param mixed $forum If int, the id of the forum being checked; if object, the forum object * @param int $userid The id of the user being checked (optional). * @return boolean */ function forum_tp_is_tracked($forum, $user=false) { global $USER, $CFG, $DB; if ($user === false) { $user = $USER; } if (isguestuser($user) or empty($user->id)) { return false; } $cache = cache::make('mod_forum', 'forum_is_tracked'); $forumid = is_numeric($forum) ? $forum : $forum->id; $key = $forumid . '_' . $user->id; if ($cachedvalue = $cache->get($key)) { return $cachedvalue == 'tracked'; } // Work toward always passing an object... if (is_numeric($forum)) { debugging('Better use proper forum object.', DEBUG_DEVELOPER); $forum = $DB->get_record('forum', array('id' => $forum)); } if (!forum_tp_can_track_forums($forum, $user)) { return false; } $forumallows = ($forum->trackingtype == FORUM_TRACKING_OPTIONAL); $forumforced = ($forum->trackingtype == FORUM_TRACKING_FORCED); $userpref = $DB->get_record('forum_track_prefs', array('userid' => $user->id, 'forumid' => $forum->id)); if ($CFG->forum_allowforcedreadtracking) { $istracked = $forumforced || ($forumallows && $userpref === false); } else { $istracked = ($forumallows || $forumforced) && $userpref === false; } // We have to store a string here because the cache API returns false // when it can't find the key which would be confused with our legitimate // false value. *sigh*. $cache->set($key, $istracked ? 'tracked' : 'not'); return $istracked; } /** * @global object * @global object * @param int $forumid * @param int $userid */ function forum_tp_start_tracking($forumid, $userid=false) { global $USER, $DB; if ($userid === false) { $userid = $USER->id; } return $DB->delete_records('forum_track_prefs', array('userid' => $userid, 'forumid' => $forumid)); } /** * @global object * @global object * @param int $forumid * @param int $userid */ function forum_tp_stop_tracking($forumid, $userid=false) { global $USER, $DB; if ($userid === false) { $userid = $USER->id; } if (!$DB->record_exists('forum_track_prefs', array('userid' => $userid, 'forumid' => $forumid))) { $track_prefs = new stdClass(); $track_prefs->userid = $userid; $track_prefs->forumid = $forumid; $DB->insert_record('forum_track_prefs', $track_prefs); } return forum_tp_delete_read_records($userid, -1, -1, $forumid); } /** * Clean old records from the forum_read table. * @global object * @global object * @return void */ function forum_tp_clean_read_records() { global $CFG, $DB; if (!isset($CFG->forum_oldpostdays)) { return; } // Look for records older than the cutoffdate that are still in the forum_read table. $cutoffdate = time() - ($CFG->forum_oldpostdays*24*60*60); //first get the oldest tracking present - we need tis to speedup the next delete query $sql = "SELECT MIN(fp.modified) AS first FROM {forum_posts} fp JOIN {forum_read} fr ON fr.postid=fp.id"; if (!$first = $DB->get_field_sql($sql)) { // nothing to delete; return; } // now delete old tracking info $sql = "DELETE FROM {forum_read} WHERE postid IN (SELECT fp.id FROM {forum_posts} fp WHERE fp.modified >= ? AND fp.modified < ?)"; $DB->execute($sql, array($first, $cutoffdate)); } /** * Sets the last post for a given discussion * * @global object * @global object * @param into $discussionid * @return bool|int **/ function forum_discussion_update_last_post($discussionid) { global $CFG, $DB; // Check the given discussion exists if (!$DB->record_exists('forum_discussions', array('id' => $discussionid))) { return false; } // Use SQL to find the last post for this discussion $sql = "SELECT id, userid, modified FROM {forum_posts} WHERE discussion=? ORDER BY modified DESC, id DESC"; // Lets go find the last post if (($lastposts = $DB->get_records_sql($sql, array($discussionid), 0, 1))) { $lastpost = reset($lastposts); $discussionobject = new stdClass(); $discussionobject->id = $discussionid; $discussionobject->usermodified = $lastpost->userid; $discussionobject->timemodified = $lastpost->modified; $DB->update_record('forum_discussions', $discussionobject); return $lastpost->id; } // To get here either we couldn't find a post for the discussion (weird) // or we couldn't update the discussion record (weird x2) return false; } /** * List the actions that correspond to a view of this module. * This is used by the participation report. * * Note: This is not used by new logging system. Event with * crud = 'r' and edulevel = LEVEL_PARTICIPATING will * be considered as view action. * * @return array */ function forum_get_view_actions() { return array('view discussion', 'search', 'forum', 'forums', 'subscribers', 'view forum'); } /** * List the options for forum subscription modes. * This is used by the settings page and by the mod_form page. * * @return array */ function forum_get_subscriptionmode_options() { $options = array(); $options[FORUM_CHOOSESUBSCRIBE] = get_string('subscriptionoptional', 'forum'); $options[FORUM_FORCESUBSCRIBE] = get_string('subscriptionforced', 'forum'); $options[FORUM_INITIALSUBSCRIBE] = get_string('subscriptionauto', 'forum'); $options[FORUM_DISALLOWSUBSCRIBE] = get_string('subscriptiondisabled', 'forum'); return $options; } /** * List the actions that correspond to a post of this module. * This is used by the participation report. * * Note: This is not used by new logging system. Event with * crud = ('c' || 'u' || 'd') and edulevel = LEVEL_PARTICIPATING * will be considered as post action. * * @return array */ function forum_get_post_actions() { return array('add discussion','add post','delete discussion','delete post','move discussion','prune post','update post'); } /** * Returns a warning object if a user has reached the number of posts equal to * the warning/blocking setting, or false if there is no warning to show. * * @param int|stdClass $forum the forum id or the forum object * @param stdClass $cm the course module * @return stdClass|bool returns an object with the warning information, else * returns false if no warning is required. */ function forum_check_throttling($forum, $cm = null) { global $CFG, $DB, $USER; if (is_numeric($forum)) { $forum = $DB->get_record('forum', ['id' => $forum], 'id, course, blockperiod, blockafter, warnafter', MUST_EXIST); } if (!is_object($forum) || !isset($forum->id) || !isset($forum->course)) { // The passed forum parameter is invalid. This can happen if: // - a non-object and non-numeric forum is passed; or // - the forum object does not have an ID or course attributes. // This is unlikely to happen with properly formed forum record fetched from the database, // so it's most likely a dev error if we hit such this case. throw new coding_exception('Invalid forum parameter passed'); } if (empty($forum->blockafter)) { return false; } if (empty($forum->blockperiod)) { return false; } if (!$cm) { // Try to fetch the $cm object via get_fast_modinfo() so we don't incur DB reads. $modinfo = get_fast_modinfo($forum->course); $forumcms = $modinfo->get_instances_of('forum'); foreach ($forumcms as $tmpcm) { if ($tmpcm->instance == $forum->id) { $cm = $tmpcm; break; } } // Last resort. Try to fetch via get_coursemodule_from_instance(). if (!$cm) { $cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course, false, MUST_EXIST); } } $modcontext = context_module::instance($cm->id); if (has_capability('mod/forum:postwithoutthrottling', $modcontext)) { return false; } // Get the number of posts in the last period we care about. $timenow = time(); $timeafter = $timenow - $forum->blockperiod; $numposts = $DB->count_records_sql('SELECT COUNT(p.id) FROM {forum_posts} p JOIN {forum_discussions} d ON p.discussion = d.id WHERE d.forum = ? AND p.userid = ? AND p.created > ?', array($forum->id, $USER->id, $timeafter)); $a = new stdClass(); $a->blockafter = $forum->blockafter; $a->numposts = $numposts; $a->blockperiod = get_string('secondstotime'.$forum->blockperiod); if ($forum->blockafter <= $numposts) { $warning = new stdClass(); $warning->canpost = false; $warning->errorcode = 'forumblockingtoomanyposts'; $warning->module = 'error'; $warning->additional = $a; $warning->link = $CFG->wwwroot . '/mod/forum/view.php?f=' . $forum->id; return $warning; } if ($forum->warnafter <= $numposts) { $warning = new stdClass(); $warning->canpost = true; $warning->errorcode = 'forumblockingalmosttoomanyposts'; $warning->module = 'forum'; $warning->additional = $a; $warning->link = null; return $warning; } // No warning needs to be shown yet. return false; } /** * Throws an error if the user is no longer allowed to post due to having reached * or exceeded the number of posts specified in 'Post threshold for blocking' * setting. * * @since Moodle 2.5 * @param stdClass $thresholdwarning the warning information returned * from the function forum_check_throttling. */ function forum_check_blocking_threshold($thresholdwarning) { if (!empty($thresholdwarning) && !$thresholdwarning->canpost) { throw new \moodle_exception($thresholdwarning->errorcode, $thresholdwarning->module, $thresholdwarning->link, $thresholdwarning->additional); } } /** * Removes all grades from gradebook * * @global object * @global object * @param int $courseid * @param string $type optional */ function forum_reset_gradebook($courseid, $type='') { global $CFG, $DB; $wheresql = ''; $params = array($courseid); if ($type) { $wheresql = "AND f.type=?"; $params[] = $type; } $sql = "SELECT f.*, cm.idnumber as cmidnumber, f.course as courseid FROM {forum} f, {course_modules} cm, {modules} m WHERE m.name='forum' AND m.id=cm.module AND cm.instance=f.id AND f.course=? $wheresql"; if ($forums = $DB->get_records_sql($sql, $params)) { foreach ($forums as $forum) { forum_grade_item_update($forum, 'reset', 'reset'); } } } /** * This function is used by the reset_course_userdata function in moodlelib. * This function will remove all posts from the specified forum * and clean up any related data. * * @global object * @global object * @param stdClass $data the data submitted from the reset course. * @return array status array */ function forum_reset_userdata($data) { global $CFG, $DB; require_once($CFG->dirroot.'/rating/lib.php'); $componentstr = get_string('modulenameplural', 'forum'); $status = []; $params = [$data->courseid]; $removeposts = false; $typesql = ''; if (!empty($data->reset_forum_all)) { $removeposts = true; $typesstr = get_string('resetforumsall', 'forum'); $types = []; } else if (!empty($data->reset_forum_types)) { $removeposts = true; $types = []; $sqltypes = []; $forumtypesall = forum_get_forum_types_all(); foreach ($data->reset_forum_types as $type) { if (!array_key_exists($type, $forumtypesall)) { continue; } $types[] = $forumtypesall[$type]; $sqltypes[] = $type; } if (!empty($sqltypes)) { list($typesql, $typeparams) = $DB->get_in_or_equal($sqltypes); $typesql = " AND f.type " . $typesql; $params = array_merge($params, $typeparams); } $typesstr = get_string('resetforums', 'forum').': '.implode(', ', $types); } $alldiscussionssql = "SELECT fd.id FROM {forum_discussions} fd, {forum} f WHERE f.course=? AND f.id=fd.forum"; $allforumssql = "SELECT f.id FROM {forum} f WHERE f.course=?"; $allpostssql = "SELECT fp.id FROM {forum_posts} fp, {forum_discussions} fd, {forum} f WHERE f.course=? AND f.id=fd.forum AND fd.id=fp.discussion"; $forumssql = $forums = $rm = null; // Check if we need to get additional data. if ($removeposts || !empty($data->reset_forum_ratings) || !empty($data->reset_forum_tags)) { // Set this up if we have to remove ratings. $rm = new rating_manager(); $ratingdeloptions = new stdClass; $ratingdeloptions->component = 'mod_forum'; $ratingdeloptions->ratingarea = 'post'; // Get the forums for actions that require it. $forumssql = "$allforumssql $typesql"; $forums = $DB->get_records_sql($forumssql, $params); } if ($removeposts) { $discussionssql = "$alldiscussionssql $typesql"; $postssql = "$allpostssql $typesql"; // Now get rid of all attachments. $fs = get_file_storage(); if ($forums) { foreach ($forums as $forumid => $unused) { if (!$cm = get_coursemodule_from_instance('forum', $forumid)) { continue; } $context = context_module::instance($cm->id); $fs->delete_area_files($context->id, 'mod_forum', 'attachment'); $fs->delete_area_files($context->id, 'mod_forum', 'post'); // Remove ratings. $ratingdeloptions->contextid = $context->id; $rm->delete_ratings($ratingdeloptions); core_tag_tag::delete_instances('mod_forum', null, $context->id); } } // First delete all read flags. $DB->delete_records_select('forum_read', "forumid IN ($forumssql)", $params); // Remove tracking prefs. $DB->delete_records_select('forum_track_prefs', "forumid IN ($forumssql)", $params); // Remove posts from queue. $DB->delete_records_select('forum_queue', "discussionid IN ($discussionssql)", $params); // All posts - initial posts must be kept in single simple discussion forums. // First all children. $DB->delete_records_select('forum_posts', "discussion IN ($discussionssql) AND parent <> 0", $params); // Now the initial posts for non single simple. $DB->delete_records_select( 'forum_posts', "discussion IN ($discussionssql AND f.type <> 'single') AND parent = 0", $params ); // Finally all discussions except single simple forums. $DB->delete_records_select('forum_discussions', "forum IN ($forumssql AND f.type <> 'single')", $params); // Remove all grades from gradebook. if (empty($data->reset_gradebook_grades)) { if (empty($types)) { forum_reset_gradebook($data->courseid); } else { foreach ($types as $type) { forum_reset_gradebook($data->courseid, $type); } } } $status[] = [ 'component' => $componentstr, 'item' => $typesstr, 'error' => false, ]; } // Remove all ratings in this course's forums. if (!empty($data->reset_forum_ratings)) { if ($forums) { foreach ($forums as $forumid => $unused) { if (!$cm = get_coursemodule_from_instance('forum', $forumid)) { continue; } $context = context_module::instance($cm->id); // Remove ratings. $ratingdeloptions->contextid = $context->id; $rm->delete_ratings($ratingdeloptions); } } // Remove all grades from gradebook. if (empty($data->reset_gradebook_grades)) { forum_reset_gradebook($data->courseid); } } // Remove all the tags. if (!empty($data->reset_forum_tags)) { if ($forums) { foreach ($forums as $forumid => $unused) { if (!$cm = get_coursemodule_from_instance('forum', $forumid)) { continue; } $context = context_module::instance($cm->id); core_tag_tag::delete_instances('mod_forum', null, $context->id); } } $status[] = [ 'component' => $componentstr, 'item' => get_string('removeallforumtags', 'forum'), 'error' => false, ]; } // Remove all digest settings unconditionally - even for users still enrolled in course. if (!empty($data->reset_forum_digests)) { $DB->delete_records_select('forum_digests', "forum IN ($allforumssql)", $params); $status[] = [ 'component' => $componentstr, 'item' => get_string('resetdigests', 'forum'), 'error' => false, ]; } // Remove all subscriptions unconditionally - even for users still enrolled in course. if (!empty($data->reset_forum_subscriptions)) { $DB->delete_records_select('forum_subscriptions', "forum IN ($allforumssql)", $params); $DB->delete_records_select('forum_discussion_subs', "forum IN ($allforumssql)", $params); $status[] = [ 'component' => $componentstr, 'item' => get_string('resetsubscriptions', 'forum'), 'error' => false, ]; } // Remove all tracking prefs unconditionally - even for users still enrolled in course. if (!empty($data->reset_forum_track_prefs)) { $DB->delete_records_select('forum_track_prefs', "forumid IN ($allforumssql)", $params); $status[] = [ 'component' => $componentstr, 'item' => get_string('resettrackprefs', 'forum'), 'error' => false, ]; } // Updating dates - shift may be negative too. if ($data->timeshift) { // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. // See MDL-9367. shift_course_mod_dates('forum', [ 'assesstimestart', 'assesstimefinish', 'duedate', 'cutoffdate', ], $data->timeshift, $data->courseid); $status[] = [ 'component' => $componentstr, 'item' => get_string('date'), 'error' => false, ]; } return $status; } /** * Called by course/reset.php * * @param MoodleQuickForm $mform form passed by reference */ function forum_reset_course_form_definition(&$mform) { $mform->addElement('header', 'forumheader', get_string('modulenameplural', 'forum')); $mform->addElement('static', 'forumdelete', get_string('delete')); $mform->addElement('checkbox', 'reset_forum_digests', get_string('resetdigests', 'forum')); $mform->addElement('checkbox', 'reset_forum_subscriptions', get_string('resetsubscriptions', 'forum')); $mform->addElement('checkbox', 'reset_forum_all', get_string('resetforumsall', 'forum')); $mform->addElement( 'select', 'reset_forum_types', get_string('resetforums', 'forum'), forum_get_forum_types_all(), ['multiple' => 'multiple'], ); $mform->hideIf('reset_forum_types', 'reset_forum_all', 'checked'); $mform->addElement('checkbox', 'reset_forum_track_prefs', get_string('resettrackprefs', 'forum')); $mform->hideIf('reset_forum_track_prefs', 'reset_forum_all', 'checked'); $mform->addElement('checkbox', 'reset_forum_ratings', get_string('deleteallratings')); $mform->hideIf('reset_forum_ratings', 'reset_forum_all', 'checked'); $mform->addElement('checkbox', 'reset_forum_tags', get_string('removeallforumtags', 'forum')); $mform->hideIf('reset_forum_tags', 'reset_forum_all', 'checked'); } /** * Course reset form defaults. * @return array */ function forum_reset_course_form_defaults($course) { return array('reset_forum_all'=>1, 'reset_forum_digests' => 0, 'reset_forum_subscriptions'=>0, 'reset_forum_track_prefs'=>0, 'reset_forum_ratings'=>1); } /** * Returns array of forum layout modes * * @param bool $useexperimentalui use experimental layout modes or not * @return array */ function forum_get_layout_modes(bool $useexperimentalui = false) { $modes = [ FORUM_MODE_FLATOLDEST => get_string('modeflatoldestfirst', 'forum'), FORUM_MODE_FLATNEWEST => get_string('modeflatnewestfirst', 'forum'), FORUM_MODE_THREADED => get_string('modethreaded', 'forum') ]; if ($useexperimentalui) { $modes[FORUM_MODE_NESTED_V2] = get_string('modenestedv2', 'forum'); } else { $modes[FORUM_MODE_NESTED] = get_string('modenested', 'forum'); } return $modes; } /** * Returns array of forum types chooseable on the forum editing form * * @return array */ function forum_get_forum_types() { return array ('general' => get_string('generalforum', 'forum'), 'eachuser' => get_string('eachuserforum', 'forum'), 'single' => get_string('singleforum', 'forum'), 'qanda' => get_string('qandaforum', 'forum'), 'blog' => get_string('blogforum', 'forum')); } /** * Returns array of all forum layout modes * * @return array */ function forum_get_forum_types_all() { return array ('news' => get_string('namenews','forum'), 'social' => get_string('namesocial','forum'), 'general' => get_string('generalforum', 'forum'), 'eachuser' => get_string('eachuserforum', 'forum'), 'single' => get_string('singleforum', 'forum'), 'qanda' => get_string('qandaforum', 'forum'), 'blog' => get_string('blogforum', 'forum')); } /** * Returns all other caps used in module * * @return array */ function forum_get_extra_capabilities() { return ['moodle/rating:view', 'moodle/rating:viewany', 'moodle/rating:viewall', 'moodle/rating:rate']; } /** * Adds module specific settings to the settings block * * @param settings_navigation $settings The settings navigation object * @param navigation_node $forumnode The node to add module settings to */ function forum_extend_settings_navigation(settings_navigation $settingsnav, navigation_node $forumnode) { global $USER, $CFG; if (empty($settingsnav->get_page()->cm->context)) { $settingsnav->get_page()->cm->context = context_module::instance($settingsnav->get_page()->cm->instance); } $vaultfactory = mod_forum\local\container::get_vault_factory(); $managerfactory = mod_forum\local\container::get_manager_factory(); $legacydatamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory(); $forumvault = $vaultfactory->get_forum_vault(); $forumentity = $forumvault->get_from_id($settingsnav->get_page()->cm->instance); $forumobject = $legacydatamapperfactory->get_forum_data_mapper()->to_legacy_object($forumentity); $params = $settingsnav->get_page()->url->params(); if (!empty($params['d'])) { $discussionid = $params['d']; } // For some actions you need to be enrolled, being admin is not enough sometimes here. $enrolled = is_enrolled($settingsnav->get_page()->context, $USER, '', false); $activeenrolled = is_enrolled($settingsnav->get_page()->context, $USER, '', true); $canmanage = has_capability('mod/forum:managesubscriptions', $settingsnav->get_page()->context); $subscriptionmode = \mod_forum\subscriptions::get_subscription_mode($forumobject); $cansubscribe = $activeenrolled && !\mod_forum\subscriptions::is_forcesubscribed($forumobject) && (!\mod_forum\subscriptions::subscription_disabled($forumobject) || $canmanage); if ($canmanage) { $mode = $forumnode->add(get_string('subscriptionmode', 'forum'), null, navigation_node::TYPE_CONTAINER); $mode->add_class('subscriptionmode'); $mode->set_show_in_secondary_navigation(false); // Optional subscription mode. $allowchoicestring = get_string('subscriptionoptional', 'forum'); $allowchoiceaction = new action_link( new moodle_url('/mod/forum/subscribe.php', [ 'id' => $forumobject->id, 'mode' => FORUM_CHOOSESUBSCRIBE, 'sesskey' => sesskey(), ]), $allowchoicestring, new confirm_action(get_string('subscriptionmodeconfirm', 'mod_forum', $allowchoicestring)) ); $allowchoice = $mode->add($allowchoicestring, $allowchoiceaction, navigation_node::TYPE_SETTING); // Forced subscription mode. $forceforeverstring = get_string('subscriptionforced', 'forum'); $forceforeveraction = new action_link( new moodle_url('/mod/forum/subscribe.php', [ 'id' => $forumobject->id, 'mode' => FORUM_FORCESUBSCRIBE, 'sesskey' => sesskey(), ]), $forceforeverstring, new confirm_action(get_string('subscriptionmodeconfirm', 'mod_forum', $forceforeverstring)) ); $forceforever = $mode->add($forceforeverstring, $forceforeveraction, navigation_node::TYPE_SETTING); // Initial subscription mode. $forceinitiallystring = get_string('subscriptionauto', 'forum'); $forceinitiallyaction = new action_link( new moodle_url('/mod/forum/subscribe.php', [ 'id' => $forumobject->id, 'mode' => FORUM_INITIALSUBSCRIBE, 'sesskey' => sesskey(), ]), $forceinitiallystring, new confirm_action(get_string('subscriptionmodeconfirm', 'mod_forum', $forceinitiallystring)) ); $forceinitially = $mode->add($forceinitiallystring, $forceinitiallyaction, navigation_node::TYPE_SETTING); // Disabled subscription mode. $disallowchoicestring = get_string('subscriptiondisabled', 'forum'); $disallowchoiceaction = new action_link( new moodle_url('/mod/forum/subscribe.php', [ 'id' => $forumobject->id, 'mode' => FORUM_DISALLOWSUBSCRIBE, 'sesskey' => sesskey(), ]), $disallowchoicestring, new confirm_action(get_string('subscriptionmodeconfirm', 'mod_forum', $disallowchoicestring)) ); $disallowchoice = $mode->add($disallowchoicestring, $disallowchoiceaction, navigation_node::TYPE_SETTING); switch ($subscriptionmode) { case FORUM_CHOOSESUBSCRIBE : // 0 $allowchoice->action = null; $allowchoice->add_class('activesetting'); $allowchoice->icon = new pix_icon('t/selected', '', 'mod_forum'); break; case FORUM_FORCESUBSCRIBE : // 1 $forceforever->action = null; $forceforever->add_class('activesetting'); $forceforever->icon = new pix_icon('t/selected', '', 'mod_forum'); break; case FORUM_INITIALSUBSCRIBE : // 2 $forceinitially->action = null; $forceinitially->add_class('activesetting'); $forceinitially->icon = new pix_icon('t/selected', '', 'mod_forum'); break; case FORUM_DISALLOWSUBSCRIBE : // 3 $disallowchoice->action = null; $disallowchoice->add_class('activesetting'); $disallowchoice->icon = new pix_icon('t/selected', '', 'mod_forum'); break; } } else if ($activeenrolled) { switch ($subscriptionmode) { case FORUM_CHOOSESUBSCRIBE : // 0 $notenode = $forumnode->add(get_string('subscriptionoptional', 'forum')); break; case FORUM_FORCESUBSCRIBE : // 1 $notenode = $forumnode->add(get_string('subscriptionforced', 'forum')); break; case FORUM_INITIALSUBSCRIBE : // 2 $notenode = $forumnode->add(get_string('subscriptionauto', 'forum')); break; case FORUM_DISALLOWSUBSCRIBE : // 3 $notenode = $forumnode->add(get_string('subscriptiondisabled', 'forum')); break; } } if (has_capability('mod/forum:viewsubscribers', $settingsnav->get_page()->context)) { $url = new moodle_url('/mod/forum/subscribers.php', ['id' => $forumobject->id, 'edit' => 'off']); $forumnode->add(get_string('subscriptions', 'forum'), $url, navigation_node::TYPE_SETTING, null, 'forumsubscriptions'); } // Display all forum reports user has access to. if (isloggedin() && !isguestuser()) { $reportnames = array_keys(core_component::get_plugin_list('forumreport')); foreach ($reportnames as $reportname) { if (has_capability("forumreport/{$reportname}:view", $settingsnav->get_page()->context)) { $reportlinkparams = [ 'courseid' => $forumobject->course, 'forumid' => $forumobject->id, ]; $reportlink = new moodle_url("/mod/forum/report/{$reportname}/index.php", $reportlinkparams); $forumnode->add(get_string('reports'), $reportlink, navigation_node::TYPE_CONTAINER); } } } if ($enrolled && forum_tp_can_track_forums($forumobject)) { // keep tracking info for users with suspended enrolments if ($forumobject->trackingtype == FORUM_TRACKING_OPTIONAL || ((!$CFG->forum_allowforcedreadtracking) && $forumobject->trackingtype == FORUM_TRACKING_FORCED)) { if (forum_tp_is_tracked($forumobject)) { $linktext = get_string('notrackforum', 'forum'); } else { $linktext = get_string('trackforum', 'forum'); } $url = new moodle_url('/mod/forum/settracking.php', array( 'id' => $forumobject->id, 'sesskey' => sesskey(), )); $forumnode->add($linktext, $url, navigation_node::TYPE_SETTING); } } if (!isloggedin() && $settingsnav->get_page()->course->id == SITEID) { $userid = guest_user()->id; } else { $userid = $USER->id; } $hascourseaccess = ($settingsnav->get_page()->course->id == SITEID) || can_access_course($settingsnav->get_page()->course, $userid); $enablerssfeeds = !empty($CFG->enablerssfeeds) && !empty($CFG->forum_enablerssfeeds); if ($enablerssfeeds && $forumobject->rsstype && $forumobject->rssarticles && $hascourseaccess) { if (!function_exists('rss_get_url')) { require_once("$CFG->libdir/rsslib.php"); } if ($forumobject->rsstype == 1) { $string = get_string('rsssubscriberssdiscussions','forum'); } else { $string = get_string('rsssubscriberssposts','forum'); } $url = new moodle_url(rss_get_url($settingsnav->get_page()->cm->context->id, $userid, "mod_forum", $forumobject->id)); $forumnode->add($string, $url, settings_navigation::TYPE_SETTING, null, null, new pix_icon('i/rss', '')); } $capabilitymanager = $managerfactory->get_capability_manager($forumentity); if ($capabilitymanager->can_export_forum($USER)) { $url = new moodle_url('/mod/forum/export.php', ['id' => $forumobject->id]); $forumnode->add(get_string('export', 'mod_forum'), $url, navigation_node::TYPE_SETTING); } } /** * Return a list of page types * @param string $pagetype current page type * @param stdClass $parentcontext Block's parent context * @param stdClass $currentcontext Current context of block */ function forum_page_type_list($pagetype, $parentcontext, $currentcontext) { $forum_pagetype = array( 'mod-forum-*'=>get_string('page-mod-forum-x', 'forum'), 'mod-forum-view'=>get_string('page-mod-forum-view', 'forum'), 'mod-forum-discuss'=>get_string('page-mod-forum-discuss', 'forum') ); return $forum_pagetype; } /** * Gets all of the courses where the provided user has posted in a forum. * * @global moodle_database $DB The database connection * @param stdClass $user The user who's posts we are looking for * @param bool $discussionsonly If true only look for discussions started by the user * @param bool $includecontexts If set to trye contexts for the courses will be preloaded * @param int $limitfrom The offset of records to return * @param int $limitnum The number of records to return * @return array An array of courses */ function forum_get_courses_user_posted_in($user, $discussionsonly = false, $includecontexts = true, $limitfrom = null, $limitnum = null) { global $DB; // If we are only after discussions we need only look at the forum_discussions // table and join to the userid there. If we are looking for posts then we need // to join to the forum_posts table. if (!$discussionsonly) { $subquery = "(SELECT DISTINCT fd.course FROM {forum_discussions} fd JOIN {forum_posts} fp ON fp.discussion = fd.id WHERE fp.userid = :userid )"; } else { $subquery= "(SELECT DISTINCT fd.course FROM {forum_discussions} fd WHERE fd.userid = :userid )"; } $params = array('userid' => $user->id); // Join to the context table so that we can preload contexts if required. if ($includecontexts) { $ctxselect = ', ' . context_helper::get_preload_record_columns_sql('ctx'); $ctxjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)"; $params['contextlevel'] = CONTEXT_COURSE; } else { $ctxselect = ''; $ctxjoin = ''; } // Now we need to get all of the courses to search. // All courses where the user has posted within a forum will be returned. $sql = "SELECT c.* $ctxselect FROM {course} c $ctxjoin WHERE c.id IN ($subquery)"; $courses = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum); if ($includecontexts) { array_map('context_helper::preload_from_record', $courses); } return $courses; } /** * Gets all of the forums a user has posted in for one or more courses. * * @global moodle_database $DB * @param stdClass $user * @param array $courseids An array of courseids to search or if not provided * all courses the user has posted within * @param bool $discussionsonly If true then only forums where the user has started * a discussion will be returned. * @param int $limitfrom The offset of records to return * @param int $limitnum The number of records to return * @return array An array of forums the user has posted within in the provided courses */ function forum_get_forums_user_posted_in($user, ?array $courseids = null, $discussionsonly = false, $limitfrom = null, $limitnum = null) { global $DB; if (!is_null($courseids)) { list($coursewhere, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED, 'courseid'); $coursewhere = ' AND f.course '.$coursewhere; } else { $coursewhere = ''; $params = array(); } $params['userid'] = $user->id; $params['forum'] = 'forum'; if ($discussionsonly) { $join = 'JOIN {forum_discussions} ff ON ff.forum = f.id'; } else { $join = 'JOIN {forum_discussions} fd ON fd.forum = f.id JOIN {forum_posts} ff ON ff.discussion = fd.id'; } $sql = "SELECT f.*, cm.id AS cmid FROM {forum} f JOIN {course_modules} cm ON cm.instance = f.id JOIN {modules} m ON m.id = cm.module JOIN ( SELECT f.id FROM {forum} f {$join} WHERE ff.userid = :userid GROUP BY f.id ) j ON j.id = f.id WHERE m.name = :forum {$coursewhere}"; $courseforums = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum); return $courseforums; } /** * Returns posts made by the selected user in the requested courses. * * This method can be used to return all of the posts made by the requested user * within the given courses. * For each course the access of the current user and requested user is checked * and then for each post access to the post and forum is checked as well. * * This function is safe to use with usercapabilities. * * @global moodle_database $DB * @param stdClass $user The user whose posts we want to get * @param array $courses The courses to search * @param bool $musthaveaccess If set to true errors will be thrown if the user * cannot access one or more of the courses to search * @param bool $discussionsonly If set to true only discussion starting posts * will be returned. * @param int $limitfrom The offset of records to return * @param int $limitnum The number of records to return * @return stdClass An object the following properties * ->totalcount: the total number of posts made by the requested user * that the current user can see. * ->courses: An array of courses the current user can see that the * requested user has posted in. * ->forums: An array of forums relating to the posts returned in the * property below. * ->posts: An array containing the posts to show for this request. */ function forum_get_posts_by_user($user, array $courses, $musthaveaccess = false, $discussionsonly = false, $limitfrom = 0, $limitnum = 50) { global $DB, $USER, $CFG; $return = new stdClass; $return->totalcount = 0; // The total number of posts that the current user is able to view $return->courses = array(); // The courses the current user can access $return->forums = array(); // The forums that the current user can access that contain posts $return->posts = array(); // The posts to display // First up a small sanity check. If there are no courses to check we can // return immediately, there is obviously nothing to search. if (empty($courses)) { return $return; } // A couple of quick setups $isloggedin = isloggedin(); $isguestuser = $isloggedin && isguestuser(); $iscurrentuser = $isloggedin && $USER->id == $user->id; // Checkout whether or not the current user has capabilities over the requested // user and if so they have the capabilities required to view the requested // users content. $usercontext = context_user::instance($user->id, MUST_EXIST); $hascapsonuser = !$iscurrentuser && $DB->record_exists('role_assignments', array('userid' => $USER->id, 'contextid' => $usercontext->id)); $hascapsonuser = $hascapsonuser && has_all_capabilities(array('moodle/user:viewdetails', 'moodle/user:readuserposts'), $usercontext); // Before we actually search each course we need to check the user's access to the // course. If the user doesn't have the appropraite access then we either throw an // error if a particular course was requested or we just skip over the course. foreach ($courses as $course) { $coursecontext = context_course::instance($course->id, MUST_EXIST); if ($iscurrentuser || $hascapsonuser) { // If it is the current user, or the current user has capabilities to the // requested user then all we need to do is check the requested users // current access to the course. // Note: There is no need to check group access or anything of the like // as either the current user is the requested user, or has granted // capabilities on the requested user. Either way they can see what the // requested user posted, although its VERY unlikely in the `parent` situation // that the current user will be able to view the posts in context. if (!is_viewing($coursecontext, $user) && !is_enrolled($coursecontext, $user)) { // Need to have full access to a course to see the rest of own info if ($musthaveaccess) { throw new \moodle_exception('errorenrolmentrequired', 'forum'); } continue; } } else { // Check whether the current user is enrolled or has access to view the course // if they don't we immediately have a problem. if (!can_access_course($course)) { if ($musthaveaccess) { throw new \moodle_exception('errorenrolmentrequired', 'forum'); } continue; } // If groups are in use and enforced throughout the course then make sure // we can meet in at least one course level group. // Note that we check if either the current user or the requested user have // the capability to access all groups. This is because with that capability // a user in group A could post in the group B forum. Grrrr. if (groups_get_course_groupmode($course) == SEPARATEGROUPS && $course->groupmodeforce && !has_capability('moodle/site:accessallgroups', $coursecontext) && !has_capability('moodle/site:accessallgroups', $coursecontext, $user->id)) { // If its the guest user to bad... the guest user cannot access groups if (!$isloggedin or $isguestuser) { // do not use require_login() here because we might have already used require_login($course) if ($musthaveaccess) { redirect(get_login_url()); } continue; } // Get the groups of the current user $mygroups = array_keys(groups_get_all_groups($course->id, $USER->id, $course->defaultgroupingid, 'g.id, g.name')); // Get the groups the requested user is a member of $usergroups = array_keys(groups_get_all_groups($course->id, $user->id, $course->defaultgroupingid, 'g.id, g.name')); // Check whether they are members of the same group. If they are great. $intersect = array_intersect($mygroups, $usergroups); if (empty($intersect)) { // But they're not... if it was a specific course throw an error otherwise // just skip this course so that it is not searched. if ($musthaveaccess) { throw new \moodle_exception("groupnotamember", '', $CFG->wwwroot."/course/view.php?id=$course->id"); } continue; } } } // Woo hoo we got this far which means the current user can search this // this course for the requested user. Although this is only the course accessibility // handling that is complete, the forum accessibility tests are yet to come. $return->courses[$course->id] = $course; } // No longer beed $courses array - lose it not it may be big unset($courses); // Make sure that we have some courses to search if (empty($return->courses)) { // If we don't have any courses to search then the reality is that the current // user doesn't have access to any courses is which the requested user has posted. // Although we do know at this point that the requested user has posts. if ($musthaveaccess) { throw new \moodle_exception('permissiondenied'); } else { return $return; } } // Next step: Collect all of the forums that we will want to search. // It is important to note that this step isn't actually about searching, it is // about determining which forums we can search by testing accessibility. $forums = forum_get_forums_user_posted_in($user, array_keys($return->courses), $discussionsonly); // Will be used to build the where conditions for the search $forumsearchwhere = array(); // Will be used to store the where condition params for the search $forumsearchparams = array(); // Will record forums where the user can freely access everything $forumsearchfullaccess = array(); // DB caching friendly $now = floor(time() / 60) * 60; // For each course to search we want to find the forums the user has posted in // and providing the current user can access the forum create a search condition // for the forum to get the requested users posts. foreach ($return->courses as $course) { // Now we need to get the forums $modinfo = get_fast_modinfo($course); if (empty($modinfo->instances['forum'])) { // hmmm, no forums? well at least its easy... skip! continue; } // Iterate foreach ($modinfo->get_instances_of('forum') as $forumid => $cm) { if (!$cm->uservisible or !isset($forums[$forumid])) { continue; } // Get the forum in question $forum = $forums[$forumid]; // This is needed for functionality later on in the forum code. It is converted to an object // because the cm_info is readonly from 2.6. This is a dirty hack because some other parts of the // code were expecting an writeable object. See {@link forum_print_post()}. $forum->cm = new stdClass(); foreach ($cm as $key => $value) { $forum->cm->$key = $value; } // Check that either the current user can view the forum, or that the // current user has capabilities over the requested user and the requested // user can view the discussion if (!has_capability('mod/forum:viewdiscussion', $cm->context) && !($hascapsonuser && has_capability('mod/forum:viewdiscussion', $cm->context, $user->id))) { continue; } // This will contain forum specific where clauses $forumsearchselect = array(); if (!$iscurrentuser && !$hascapsonuser) { // Make sure we check group access if (groups_get_activity_groupmode($cm, $course) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $cm->context)) { $groups = $modinfo->get_groups($cm->groupingid); $groups[] = -1; list($groupid_sql, $groupid_params) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED, 'grps'.$forumid.'_'); $forumsearchparams = array_merge($forumsearchparams, $groupid_params); $forumsearchselect[] = "d.groupid $groupid_sql"; } // hidden timed discussions if (!empty($CFG->forum_enabletimedposts) && !has_capability('mod/forum:viewhiddentimedposts', $cm->context)) { $forumsearchselect[] = "(d.userid = :userid{$forumid} OR (d.timestart < :timestart{$forumid} AND (d.timeend = 0 OR d.timeend > :timeend{$forumid})))"; $forumsearchparams['userid'.$forumid] = $user->id; $forumsearchparams['timestart'.$forumid] = $now; $forumsearchparams['timeend'.$forumid] = $now; } // qanda access if ($forum->type == 'qanda' && !has_capability('mod/forum:viewqandawithoutposting', $cm->context)) { // We need to check whether the user has posted in the qanda forum. $discussionspostedin = forum_discussions_user_has_posted_in($forum->id, $user->id); if (!empty($discussionspostedin)) { $forumonlydiscussions = array(); // Holds discussion ids for the discussions the user is allowed to see in this forum. foreach ($discussionspostedin as $d) { $forumonlydiscussions[] = $d->id; } list($discussionid_sql, $discussionid_params) = $DB->get_in_or_equal($forumonlydiscussions, SQL_PARAMS_NAMED, 'qanda'.$forumid.'_'); $forumsearchparams = array_merge($forumsearchparams, $discussionid_params); $forumsearchselect[] = "(d.id $discussionid_sql OR p.parent = 0)"; } else { $forumsearchselect[] = "p.parent = 0"; } } if (count($forumsearchselect) > 0) { $forumsearchwhere[] = "(d.forum = :forum{$forumid} AND ".implode(" AND ", $forumsearchselect).")"; $forumsearchparams['forum'.$forumid] = $forumid; } else { $forumsearchfullaccess[] = $forumid; } } else { // The current user/parent can see all of their own posts $forumsearchfullaccess[] = $forumid; } } } // If we dont have any search conditions, and we don't have any forums where // the user has full access then we just return the default. if (empty($forumsearchwhere) && empty($forumsearchfullaccess)) { return $return; } // Prepare a where condition for the full access forums. if (count($forumsearchfullaccess) > 0) { list($fullidsql, $fullidparams) = $DB->get_in_or_equal($forumsearchfullaccess, SQL_PARAMS_NAMED, 'fula'); $forumsearchparams = array_merge($forumsearchparams, $fullidparams); $forumsearchwhere[] = "(d.forum $fullidsql)"; } // Prepare SQL to both count and search. // We alias user.id to useridx because we forum_posts already has a userid field and not aliasing this would break // mssql. $userfieldsapi = \core_user\fields::for_userpic(); $userfields = $userfieldsapi->get_sql('u', false, '', 'useridx', false)->selects; $countsql = 'SELECT COUNT(*) '; $selectsql = 'SELECT p.*, d.forum, d.name AS discussionname, '.$userfields.' '; $wheresql = implode(" OR ", $forumsearchwhere); if ($discussionsonly) { if ($wheresql == '') { $wheresql = 'p.parent = 0'; } else { $wheresql = 'p.parent = 0 AND ('.$wheresql.')'; } } $sql = "FROM {forum_posts} p JOIN {forum_discussions} d ON d.id = p.discussion JOIN {user} u ON u.id = p.userid WHERE ($wheresql) AND p.userid = :userid "; $orderby = "ORDER BY p.modified DESC"; $forumsearchparams['userid'] = $user->id; // Set the total number posts made by the requested user that the current user can see $return->totalcount = $DB->count_records_sql($countsql.$sql, $forumsearchparams); // Set the collection of posts that has been requested $return->posts = $DB->get_records_sql($selectsql.$sql.$orderby, $forumsearchparams, $limitfrom, $limitnum); // We need to build an array of forums for which posts will be displayed. // We do this here to save the caller needing to retrieve them themselves before // printing these forums posts. Given we have the forums already there is // practically no overhead here. foreach ($return->posts as $post) { if (!array_key_exists($post->forum, $return->forums)) { $return->forums[$post->forum] = $forums[$post->forum]; } } return $return; } /** * Set the per-forum maildigest option for the specified user. * * @param stdClass $forum The forum to set the option for. * @param int $maildigest The maildigest option. * @param stdClass $user The user object. This defaults to the global $USER object. * @throws invalid_digest_setting thrown if an invalid maildigest option is provided. */ function forum_set_user_maildigest($forum, $maildigest, $user = null) { global $DB, $USER; if (is_number($forum)) { $forum = $DB->get_record('forum', array('id' => $forum)); } if ($user === null) { $user = $USER; } $course = $DB->get_record('course', array('id' => $forum->course), '*', MUST_EXIST); $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id, false, MUST_EXIST); $context = context_module::instance($cm->id); // User must be allowed to see this forum. require_capability('mod/forum:viewdiscussion', $context, $user->id); // Validate the maildigest setting. $digestoptions = forum_get_user_digest_options($user); if (!isset($digestoptions[$maildigest])) { throw new moodle_exception('invaliddigestsetting', 'mod_forum'); } // Attempt to retrieve any existing forum digest record. $subscription = $DB->get_record('forum_digests', array( 'userid' => $user->id, 'forum' => $forum->id, )); // Create or Update the existing maildigest setting. if ($subscription) { if ($maildigest == -1) { $DB->delete_records('forum_digests', array('forum' => $forum->id, 'userid' => $user->id)); } else if ($maildigest !== $subscription->maildigest) { // Only update the maildigest setting if it's changed. $subscription->maildigest = $maildigest; $DB->update_record('forum_digests', $subscription); } } else { if ($maildigest != -1) { // Only insert the maildigest setting if it's non-default. $subscription = new stdClass(); $subscription->forum = $forum->id; $subscription->userid = $user->id; $subscription->maildigest = $maildigest; $subscription->id = $DB->insert_record('forum_digests', $subscription); } } } /** * Determine the maildigest setting for the specified user against the * specified forum. * * @param Array $digests An array of forums and user digest settings. * @param stdClass $user The user object containing the id and maildigest default. * @param int $forumid The ID of the forum to check. * @return int The calculated maildigest setting for this user and forum. */ function forum_get_user_maildigest_bulk($digests, $user, $forumid) { if (isset($digests[$forumid]) && isset($digests[$forumid][$user->id])) { $maildigest = $digests[$forumid][$user->id]; if ($maildigest === -1) { $maildigest = $user->maildigest; } } else { $maildigest = $user->maildigest; } return $maildigest; } /** * Retrieve the list of available user digest options. * * @param stdClass $user The user object. This defaults to the global $USER object. * @return array The mapping of values to digest options. */ function forum_get_user_digest_options($user = null) { global $USER; // Revert to the global user object. if ($user === null) { $user = $USER; } $digestoptions = array(); $digestoptions['0'] = get_string('emaildigestoffshort', 'mod_forum'); $digestoptions['1'] = get_string('emaildigestcompleteshort', 'mod_forum'); $digestoptions['2'] = get_string('emaildigestsubjectsshort', 'mod_forum'); // We need to add the default digest option at the end - it relies on // the contents of the existing values. $digestoptions['-1'] = get_string('emaildigestdefault', 'mod_forum', $digestoptions[$user->maildigest]); // Resort the options to be in a sensible order. ksort($digestoptions); return $digestoptions; } /** * Determine the current context if one was not already specified. * * If a context of type context_module is specified, it is immediately * returned and not checked. * * @param int $forumid The ID of the forum * @param context_module $context The current context. * @return context_module The context determined */ function forum_get_context($forumid, $context = null) { global $PAGE; if (!$context || !($context instanceof context_module)) { // Find out forum context. First try to take current page context to save on DB query. if ($PAGE->cm && $PAGE->cm->modname === 'forum' && $PAGE->cm->instance == $forumid && $PAGE->context->contextlevel == CONTEXT_MODULE && $PAGE->context->instanceid == $PAGE->cm->id) { $context = $PAGE->context; } else { $cm = get_coursemodule_from_instance('forum', $forumid); $context = \context_module::instance($cm->id); } } return $context; } /** * Mark the activity completed (if required) and trigger the course_module_viewed event. * * @param stdClass $forum forum object * @param stdClass $course course object * @param stdClass $cm course module object * @param stdClass $context context object * @since Moodle 2.9 */ function forum_view($forum, $course, $cm, $context) { // Completion. $completion = new completion_info($course); $completion->set_module_viewed($cm); // Trigger course_module_viewed event. $params = array( 'context' => $context, 'objectid' => $forum->id ); $event = \mod_forum\event\course_module_viewed::create($params); $event->add_record_snapshot('course_modules', $cm); $event->add_record_snapshot('course', $course); $event->add_record_snapshot('forum', $forum); $event->trigger(); } /** * Trigger the discussion viewed event * * @param stdClass $modcontext module context object * @param stdClass $forum forum object * @param stdClass $discussion discussion object * @since Moodle 2.9 */ function forum_discussion_view($modcontext, $forum, $discussion) { $params = array( 'context' => $modcontext, 'objectid' => $discussion->id, ); $event = \mod_forum\event\discussion_viewed::create($params); $event->add_record_snapshot('forum_discussions', $discussion); $event->add_record_snapshot('forum', $forum); $event->trigger(); } /** * Set the discussion to pinned and trigger the discussion pinned event * * @param stdClass $modcontext module context object * @param stdClass $forum forum object * @param stdClass $discussion discussion object * @since Moodle 3.1 */ function forum_discussion_pin($modcontext, $forum, $discussion) { global $DB; $DB->set_field('forum_discussions', 'pinned', FORUM_DISCUSSION_PINNED, array('id' => $discussion->id)); $params = array( 'context' => $modcontext, 'objectid' => $discussion->id, 'other' => array('forumid' => $forum->id) ); $event = \mod_forum\event\discussion_pinned::create($params); $event->add_record_snapshot('forum_discussions', $discussion); $event->trigger(); } /** * Set discussion to unpinned and trigger the discussion unpin event * * @param stdClass $modcontext module context object * @param stdClass $forum forum object * @param stdClass $discussion discussion object * @since Moodle 3.1 */ function forum_discussion_unpin($modcontext, $forum, $discussion) { global $DB; $DB->set_field('forum_discussions', 'pinned', FORUM_DISCUSSION_UNPINNED, array('id' => $discussion->id)); $params = array( 'context' => $modcontext, 'objectid' => $discussion->id, 'other' => array('forumid' => $forum->id) ); $event = \mod_forum\event\discussion_unpinned::create($params); $event->add_record_snapshot('forum_discussions', $discussion); $event->trigger(); } /** * Add nodes to myprofile page. * * @param \core_user\output\myprofile\tree $tree Tree object * @param stdClass $user user object * @param bool $iscurrentuser * @param stdClass $course Course object * * @return bool */ function mod_forum_myprofile_navigation(core_user\output\myprofile\tree $tree, $user, $iscurrentuser, $course) { if (isguestuser($user)) { // The guest user cannot post, so it is not possible to view any posts. // May as well just bail aggressively here. return false; } $postsurl = new moodle_url('/mod/forum/user.php', array('id' => $user->id)); if (!empty($course)) { $postsurl->param('course', $course->id); } $string = get_string('forumposts', 'mod_forum'); $node = new core_user\output\myprofile\node('miscellaneous', 'forumposts', $string, null, $postsurl); $tree->add_node($node); $discussionssurl = new moodle_url('/mod/forum/user.php', array('id' => $user->id, 'mode' => 'discussions')); if (!empty($course)) { $discussionssurl->param('course', $course->id); } $string = get_string('myprofileotherdis', 'mod_forum'); $node = new core_user\output\myprofile\node('miscellaneous', 'forumdiscussions', $string, null, $discussionssurl); $tree->add_node($node); return true; } /** * Checks whether the author's name and picture for a given post should be hidden or not. * * @param object $post The forum post. * @param object $forum The forum object. * @return bool * @throws coding_exception */ function forum_is_author_hidden($post, $forum) { if (!isset($post->parent)) { throw new coding_exception('$post->parent must be set.'); } if (!isset($forum->type)) { throw new coding_exception('$forum->type must be set.'); } if ($forum->type === 'single' && empty($post->parent)) { return true; } return false; } /** * Manage inplace editable saves. * * @param string $itemtype The type of item. * @param int $itemid The ID of the item. * @param mixed $newvalue The new value * @return string */ function mod_forum_inplace_editable($itemtype, $itemid, $newvalue) { global $DB, $PAGE; if ($itemtype === 'digestoptions') { // The itemid is the forumid. $forum = $DB->get_record('forum', array('id' => $itemid), '*', MUST_EXIST); $course = $DB->get_record('course', array('id' => $forum->course), '*', MUST_EXIST); $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id, false, MUST_EXIST); $context = context_module::instance($cm->id); $PAGE->set_context($context); require_login($course, false, $cm); forum_set_user_maildigest($forum, $newvalue); $renderer = $PAGE->get_renderer('mod_forum'); return $renderer->render_digest_options($forum, $newvalue); } } /** * Determine whether the specified forum's cutoff date is reached. * * @param stdClass $forum The forum * @return bool */ function forum_is_cutoff_date_reached($forum) { $entityfactory = \mod_forum\local\container::get_entity_factory(); $coursemoduleinfo = get_fast_modinfo($forum->course); $cminfo = $coursemoduleinfo->instances['forum'][$forum->id]; $forumentity = $entityfactory->get_forum_from_stdclass( $forum, context_module::instance($cminfo->id), $cminfo->get_course_module_record(), $cminfo->get_course() ); return $forumentity->is_cutoff_date_reached(); } /** * Determine whether the specified forum's due date is reached. * * @param stdClass $forum The forum * @return bool */ function forum_is_due_date_reached($forum) { $entityfactory = \mod_forum\local\container::get_entity_factory(); $coursemoduleinfo = get_fast_modinfo($forum->course); $cminfo = $coursemoduleinfo->instances['forum'][$forum->id]; $forumentity = $entityfactory->get_forum_from_stdclass( $forum, context_module::instance($cminfo->id), $cminfo->get_course_module_record(), $cminfo->get_course() ); return $forumentity->is_due_date_reached(); } /** * Determine whether the specified discussion is time-locked. * * @param stdClass $forum The forum that the discussion belongs to * @param stdClass $discussion The discussion to test * @return bool */ function forum_discussion_is_locked($forum, $discussion) { $entityfactory = \mod_forum\local\container::get_entity_factory(); $coursemoduleinfo = get_fast_modinfo($forum->course); $cminfo = $coursemoduleinfo->instances['forum'][$forum->id]; $forumentity = $entityfactory->get_forum_from_stdclass( $forum, context_module::instance($cminfo->id), $cminfo->get_course_module_record(), $cminfo->get_course() ); $discussionentity = $entityfactory->get_discussion_from_stdclass($discussion); return $forumentity->is_discussion_locked($discussionentity); } /** * Check if the module has any update that affects the current user since a given time. * * @param cm_info $cm course module data * @param int $from the time to check updates from * @param array $filter if we need to check only specific updates * @return stdClass an object with the different type of areas indicating if they were updated or not * @since Moodle 3.2 */ function forum_check_updates_since(cm_info $cm, $from, $filter = array()) { $context = $cm->context; $updates = new stdClass(); if (!has_capability('mod/forum:viewdiscussion', $context)) { return $updates; } $updates = course_check_module_updates_since($cm, $from, array(), $filter); // Check if there are new discussions in the forum. $updates->discussions = (object) array('updated' => false); $discussions = forum_get_discussions($cm, '', false, -1, -1, true, -1, 0, FORUM_POSTS_ALL_USER_GROUPS, $from); if (!empty($discussions)) { $updates->discussions->updated = true; $updates->discussions->itemids = array_keys($discussions); } return $updates; } /** * Check if the user can create attachments in a forum. * @param stdClass $forum forum object * @param stdClass $context context object * @return bool true if the user can create attachments, false otherwise * @since Moodle 3.3 */ function forum_can_create_attachment($forum, $context) { // If maxbytes == 1 it means no attachments at all. if (empty($forum->maxattachments) || $forum->maxbytes == 1 || !has_capability('mod/forum:createattachment', $context)) { return false; } return true; } /** * Get icon mapping for font-awesome. * * @return array */ function mod_forum_get_fontawesome_icon_map() { return [ 'mod_forum:i/pinned' => 'fa-thumbtack', 'mod_forum:t/selected' => 'fa-check', 'mod_forum:t/star' => 'fa-star', 'mod_forum:t/subscribed' => 'fa-regular fa-envelope', 'mod_forum:t/unsubscribed' => 'fa-regular fa-envelope-open', ]; } /** * Callback function that determines whether an action event should be showing its item count * based on the event type and the item count. * * @param calendar_event $event The calendar event. * @param int $itemcount The item count associated with the action event. * @return bool */ function mod_forum_core_calendar_event_action_shows_item_count(calendar_event $event, $itemcount = 0) { // Always show item count for forums if item count is greater than 1. // If only one action is required than it is obvious and we don't show it for other modules. return $itemcount > 1; } /** * This function receives a calendar event and returns the action associated with it, or null if there is none. * * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event * is not displayed on the block. * * @param calendar_event $event * @param \core_calendar\action_factory $factory * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default). * @return \core_calendar\local\event\entities\action_interface|null */ function mod_forum_core_calendar_provide_event_action(calendar_event $event, \core_calendar\action_factory $factory, int $userid = 0) { global $DB, $USER; if (!$userid) { $userid = $USER->id; } $cm = get_fast_modinfo($event->courseid, $userid)->instances['forum'][$event->instance]; if (!$cm->uservisible) { // The module is not visible to the user for any reason. return null; } $context = context_module::instance($cm->id); if (!has_capability('mod/forum:viewdiscussion', $context, $userid)) { return null; } $completion = new \completion_info($cm->get_course()); $completiondata = $completion->get_data($cm, false, $userid); if ($completiondata->completionstate != COMPLETION_INCOMPLETE) { return null; } // Get action itemcount. $itemcount = 0; $forum = $DB->get_record('forum', array('id' => $cm->instance)); $postcountsql = " SELECT COUNT(1) FROM {forum_posts} fp INNER JOIN {forum_discussions} fd ON fp.discussion=fd.id WHERE fp.userid=:userid AND fd.forum=:forumid"; $postcountparams = array('userid' => $userid, 'forumid' => $forum->id); if ($forum->completiondiscussions) { $count = $DB->count_records('forum_discussions', array('forum' => $forum->id, 'userid' => $userid)); $itemcount += ($forum->completiondiscussions >= $count) ? ($forum->completiondiscussions - $count) : 0; } if ($forum->completionreplies) { $count = $DB->get_field_sql( $postcountsql.' AND fp.parent<>0', $postcountparams); $itemcount += ($forum->completionreplies >= $count) ? ($forum->completionreplies - $count) : 0; } if ($forum->completionposts) { $count = $DB->get_field_sql($postcountsql, $postcountparams); $itemcount += ($forum->completionposts >= $count) ? ($forum->completionposts - $count) : 0; } // Well there is always atleast one actionable item (view forum, etc). $itemcount = $itemcount > 0 ? $itemcount : 1; return $factory->create_instance( get_string('view'), new \moodle_url('/mod/forum/view.php', ['id' => $cm->id]), $itemcount, true ); } /** * Add a get_coursemodule_info function in case any forum type wants to add 'extra' information * for the course (see resource). * * Given a course_module object, this function returns any "extra" information that may be needed * when printing this activity in a course listing. See get_array_of_activities() in course/lib.php. * * @param stdClass $coursemodule The coursemodule object (record). * @return cached_cm_info An object on information that the courses * will know about (most noticeably, an icon). */ function forum_get_coursemodule_info($coursemodule) { global $DB; $dbparams = ['id' => $coursemodule->instance]; $fields = 'id, name, intro, introformat, completionposts, completiondiscussions, completionreplies, duedate, cutoffdate, trackingtype'; if (!$forum = $DB->get_record('forum', $dbparams, $fields)) { return false; } $result = new cached_cm_info(); $result->name = $forum->name; if ($coursemodule->showdescription) { // Convert intro to html. Do not filter cached version, filters run at display time. $result->content = format_module_intro('forum', $forum, $coursemodule->id, false); } // Populate the custom completion rules as key => value pairs, but only if the completion mode is 'automatic'. if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) { $result->customdata['customcompletionrules']['completiondiscussions'] = $forum->completiondiscussions; $result->customdata['customcompletionrules']['completionreplies'] = $forum->completionreplies; $result->customdata['customcompletionrules']['completionposts'] = $forum->completionposts; } // Populate some other values that can be used in calendar or on dashboard. if ($forum->duedate) { $result->customdata['duedate'] = $forum->duedate; } if ($forum->cutoffdate) { $result->customdata['cutoffdate'] = $forum->cutoffdate; } // Add the forum type to the custom data for Web Services (core_course_get_contents). $result->customdata['trackingtype'] = $forum->trackingtype; return $result; } /** * Callback which returns human-readable strings describing the active completion custom rules for the module instance. * * @param cm_info|stdClass $cm object with fields ->completion and ->customdata['customcompletionrules'] * @return array $descriptions the array of descriptions for the custom rules. */ function mod_forum_get_completion_active_rule_descriptions($cm) { // Values will be present in cm_info, and we assume these are up to date. if (empty($cm->customdata['customcompletionrules']) || $cm->completion != COMPLETION_TRACKING_AUTOMATIC) { return []; } $descriptions = []; foreach ($cm->customdata['customcompletionrules'] as $key => $val) { switch ($key) { case 'completiondiscussions': if (!empty($val)) { $descriptions[] = get_string('completiondiscussionsdesc', 'forum', $val); } break; case 'completionreplies': if (!empty($val)) { $descriptions[] = get_string('completionrepliesdesc', 'forum', $val); } break; case 'completionposts': if (!empty($val)) { $descriptions[] = get_string('completionpostsdesc', 'forum', $val); } break; default: break; } } return $descriptions; } /** * Check whether the forum post is a private reply visible to this user. * * @param stdClass $post The post to check. * @param cm_info $cm The context module instance. * @return bool Whether the post is visible in terms of private reply configuration. */ function forum_post_is_visible_privately($post, $cm) { global $USER; if (!empty($post->privatereplyto)) { // Allow the user to see the private reply if: // * they hold the permission; // * they are the author; or // * they are the intended recipient. $cansee = false; $cansee = $cansee || ($post->userid == $USER->id); $cansee = $cansee || ($post->privatereplyto == $USER->id); $cansee = $cansee || has_capability('mod/forum:readprivatereplies', context_module::instance($cm->id)); return $cansee; } return true; } /** * Check whether the user can reply privately to the parent post. * * @param \context_module $context * @param \stdClass $parent * @return bool */ function forum_user_can_reply_privately(\context_module $context, \stdClass $parent): bool { if ($parent->privatereplyto) { // You cannot reply privately to a post which is, itself, a private reply. return false; } return has_capability('mod/forum:postprivatereply', $context); } /** * This function calculates the minimum and maximum cutoff values for the timestart of * the given event. * * It will return an array with two values, the first being the minimum cutoff value and * the second being the maximum cutoff value. Either or both values can be null, which * indicates there is no minimum or maximum, respectively. * * If a cutoff is required then the function must return an array containing the cutoff * timestamp and error string to display to the user if the cutoff value is violated. * * A minimum and maximum cutoff return value will look like: * [ * [1505704373, 'The date must be after this date'], * [1506741172, 'The date must be before this date'] * ] * * @param calendar_event $event The calendar event to get the time range for * @param stdClass $forum The module instance to get the range from * @return array Returns an array with min and max date. */ function mod_forum_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $forum) { global $CFG; require_once($CFG->dirroot . '/mod/forum/locallib.php'); $mindate = null; $maxdate = null; if ($event->eventtype == FORUM_EVENT_TYPE_DUE) { if (!empty($forum->cutoffdate)) { $maxdate = [ $forum->cutoffdate, get_string('cutoffdatevalidation', 'forum'), ]; } } return [$mindate, $maxdate]; } /** * This function will update the forum module according to the * event that has been modified. * * It will set the timeclose value of the forum instance * according to the type of event provided. * * @throws \moodle_exception * @param \calendar_event $event * @param stdClass $forum The module instance to get the range from */ function mod_forum_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $forum) { global $CFG, $DB; require_once($CFG->dirroot . '/mod/forum/locallib.php'); if ($event->eventtype != FORUM_EVENT_TYPE_DUE) { return; } $courseid = $event->courseid; $modulename = $event->modulename; $instanceid = $event->instance; // Something weird going on. The event is for a different module so // we should ignore it. if ($modulename != 'forum') { return; } if ($forum->id != $instanceid) { return; } $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid]; $context = context_module::instance($coursemodule->id); // The user does not have the capability to modify this activity. if (!has_capability('moodle/course:manageactivities', $context)) { return; } if ($event->eventtype == FORUM_EVENT_TYPE_DUE) { if ($forum->duedate != $event->timestart) { $forum->duedate = $event->timestart; $forum->timemodified = time(); // Persist the instance changes. $DB->update_record('forum', $forum); $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context); $event->trigger(); } } } /** * Fetch the data used to display the discussions on the current page. * * @param \mod_forum\local\entities\forum $forum The forum entity * @param stdClass $user The user to render for * @param int[]|null $groupid The group to render * @param int|null $sortorder The sort order to use when selecting the discussions in the list * @param int|null $pageno The zero-indexed page number to use * @param int|null $pagesize The number of discussions to show on the page * @return array The data to use for display */ function mod_forum_get_discussion_summaries(\mod_forum\local\entities\forum $forum, stdClass $user, ?int $groupid, ?int $sortorder, ?int $pageno = 0, ?int $pagesize = 0) { $vaultfactory = mod_forum\local\container::get_vault_factory(); $discussionvault = $vaultfactory->get_discussions_in_forum_vault(); $managerfactory = mod_forum\local\container::get_manager_factory(); $capabilitymanager = $managerfactory->get_capability_manager($forum); $groupids = mod_forum_get_groups_from_groupid($forum, $user, $groupid); if (null === $groupids) { return $discussions = $discussionvault->get_from_forum_id( $forum->get_id(), $capabilitymanager->can_view_hidden_posts($user), $user->id, $sortorder, $pagesize, $pageno * $pagesize); } else { return $discussions = $discussionvault->get_from_forum_id_and_group_id( $forum->get_id(), $groupids, $capabilitymanager->can_view_hidden_posts($user), $user->id, $sortorder, $pagesize, $pageno * $pagesize); } } /** * Get a count of all discussions in a forum. * * @param \mod_forum\local\entities\forum $forum The forum entity * @param stdClass $user The user to render for * @param int $groupid The group to render * @return int The number of discussions in a forum */ function mod_forum_count_all_discussions(\mod_forum\local\entities\forum $forum, stdClass $user, ?int $groupid) { $managerfactory = mod_forum\local\container::get_manager_factory(); $capabilitymanager = $managerfactory->get_capability_manager($forum); $vaultfactory = mod_forum\local\container::get_vault_factory(); $discussionvault = $vaultfactory->get_discussions_in_forum_vault(); $groupids = mod_forum_get_groups_from_groupid($forum, $user, $groupid); if (null === $groupids) { return $discussionvault->get_total_discussion_count_from_forum_id( $forum->get_id(), $capabilitymanager->can_view_hidden_posts($user), $user->id); } else { return $discussionvault->get_total_discussion_count_from_forum_id_and_group_id( $forum->get_id(), $groupids, $capabilitymanager->can_view_hidden_posts($user), $user->id); } } /** * Get the list of groups to show based on the current user and requested groupid. * * @param \mod_forum\local\entities\forum $forum The forum entity * @param stdClass $user The user viewing * @param int $groupid The groupid requested * @return array The list of groups to show */ function mod_forum_get_groups_from_groupid(\mod_forum\local\entities\forum $forum, stdClass $user, ?int $groupid): ?array { $effectivegroupmode = $forum->get_effective_group_mode(); if (empty($effectivegroupmode)) { // This forum is not in a group mode. Show all posts always. return null; } if (null == $groupid) { $managerfactory = mod_forum\local\container::get_manager_factory(); $capabilitymanager = $managerfactory->get_capability_manager($forum); // No group was specified. $showallgroups = (VISIBLEGROUPS == $effectivegroupmode); $showallgroups = $showallgroups || $capabilitymanager->can_access_all_groups($user); if ($showallgroups) { // Return null to show all groups. return null; } else { // No group was specified. Only show the users current groups. return array_keys( groups_get_all_groups( $forum->get_course_id(), $user->id, $forum->get_course_module_record()->groupingid ) ); } } else { // A group was specified. Just show that group. return [$groupid]; } } /** * Return a list of all the user preferences used by mod_forum. * * @return array */ function mod_forum_user_preferences() { $vaultfactory = \mod_forum\local\container::get_vault_factory(); $discussionlistvault = $vaultfactory->get_discussions_in_forum_vault(); $preferences = array(); $preferences['forum_discussionlistsortorder'] = array( 'null' => NULL_NOT_ALLOWED, 'default' => $discussionlistvault::SORTORDER_LASTPOST_DESC, 'type' => PARAM_INT, 'choices' => array( $discussionlistvault::SORTORDER_LASTPOST_DESC, $discussionlistvault::SORTORDER_LASTPOST_ASC, $discussionlistvault::SORTORDER_CREATED_DESC, $discussionlistvault::SORTORDER_CREATED_ASC, $discussionlistvault::SORTORDER_REPLIES_DESC, $discussionlistvault::SORTORDER_REPLIES_ASC ) ); $preferences['forum_useexperimentalui'] = [ 'null' => NULL_NOT_ALLOWED, 'default' => false, 'type' => PARAM_BOOL ]; return $preferences; } /** * Lists all gradable areas for the advanced grading methods gramework. * * @return array('string'=>'string') An array with area names as keys and descriptions as values */ function forum_grading_areas_list() { return [ 'forum' => get_string('grade_forum_header', 'forum'), ]; } /** * Callback to fetch the activity event type lang string. * * @param string $eventtype The event type. * @return lang_string The event type lang string. */ function mod_forum_core_calendar_get_event_action_string(string $eventtype): string { global $CFG; require_once($CFG->dirroot . '/mod/forum/locallib.php'); $modulename = get_string('modulename', 'forum'); if ($eventtype == FORUM_EVENT_TYPE_DUE) { return get_string('calendardue', 'forum', $modulename); } else { return get_string('requiresaction', 'calendar', $modulename); } } /** * This callback will check the provided instance of this module * and make sure there are up-to-date events created for it. * * @param int $courseid Not used. * @param stdClass $instance Forum module instance. * @param stdClass $cm Course module object. */ function forum_refresh_events(int $courseid, stdClass $instance, stdClass $cm): void { global $CFG; // This function is called by cron and we need to include the locallib for calls further down. require_once($CFG->dirroot . '/mod/forum/locallib.php'); forum_update_calendar($instance, $cm->id); } /** * Callback adds navigation to view user posts if the navadduserpostslinks config is on. * * @param navigation_node $usernode User node within navigation * @param stdClass $user User object * @param \core\context\user $usercontext User context * @param stdClass $course Current course * @param \core\context $coursecontext Course context */ function mod_forum_extend_navigation_user( navigation_node $usernode, stdClass $user, \core\context\user $usercontext, stdClass $course, \core\context $coursecontext, ): void { global $CFG; if (!empty($CFG->navadduserpostslinks) && $coursecontext instanceof \core\context\system) { $baseargs = ['id' => $user->id]; // Add nodes for forum posts and discussions if the user can view either or both // There are no capability checks here as the content of the page is based // purely on the forums the current user has access too. $forumtab = \navigation_node::create(get_string('forumposts', 'forum')); $forumtab->add( get_string('posts', 'forum'), new moodle_url('/mod/forum/user.php', $baseargs), ); $forumtab->add( get_string('discussions', 'forum'), new moodle_url('/mod/forum/user.php', array_merge($baseargs, ['mode' => 'discussions']), ), ); // We add the forum link either immediately after the 'viewuserdetails' link, or as the first item in the list. foreach ($usernode->children as $child) { if ($child->key === 'viewuserdetails') { continue; } $addbefore = $child; break; } $usernode->add_node($forumtab, $addbefore->key); } } PK-B\)t, version.phpnu[. /** * Version information * * @package mod_forum * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'mod_forum'; // Full name of the plugin (used for diagnostics) PK-B\0q00 export.phpnu[. /** * Page to export forum discussions. * * @package mod_forum * @copyright 2019 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('NO_OUTPUT_BUFFERING', true); require_once(__DIR__ . '/../../config.php'); require_once($CFG->libdir . '/adminlib.php'); require_once($CFG->dirroot . '/calendar/externallib.php'); $forumid = required_param('id', PARAM_INT); $userids = optional_param_array('userids', [], PARAM_INT); $discussionids = optional_param_array('discids', [], PARAM_INT); $from = optional_param_array('from', [], PARAM_INT); $to = optional_param_array('to', [], PARAM_INT); $fromtimestamp = optional_param('timestampfrom', '', PARAM_INT); $totimestamp = optional_param('timestampto', '', PARAM_INT); if (!empty($from['enabled'])) { unset($from['enabled']); $from = core_calendar_external::get_timestamps([$from])['timestamps'][0]['timestamp']; } else { $from = $fromtimestamp; } if (!empty($to['enabled'])) { unset($to['enabled']); $to = core_calendar_external::get_timestamps([$to])['timestamps'][0]['timestamp']; } else { $to = $totimestamp; } $vaultfactory = mod_forum\local\container::get_vault_factory(); $managerfactory = mod_forum\local\container::get_manager_factory(); $legacydatamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory(); $forumvault = $vaultfactory->get_forum_vault(); $forum = $forumvault->get_from_id($forumid); if (empty($forum)) { throw new moodle_exception('Unable to find forum with id ' . $forumid); } $capabilitymanager = $managerfactory->get_capability_manager($forum); if (!$capabilitymanager->can_export_forum($USER)) { throw new moodle_exception('cannotexportforum', 'forum'); } $course = $forum->get_course_record(); $coursemodule = $forum->get_course_module_record(); $cm = cm_info::create($coursemodule); require_course_login($course, true, $cm); $url = new moodle_url('/mod/forum/export.php'); $pagetitle = get_string('export', 'mod_forum'); $context = $forum->get_context(); $form = new mod_forum\form\export_form($url->out(false), [ 'forum' => $forum ]); if ($form->is_cancelled()) { redirect(new moodle_url('/mod/forum/view.php', ['id' => $cm->id])); } else if ($data = $form->get_data()) { $dataformat = $data->format; // This may take a very long time and extra memory. \core_php_time_limit::raise(); raise_memory_limit(MEMORY_HUGE); $discussionvault = $vaultfactory->get_discussion_vault(); if ($data->discussionids) { $discussionids = $data->discussionids; } else if (empty($discussionids)) { $discussions = $discussionvault->get_all_discussions_in_forum($forum); $discussionids = array_map(function ($discussion) { return $discussion->get_id(); }, $discussions); } $filters = ['discussionids' => $discussionids]; if ($data->useridsselected) { $filters['userids'] = $data->useridsselected; } if ($data->from) { $filters['from'] = $data->from; } if ($data->to) { $filters['to'] = $data->to; } // Retrieve posts based on the selected filters, note if forum has no discussions then there is nothing to export. if (!empty($filters['discussionids'])) { $postvault = $vaultfactory->get_post_vault(); $posts = $postvault->get_from_filters($USER, $filters, $capabilitymanager->can_view_any_private_reply($USER)); } else { $posts = []; } $striphtml = !empty($data->striphtml); $humandates = !empty($data->humandates); $fields = ['id', 'discussion', 'parent', 'userid', 'userfullname', 'created', 'modified', 'mailed', 'subject', 'message', 'messageformat', 'messagetrust', 'attachment', 'totalscore', 'mailnow', 'deleted', 'privatereplyto', 'privatereplytofullname', 'wordcount', 'charcount']; $canviewfullname = has_capability('moodle/site:viewfullnames', $context); $datamapper = $legacydatamapperfactory->get_post_data_mapper(); $exportdata = new ArrayObject($datamapper->to_legacy_objects($posts)); $iterator = $exportdata->getIterator(); $filename = clean_filename('discussion'); \core\dataformat::download_data( $filename, $dataformat, $fields, $iterator, function($exportdata) use ($fields, $striphtml, $humandates, $canviewfullname, $context) { $data = new stdClass(); foreach ($fields as $field) { // Set data field's value from the export data's equivalent field by default. $data->$field = $exportdata->$field ?? null; if ($field == 'userfullname') { $user = \core_user::get_user($data->userid); $data->userfullname = fullname($user, $canviewfullname); } if ($field == 'privatereplytofullname' && !empty($data->privatereplyto)) { $user = \core_user::get_user($data->privatereplyto); $data->privatereplytofullname = fullname($user, $canviewfullname); } if ($field == 'message') { $data->message = file_rewrite_pluginfile_urls($data->message, 'pluginfile.php', $context->id, 'mod_forum', 'post', $data->id); } // Convert any boolean fields to their integer equivalent for output. if (is_bool($data->$field)) { $data->$field = (int) $data->$field; } } if ($striphtml) { $data->message = html_to_text(format_text($data->message, $data->messageformat), 0, false); $data->messageformat = FORMAT_PLAIN; } if ($humandates) { $data->created = userdate($data->created); $data->modified = userdate($data->modified); } return $data; }); die; } $PAGE->set_context($context); $PAGE->set_url($url); $PAGE->set_title($pagetitle); $PAGE->set_pagelayout('admin'); $PAGE->add_body_class('limitedwidth'); $PAGE->set_heading($pagetitle); $PAGE->activityheader->disable(); echo $OUTPUT->header(); if (!$PAGE->has_secondary_navigation()) { echo $OUTPUT->heading($pagetitle); } // It is possible that the following fields have been provided in the URL. $userids = array_filter($userids, static function(int $userid) use ($course, $cm): bool { return $cm->effectivegroupmode != SEPARATEGROUPS || groups_user_groups_visible($course, $userid, $cm); }); $form->set_data(['useridsselected' => $userids, 'discussionids' => $discussionids, 'from' => $from, 'to' => $to]); $form->display(); echo $OUTPUT->footer(); PK-B\'C;>> search.phpnu[. /** * @package mod_forum * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../../config.php'); require_once('lib.php'); $id = required_param('id', PARAM_INT); // course id $search = trim(optional_param('search', '', PARAM_NOTAGS)); // search string $page = optional_param('page', 0, PARAM_INT); // which page to show $perpage = optional_param('perpage', 10, PARAM_INT); // how many per page $showform = optional_param('showform', 0, PARAM_INT); // Just show the form $user = trim(optional_param('user', '', PARAM_NOTAGS)); // Names to search for $userid = trim(optional_param('userid', 0, PARAM_INT)); // UserID to search for $forumid = trim(optional_param('forumid', 0, PARAM_INT)); // ForumID to search for $subject = trim(optional_param('subject', '', PARAM_NOTAGS)); // Subject $phrase = trim(optional_param('phrase', '', PARAM_NOTAGS)); // Phrase $words = trim(optional_param('words', '', PARAM_NOTAGS)); // Words $fullwords = trim(optional_param('fullwords', '', PARAM_NOTAGS)); // Whole words $notwords = trim(optional_param('notwords', '', PARAM_NOTAGS)); // Words we don't want $tags = optional_param_array('tags', [], PARAM_TEXT); $timefromrestrict = optional_param('timefromrestrict', 0, PARAM_INT); // Use starting date $fromday = optional_param('fromday', 0, PARAM_INT); // Starting date $frommonth = optional_param('frommonth', 0, PARAM_INT); // Starting date $fromyear = optional_param('fromyear', 0, PARAM_INT); // Starting date $fromhour = optional_param('fromhour', 0, PARAM_INT); // Starting date $fromminute = optional_param('fromminute', 0, PARAM_INT); // Starting date if ($timefromrestrict) { $calendartype = \core_calendar\type_factory::get_calendar_instance(); $gregorianfrom = $calendartype->convert_to_gregorian($fromyear, $frommonth, $fromday); $datefrom = make_timestamp($gregorianfrom['year'], $gregorianfrom['month'], $gregorianfrom['day'], $fromhour, $fromminute); } else { $datefrom = optional_param('datefrom', 0, PARAM_INT); // Starting date } $timetorestrict = optional_param('timetorestrict', 0, PARAM_INT); // Use ending date $today = optional_param('today', 0, PARAM_INT); // Ending date $tomonth = optional_param('tomonth', 0, PARAM_INT); // Ending date $toyear = optional_param('toyear', 0, PARAM_INT); // Ending date $tohour = optional_param('tohour', 0, PARAM_INT); // Ending date $tominute = optional_param('tominute', 0, PARAM_INT); // Ending date if ($timetorestrict) { $calendartype = \core_calendar\type_factory::get_calendar_instance(); $gregorianto = $calendartype->convert_to_gregorian($toyear, $tomonth, $today); $dateto = make_timestamp($gregorianto['year'], $gregorianto['month'], $gregorianto['day'], $tohour, $tominute); } else { $dateto = optional_param('dateto', 0, PARAM_INT); // Ending date } $starredonly = optional_param('starredonly', false, PARAM_BOOL); // Include only favourites. $params = [ 'id' => $id, 'perpage' => $perpage, ]; if ($search !== '') { $params['search'] = $search; } if ($page) { $params['page'] = $page; } if ($showform) { $params['showform'] = $showform; } if ($user !== '') { $params['user'] = $user; } if ($userid) { $params['userid'] = $userid; } if ($forumid) { $params['forumid'] = $forumid; } if ($subject !== '') { $params['subject'] = $subject; } if ($phrase !== '') { $params['phrase'] = $phrase; } if ($words !== '') { $params['words'] = $words; } if ($fullwords !== '') { $params['fullwords'] = $fullwords; } if ($notwords !== '') { $params['notwords'] = $notwords; } if ($timefromrestrict) { $params['timefromrestrict'] = $timefromrestrict; } if ($fromday) { $params['fromday'] = $fromday; } if ($fromhour) { $params['fromhour'] = $fromhour; } if ($fromminute) { $params['fromminute'] = $fromminute; } if ($frommonth) { $params['frommonth'] = $frommonth; } if ($fromyear) { $params['fromyear'] = $fromyear; } if ($datefrom) { $params['datefrom'] = $datefrom; } if ($timetorestrict) { $params['timetorestrict'] = $timetorestrict; } if ($today) { $params['today'] = $today; } if ($tohour) { $params['tohour'] = $tohour; } if ($tominute) { $params['tominute'] = $tominute; } if ($tomonth) { $params['tomonth'] = $tomonth; } if ($toyear) { $params['toyear'] = $toyear; } if ($dateto) { $params['dateto'] = $dateto; } if ($starredonly) { $params['starredonly'] = $starredonly; } $PAGE->set_pagelayout('standard'); $PAGE->set_url(new moodle_url('/mod/forum/search.php', $params)); $PAGE->set_secondary_active_tab("coursehome"); if (empty($search)) { // Check the other parameters instead if (!empty($words)) { $search .= ' '.$words; } if (!empty($userid)) { $search .= ' userid:'.$userid; } if (!empty($forumid)) { $search .= ' forumid:'.$forumid; } if (!empty($user)) { $search .= ' '.forum_clean_search_terms($user, 'user:'); } if (!empty($subject)) { $search .= ' '.forum_clean_search_terms($subject, 'subject:'); } if (!empty($fullwords)) { $search .= ' '.forum_clean_search_terms($fullwords, '+'); } if (!empty($notwords)) { $search .= ' '.forum_clean_search_terms($notwords, '-'); } if (!empty($phrase)) { $search .= ' "'.$phrase.'"'; } if (!empty($datefrom)) { $search .= ' datefrom:'.$datefrom; } if (!empty($dateto)) { $search .= ' dateto:'.$dateto; } if (!empty($tags)) { $search .= ' tags:' . implode(',', $tags); } if (!empty($starredonly)) { $search .= ' starredonly:on'; } $individualparams = true; } else { $individualparams = false; } if ($search) { $search = forum_clean_search_terms($search); } if (!$course = $DB->get_record('course', array('id'=>$id))) { throw new \moodle_exception('invalidcourseid'); } require_course_login($course); $params = array( 'context' => $PAGE->context, 'other' => array('searchterm' => $search) ); $event = \mod_forum\event\course_searched::create($params); $event->trigger(); $strforums = get_string("modulenameplural", "forum"); $strsearch = get_string("search", "forum"); $strsearchresults = get_string("searchresults", "forum"); $strpage = get_string("page"); if (!$search || $showform) { $url = new moodle_url('/mod/forum/index.php', array('id' => $course->id)); $PAGE->navbar->add($strforums, $url); $url = new moodle_url('/mod/forum/search.php', array('id' => $course->id)); $PAGE->navbar->add(get_string('advancedsearch', 'forum'), $url); $PAGE->set_title($strsearch); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); forum_print_big_search_form($course); echo $OUTPUT->footer(); exit; } /// We need to do a search now and print results $searchterms = str_replace('forumid:', 'instance:', $search); $searchterms = explode(' ', $searchterms); $searchform = forum_search_form($course, $search); $PAGE->navbar->add($strsearch, new moodle_url('/mod/forum/search.php', array('id'=>$course->id))); $PAGE->navbar->add($strsearchresults); if (!$posts = forum_search_posts($searchterms, $course->id, $page*$perpage, $perpage, $totalcount)) { $PAGE->set_title($strsearchresults); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); if (!$PAGE->has_secondary_navigation()) { echo $OUTPUT->heading($strforums, 2); } $PAGE->activityheader->set_title($strsearchresults); $searchresultsheadinglevel = $PAGE->activityheader->get_heading_level(); echo $OUTPUT->heading($strsearchresults, $searchresultsheadinglevel); echo $OUTPUT->heading(get_string("noposts", "forum"), $searchresultsheadinglevel + 1); if (!$individualparams) { $words = $search; } forum_print_big_search_form($course); echo $OUTPUT->footer(); exit; } //including this here to prevent it being included if there are no search results require_once($CFG->dirroot.'/rating/lib.php'); //set up the ratings information that will be the same for all posts $ratingoptions = new stdClass(); $ratingoptions->component = 'mod_forum'; $ratingoptions->ratingarea = 'post'; $ratingoptions->userid = $USER->id; $ratingoptions->returnurl = $PAGE->url->out(false); $rm = new rating_manager(); $PAGE->set_title($strsearchresults); $PAGE->set_heading($course->fullname); $PAGE->add_header_action($searchform); echo $OUTPUT->header(); echo ''; echo $OUTPUT->heading($strforums, 2); echo $OUTPUT->heading("$strsearchresults: $totalcount", 3); $url = new moodle_url('search.php', array('search' => $search, 'id' => $course->id, 'perpage' => $perpage)); echo $OUTPUT->paging_bar($totalcount, $page, $perpage, $url); //added to implement highlighting of search terms found only in HTML markup //fiedorow - 9/2/2005 $strippedsearch = str_replace('user:','',$search); $strippedsearch = str_replace('subject:','',$strippedsearch); $strippedsearch = str_replace('"','',$strippedsearch); $searchterms = explode(' ', $strippedsearch); // Search for words independently foreach ($searchterms as $key => $searchterm) { if (preg_match('/^\-/',$searchterm)) { unset($searchterms[$key]); } else { $searchterms[$key] = preg_replace('/^\+/','',$searchterm); } } $strippedsearch = implode(' ', $searchterms); // Rebuild the string $entityfactory = mod_forum\local\container::get_entity_factory(); $vaultfactory = mod_forum\local\container::get_vault_factory(); $rendererfactory = mod_forum\local\container::get_renderer_factory(); $managerfactory = mod_forum\local\container::get_manager_factory(); $legacydatamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory(); $forumdatamapper = $legacydatamapperfactory->get_forum_data_mapper(); $discussionvault = $vaultfactory->get_discussion_vault(); $discussionids = array_keys(array_reduce($posts, function($carry, $post) { $carry[$post->discussion] = true; return $carry; }, [])); $discussions = $discussionvault->get_from_ids($discussionids); $discussionsbyid = array_reduce($discussions, function($carry, $discussion) { $carry[$discussion->get_id()] = $discussion; return $carry; }, []); $forumvault = $vaultfactory->get_forum_vault(); $forumids = array_keys(array_reduce($discussions, function($carry, $discussion) { $carry[$discussion->get_forum_id()] = true; return $carry; }, [])); $forums = $forumvault->get_from_ids($forumids); $forumsbyid = array_reduce($forums, function($carry, $forum) { $carry[$forum->get_id()] = $forum; return $carry; }, []); $postids = array_map(function($post) { return $post->id; }, $posts); $poststorender = []; foreach ($posts as $post) { // Replace the simple subject with the three items forum name -> thread name -> subject // (if all three are appropriate) each as a link. if (!isset($discussionsbyid[$post->discussion])) { throw new \moodle_exception('invaliddiscussionid', 'forum'); } $discussion = $discussionsbyid[$post->discussion]; if (!isset($forumsbyid[$discussion->get_forum_id()])) { throw new \moodle_exception('invalidforumid', 'forum'); } $forum = $forumsbyid[$discussion->get_forum_id()]; $capabilitymanager = $managerfactory->get_capability_manager($forum); $postentity = $entityfactory->get_post_from_stdclass($post); if (!$capabilitymanager->can_view_post($USER, $discussion, $postentity)) { // Don't render posts that the user can't view. continue; } if ($postentity->is_deleted()) { // Don't render deleted posts. continue; } $poststorender[] = $postentity; } $renderer = $rendererfactory->get_posts_search_results_renderer($searchterms); echo $renderer->render( $USER, $forumsbyid, $discussionsbyid, $poststorender ); echo $OUTPUT->paging_bar($totalcount, $page, $perpage, $url); echo $OUTPUT->footer(); /** * Print a full-sized search form for the specified course. * * @param stdClass $course The Course that will be searched. * @return void The function prints the form. */ function forum_print_big_search_form($course) { global $PAGE, $words, $subject, $phrase, $user, $fullwords, $notwords, $datefrom, $dateto, $forumid, $tags, $starredonly; $renderable = new \mod_forum\output\big_search_form($course, $user); $renderable->set_words($words); $renderable->set_phrase($phrase); $renderable->set_notwords($notwords); $renderable->set_fullwords($fullwords); $renderable->set_datefrom($datefrom); $renderable->set_dateto($dateto); $renderable->set_subject($subject); $renderable->set_user($user); $renderable->set_forumid($forumid); $renderable->set_tags($tags); $renderable->set_starredonly($starredonly); $output = $PAGE->get_renderer('mod_forum'); echo $output->render($renderable); } /** * This function takes each word out of the search string, makes sure they are at least * two characters long and returns an string of the space-separated search * terms. * * @param string $words String containing space-separated strings to search for. * @param string $prefix String to prepend to the each token taken out of $words. * @return string The filtered search terms, separated by spaces. * @todo Take the hardcoded limit out of this function and put it into a user-specified parameter. */ function forum_clean_search_terms($words, $prefix='') { $searchterms = explode(' ', $words); foreach ($searchterms as $key => $searchterm) { if (strlen($searchterm) < 2) { unset($searchterms[$key]); } else if ($prefix) { $searchterms[$key] = $prefix.$searchterm; } } return trim(implode(' ', $searchterms)); } /** * Retrieve a list of the forums that this user can view. * * @param stdClass $course The Course to use. * @return array A set of formatted forum names stored against the forum id. */ function forum_menu_list($course) { $menu = array(); $modinfo = get_fast_modinfo($course); if (empty($modinfo->instances['forum'])) { return $menu; } foreach ($modinfo->instances['forum'] as $cm) { if (!$cm->uservisible) { continue; } $context = context_module::instance($cm->id); if (!has_capability('mod/forum:viewdiscussion', $context)) { continue; } $menu[$cm->instance] = format_string($cm->name); } return $menu; } PK-B\8te e markposts.phpnu[. /** * Set tracking option for the forum. * * @package mod_forum * @copyright 2005 mchurch * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once("../../config.php"); require_once("lib.php"); $f = required_param('f',PARAM_INT); // The forum to mark $mark = required_param('mark',PARAM_ALPHA); // Read or unread? $d = optional_param('d',0,PARAM_INT); // Discussion to mark. $return = optional_param('return', null, PARAM_LOCALURL); // Page to return to. $url = new moodle_url('/mod/forum/markposts.php', array('f'=>$f, 'mark'=>$mark)); if ($d !== 0) { $url->param('d', $d); } if (null !== $return) { $url->param('return', $return); } $PAGE->set_url($url); if (! $forum = $DB->get_record("forum", array("id" => $f))) { throw new \moodle_exception('invalidforumid', 'forum'); } if (! $course = $DB->get_record("course", array("id" => $forum->course))) { throw new \moodle_exception('invalidcourseid'); } if (!$cm = get_coursemodule_from_instance("forum", $forum->id, $course->id)) { throw new \moodle_exception('invalidcoursemodule'); } $user = $USER; require_login($course, false, $cm); require_sesskey(); if (null === $return) { $returnto = new moodle_url("/mod/forum/index.php", ['id' => $course->id]); } else { $returnto = new moodle_url($return); } if (isguestuser()) { // Guests can't change forum $PAGE->set_title($course->shortname); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); echo $OUTPUT->confirm(get_string('noguesttracking', 'forum').'

'.get_string('liketologin'), get_login_url(), $returnto); echo $OUTPUT->footer(); exit; } $info = new stdClass(); $info->name = fullname($user); $info->forum = format_string($forum->name); if ($mark == 'read') { if (!empty($d)) { if (! $discussion = $DB->get_record('forum_discussions', array('id'=> $d, 'forum'=> $forum->id))) { throw new \moodle_exception('invaliddiscussionid', 'forum'); } forum_tp_mark_discussion_read($user, $d); } else { // Mark all messages read in current group $currentgroup = groups_get_activity_group($cm); if(!$currentgroup) { // mark_forum_read requires ===false, while get_activity_group // may return 0 $currentgroup=false; } forum_tp_mark_forum_read($user, $forum->id, $currentgroup); } } redirect($returnto); PK-B\gE%%"classes/grades/forum_gradeitem.phpnu[. /** * Grade item storage for mod_forum. * * @package mod_forum * @copyright Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types = 1); namespace mod_forum\grades; use coding_exception; use context; use core_grades\component_gradeitem; use core_grades\local\gradeitem as gradeitem; use mod_forum\local\container as forum_container; use mod_forum\local\entities\forum as forum_entity; use required_capability_exception; use stdClass; /** * Grade item storage for mod_forum. * * @package mod_forum * @copyright Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class forum_gradeitem extends component_gradeitem { /** @var forum_entity The forum entity being graded */ protected $forum; /** * Return an instance based on the context in which it is used. * * @param context $context */ public static function load_from_context(context $context): parent { // Get all the factories that are required. $vaultfactory = forum_container::get_vault_factory(); $forumvault = $vaultfactory->get_forum_vault(); $forum = $forumvault->get_from_course_module_id((int) $context->instanceid); return static::load_from_forum_entity($forum); } /** * Return an instance using the forum_entity instance. * * @param forum_entity $forum * * @return forum_gradeitem */ public static function load_from_forum_entity(forum_entity $forum): self { $instance = new static('mod_forum', $forum->get_context(), 'forum'); $instance->forum = $forum; return $instance; } /** * The table name used for grading. * * @return string */ protected function get_table_name(): string { return 'forum_grades'; } /** * Whether grading is enabled for this item. * * @return bool */ public function is_grading_enabled(): bool { return $this->forum->is_grading_enabled(); } /** * Whether the grader can grade the gradee. * * @param stdClass $gradeduser The user being graded * @param stdClass $grader The user who is grading * @return bool */ public function user_can_grade(stdClass $gradeduser, stdClass $grader): bool { // Validate the required capabilities. $managerfactory = forum_container::get_manager_factory(); $capabilitymanager = $managerfactory->get_capability_manager($this->forum); return $capabilitymanager->can_grade($grader, $gradeduser); } /** * Require that the user can grade, throwing an exception if not. * * @param stdClass $gradeduser The user being graded * @param stdClass $grader The user who is grading * @throws required_capability_exception */ public function require_user_can_grade(stdClass $gradeduser, stdClass $grader): void { if (!$this->user_can_grade($gradeduser, $grader)) { throw new required_capability_exception($this->forum->get_context(), 'mod/forum:grade', 'nopermissions', ''); } } /** * Get the grade value for this instance. * The itemname is translated to the relevant grade field on the forum entity. * * @return int */ protected function get_gradeitem_value(): int { $getter = "get_grade_for_{$this->itemname}"; return $this->forum->{$getter}(); } /** * Create an empty forum_grade for the specified user and grader. * * @param stdClass $gradeduser The user being graded * @param stdClass $grader The user who is grading * @return stdClass The newly created grade record * @throws \dml_exception */ public function create_empty_grade(stdClass $gradeduser, stdClass $grader): stdClass { global $DB; $grade = (object) [ 'forum' => $this->forum->get_id(), 'itemnumber' => $this->itemnumber, 'userid' => $gradeduser->id, 'timemodified' => time(), ]; $grade->timecreated = $grade->timemodified; $gradeid = $DB->insert_record($this->get_table_name(), $grade); return $DB->get_record($this->get_table_name(), ['id' => $gradeid]); } /** * Get the grade for the specified user. * * @param stdClass $gradeduser The user being graded * @param stdClass $grader The user who is grading * @return stdClass The grade value * @throws \dml_exception */ public function get_grade_for_user(stdClass $gradeduser, ?stdClass $grader = null): ?stdClass { global $DB; $params = [ 'forum' => $this->forum->get_id(), 'itemnumber' => $this->itemnumber, 'userid' => $gradeduser->id, ]; $grade = $DB->get_record($this->get_table_name(), $params); if (empty($grade)) { $grade = $this->create_empty_grade($gradeduser, $grader); } return $grade ?: null; } /** * Get the grade status for the specified user. * Check if a grade obj exists & $grade->grade !== null. * If the user has a grade return true. * * @param stdClass $gradeduser The user being graded * @return bool The grade exists * @throws \dml_exception */ public function user_has_grade(stdClass $gradeduser): bool { global $DB; $params = [ 'forum' => $this->forum->get_id(), 'itemnumber' => $this->itemnumber, 'userid' => $gradeduser->id, ]; $grade = $DB->get_record($this->get_table_name(), $params); if (empty($grade) || $grade->grade === null) { return false; } return true; } /** * Get grades for all users for the specified gradeitem. * * @return stdClass[] The grades * @throws \dml_exception */ public function get_all_grades(): array { global $DB; return $DB->get_records($this->get_table_name(), [ 'forum' => $this->forum->get_id(), 'itemnumber' => $this->itemnumber, ]); } /** * Get the grade item instance id. * * This is typically the cmid in the case of an activity, and relates to the iteminstance field in the grade_items * table. * * @return int */ public function get_grade_instance_id(): int { return (int) $this->forum->get_id(); } /** * Defines whether only active users in the course should be gradeable. * * @return bool Whether only active users in the course should be gradeable. */ public function should_grade_only_active_users(): bool { global $CFG; $showonlyactiveenrolconfig = !empty($CFG->grade_report_showonlyactiveenrol); // Grade only active users enrolled in the course either when the 'grade_report_showonlyactiveenrol' user // preference is set to true or the current user does not have the capability to view suspended users in the // course. In cases where the 'grade_report_showonlyactiveenrol' user preference is not set we are falling back // to the set value for the 'grade_report_showonlyactiveenrol' config. return get_user_preferences('grade_report_showonlyactiveenrol', $showonlyactiveenrolconfig) || !has_capability('moodle/course:viewsuspendedusers', \context_course::instance($this->forum->get_course_id())); } /** * Create or update the grade. * * @param stdClass $grade * @return bool Success * @throws \dml_exception * @throws \moodle_exception * @throws coding_exception */ protected function store_grade(stdClass $grade): bool { global $CFG, $DB; require_once("{$CFG->dirroot}/mod/forum/lib.php"); if ($grade->forum != $this->forum->get_id()) { throw new coding_exception('Incorrect forum provided for this grade'); } if ($grade->itemnumber != $this->itemnumber) { throw new coding_exception('Incorrect itemnumber provided for this grade'); } // Ensure that the grade is valid. $this->check_grade_validity($grade->grade); $grade->forum = $this->forum->get_id(); $grade->timemodified = time(); $DB->update_record($this->get_table_name(), $grade); // Update in the gradebook (note that 'cmidnumber' is required in order to update grades). $mapper = forum_container::get_legacy_data_mapper_factory()->get_forum_data_mapper(); $forumrecord = $mapper->to_legacy_object($this->forum); $forumrecord->cmidnumber = $this->forum->get_course_module_record()->idnumber; forum_update_grades($forumrecord, $grade->userid); return true; } } PK-B\! '!88classes/grades/gradeitems.phpnu[. /** * Grade item mappings for the activity. * * @package mod_forum * @copyright Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types = 1); namespace mod_forum\grades; use \core_grades\local\gradeitem\itemnumber_mapping; use \core_grades\local\gradeitem\advancedgrading_mapping as advanced_mapping; /** * Grade item mappings for the activity. * * @package mod_forum * @copyright Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class gradeitems implements itemnumber_mapping, advanced_mapping { /** * Return the list of grade item mappings for the forum. * * @return array */ public static function get_itemname_mapping_for_component(): array { return [ 0 => 'rating', 1 => 'forum', ]; } /** * Get the list of advanced grading item names for this component. * * @return array */ public static function get_advancedgrading_itemnames(): array { return [ 'forum', ]; } } PK-B\%GGclasses/form/export_form.phpnu[. /** * This file contains the form definition for discussion export. * * @package mod_forum * @copyright 2019 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\form; defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); require_once($CFG->dirroot.'/mod/forum/lib.php'); require_once($CFG->libdir.'/formslib.php'); /** * Export discussion form. * * @package mod_forum * @copyright 2019 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL Juv3 or later */ class export_form extends \moodleform { /** * Define the form - called by parent constructor */ public function definition() { $mform = $this->_form; $forum = $this->_customdata['forum']; $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $mform->setDefault('id', $forum->get_id()); $options = [ 'ajax' => 'mod_forum/form-user-selector', 'multiple' => true, 'noselectionstring' => get_string('allusers', 'mod_forum'), 'data-contextid' => $forum->get_context()->id, 'courseid' => $forum->get_course_id(), 'valuehtmlcallback' => function($value) { global $OUTPUT; $userfieldsapi = \core_user\fields::for_name(); $allusernames = $userfieldsapi->get_sql('', false, '', '', false)->selects; $fields = 'id, ' . $allusernames; $user = \core_user::get_user($value, $fields); $useroptiondata = [ 'fullname' => fullname($user), ]; return $OUTPUT->render_from_template('mod_forum/form-user-selector-suggestion', $useroptiondata); } ]; $mform->addElement('autocomplete', 'useridsselected', get_string('users'), [], $options); $vaultfactory = \mod_forum\local\container::get_vault_factory(); $discussionvault = $vaultfactory->get_discussion_vault(); $discussions = array_map(function($discussion) { return $discussion->get_name(); }, $discussionvault->get_all_discussions_in_forum($forum)); $options = [ 'multiple' => true, 'noselectionstring' => get_string('alldiscussions', 'mod_forum'), ]; $mform->addElement('autocomplete', 'discussionids', get_string('discussions', 'mod_forum'), $discussions, $options); // Date fields. $mform->addElement('date_time_selector', 'from', get_string('postsfrom', 'mod_forum'), ['optional' => true]); $mform->addElement('date_time_selector', 'to', get_string('poststo', 'mod_forum'), ['optional' => true]); // Export formats. $formats = \core_plugin_manager::instance()->get_plugins_of_type('dataformat'); $options = []; foreach ($formats as $format) { $options[$format->name] = $format->displayname; } $mform->addElement('select', 'format', 'Format', $options); $mform->addElement('header', 'optionsheader', get_string('exportoptions', 'mod_forum')); $mform->addElement('checkbox', 'striphtml', '', get_string('exportstriphtml', 'mod_forum')); $mform->addHelpButton('striphtml', 'exportstriphtml', 'mod_forum'); $mform->addElement('checkbox', 'humandates', '', get_string('exporthumandates', 'mod_forum')); $mform->addHelpButton('humandates', 'exporthumandates', 'mod_forum'); $this->add_action_buttons(true, get_string('export', 'mod_forum')); } } PK-B\Sp@ttclasses/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for mod_forum. * * @package mod_forum * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\privacy; use core_grades\component_gradeitem as gradeitem; use \core_privacy\local\request\userlist; use \core_privacy\local\request\approved_contextlist; use \core_privacy\local\request\approved_userlist; use \core_privacy\local\request\deletion_criteria; use \core_privacy\local\request\writer; use \core_privacy\local\request\helper as request_helper; use \core_privacy\local\metadata\collection; use \core_privacy\local\request\transform; use tool_dataprivacy\context_instance; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/grade/grading/lib.php'); /** * Implementation of the privacy subsystem plugin provider for the forum activity module. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements // This plugin has data. \core_privacy\local\metadata\provider, // This plugin currently implements the original plugin\provider interface. \core_privacy\local\request\plugin\provider, // This plugin is capable of determining which users have data within it. \core_privacy\local\request\core_userlist_provider, // This plugin has some sitewide user preferences to export. \core_privacy\local\request\user_preference_provider { use subcontext_info; /** * Returns meta data about this system. * * @param collection $items The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $items): collection { // The 'forum' table does not store any specific user data. $items->add_database_table('forum_digests', [ 'forum' => 'privacy:metadata:forum_digests:forum', 'userid' => 'privacy:metadata:forum_digests:userid', 'maildigest' => 'privacy:metadata:forum_digests:maildigest', ], 'privacy:metadata:forum_digests'); // The 'forum_discussions' table stores the metadata about each forum discussion. $items->add_database_table('forum_discussions', [ 'name' => 'privacy:metadata:forum_discussions:name', 'userid' => 'privacy:metadata:forum_discussions:userid', 'assessed' => 'privacy:metadata:forum_discussions:assessed', 'timemodified' => 'privacy:metadata:forum_discussions:timemodified', 'usermodified' => 'privacy:metadata:forum_discussions:usermodified', ], 'privacy:metadata:forum_discussions'); // The 'forum_discussion_subs' table stores information about which discussions a user is subscribed to. $items->add_database_table('forum_discussion_subs', [ 'discussionid' => 'privacy:metadata:forum_discussion_subs:discussionid', 'preference' => 'privacy:metadata:forum_discussion_subs:preference', 'userid' => 'privacy:metadata:forum_discussion_subs:userid', ], 'privacy:metadata:forum_discussion_subs'); // The 'forum_posts' table stores the metadata about each forum discussion. $items->add_database_table('forum_posts', [ 'discussion' => 'privacy:metadata:forum_posts:discussion', 'parent' => 'privacy:metadata:forum_posts:parent', 'created' => 'privacy:metadata:forum_posts:created', 'modified' => 'privacy:metadata:forum_posts:modified', 'subject' => 'privacy:metadata:forum_posts:subject', 'message' => 'privacy:metadata:forum_posts:message', 'userid' => 'privacy:metadata:forum_posts:userid', 'privatereplyto' => 'privacy:metadata:forum_posts:privatereplyto', ], 'privacy:metadata:forum_posts'); // The 'forum_queue' table contains user data, but it is only a temporary cache of other data. // We should not need to export it as it does not allow profiling of a user. // The 'forum_read' table stores data about which forum posts have been read by each user. $items->add_database_table('forum_read', [ 'userid' => 'privacy:metadata:forum_read:userid', 'discussionid' => 'privacy:metadata:forum_read:discussionid', 'postid' => 'privacy:metadata:forum_read:postid', 'firstread' => 'privacy:metadata:forum_read:firstread', 'lastread' => 'privacy:metadata:forum_read:lastread', ], 'privacy:metadata:forum_read'); // The 'forum_subscriptions' table stores information about which forums a user is subscribed to. $items->add_database_table('forum_subscriptions', [ 'userid' => 'privacy:metadata:forum_subscriptions:userid', 'forum' => 'privacy:metadata:forum_subscriptions:forum', ], 'privacy:metadata:forum_subscriptions'); // The 'forum_subscriptions' table stores information about which forums a user is subscribed to. $items->add_database_table('forum_track_prefs', [ 'userid' => 'privacy:metadata:forum_track_prefs:userid', 'forumid' => 'privacy:metadata:forum_track_prefs:forumid', ], 'privacy:metadata:forum_track_prefs'); // The 'forum_queue' table stores temporary data that is not exported/deleted. $items->add_database_table('forum_queue', [ 'userid' => 'privacy:metadata:forum_queue:userid', 'discussionid' => 'privacy:metadata:forum_queue:discussionid', 'postid' => 'privacy:metadata:forum_queue:postid', 'timemodified' => 'privacy:metadata:forum_queue:timemodified' ], 'privacy:metadata:forum_queue'); // The 'forum_grades' table stores grade data. $items->add_database_table('forum_grades', [ 'userid' => 'privacy:metadata:forum_grades:userid', 'forum' => 'privacy:metadata:forum_grades:forum', 'grade' => 'privacy:metadata:forum_grades:grade', ], 'privacy:metadata:forum_grades'); // Forum posts can be tagged and rated. $items->link_subsystem('core_tag', 'privacy:metadata:core_tag'); $items->link_subsystem('core_rating', 'privacy:metadata:core_rating'); // There are several user preferences. $items->add_user_preference('maildigest', 'privacy:metadata:preference:maildigest'); $items->add_user_preference('autosubscribe', 'privacy:metadata:preference:autosubscribe'); $items->add_user_preference('trackforums', 'privacy:metadata:preference:trackforums'); $items->add_user_preference('markasreadonnotification', 'privacy:metadata:preference:markasreadonnotification'); $items->add_user_preference('forum_discussionlistsortorder', 'privacy:metadata:preference:forum_discussionlistsortorder'); return $items; } /** * Get the list of contexts that contain user information for the specified user. * * In the case of forum, that is any forum where the user has made any post, rated any content, or has any preferences. * * @param int $userid The user to search. * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid): \core_privacy\local\request\contextlist { $contextlist = new \core_privacy\local\request\contextlist(); $params = [ 'modname' => 'forum', 'contextlevel' => CONTEXT_MODULE, 'userid' => $userid, ]; // Discussion creators. $sql = "SELECT c.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {forum} f ON f.id = cm.instance JOIN {forum_discussions} d ON d.forum = f.id WHERE d.userid = :userid "; $contextlist->add_from_sql($sql, $params); // Post authors. $sql = "SELECT c.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {forum} f ON f.id = cm.instance JOIN {forum_discussions} d ON d.forum = f.id JOIN {forum_posts} p ON p.discussion = d.id WHERE p.userid = :userid "; $contextlist->add_from_sql($sql, $params); // Forum digest records. $sql = "SELECT c.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {forum} f ON f.id = cm.instance JOIN {forum_digests} dig ON dig.forum = f.id WHERE dig.userid = :userid "; $contextlist->add_from_sql($sql, $params); // Forum subscriptions. $sql = "SELECT c.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {forum} f ON f.id = cm.instance JOIN {forum_subscriptions} sub ON sub.forum = f.id WHERE sub.userid = :userid "; $contextlist->add_from_sql($sql, $params); // Discussion subscriptions. $sql = "SELECT c.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {forum} f ON f.id = cm.instance JOIN {forum_discussion_subs} dsub ON dsub.forum = f.id WHERE dsub.userid = :userid "; $contextlist->add_from_sql($sql, $params); // Discussion tracking preferences. $sql = "SELECT c.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {forum} f ON f.id = cm.instance JOIN {forum_track_prefs} pref ON pref.forumid = f.id WHERE pref.userid = :userid "; $contextlist->add_from_sql($sql, $params); // Discussion read records. $sql = "SELECT c.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {forum} f ON f.id = cm.instance JOIN {forum_read} hasread ON hasread.forumid = f.id WHERE hasread.userid = :userid "; $contextlist->add_from_sql($sql, $params); // Rating authors. $ratingsql = \core_rating\privacy\provider::get_sql_join('rat', 'mod_forum', 'post', 'p.id', $userid, true); $sql = "SELECT c.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {forum} f ON f.id = cm.instance JOIN {forum_discussions} d ON d.forum = f.id JOIN {forum_posts} p ON p.discussion = d.id {$ratingsql->join} WHERE {$ratingsql->userwhere} "; $params += $ratingsql->params; $contextlist->add_from_sql($sql, $params); // Forum grades. $sql = "SELECT c.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {forum} f ON f.id = cm.instance JOIN {forum_grades} fg ON fg.forum = f.id WHERE fg.userid = :userid "; $contextlist->add_from_sql($sql, $params); return $contextlist; } /** * Get the list of users within a specific context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { $context = $userlist->get_context(); if (!is_a($context, \context_module::class)) { return; } $params = [ 'instanceid' => $context->instanceid, 'modulename' => 'forum', ]; // Discussion authors. $sql = "SELECT d.userid FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {forum} f ON f.id = cm.instance JOIN {forum_discussions} d ON d.forum = f.id WHERE cm.id = :instanceid"; $userlist->add_from_sql('userid', $sql, $params); // Forum authors. $sql = "SELECT p.userid FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {forum} f ON f.id = cm.instance JOIN {forum_discussions} d ON d.forum = f.id JOIN {forum_posts} p ON d.id = p.discussion WHERE cm.id = :instanceid"; $userlist->add_from_sql('userid', $sql, $params); // Forum post ratings. $sql = "SELECT p.id FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {forum} f ON f.id = cm.instance JOIN {forum_discussions} d ON d.forum = f.id JOIN {forum_posts} p ON d.id = p.discussion WHERE cm.id = :instanceid"; \core_rating\privacy\provider::get_users_in_context_from_sql($userlist, 'rat', 'mod_forum', 'post', $sql, $params); // Forum Digest settings. $sql = "SELECT dig.userid FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {forum} f ON f.id = cm.instance JOIN {forum_digests} dig ON dig.forum = f.id WHERE cm.id = :instanceid"; $userlist->add_from_sql('userid', $sql, $params); // Forum Subscriptions. $sql = "SELECT sub.userid FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {forum} f ON f.id = cm.instance JOIN {forum_subscriptions} sub ON sub.forum = f.id WHERE cm.id = :instanceid"; $userlist->add_from_sql('userid', $sql, $params); // Discussion subscriptions. $sql = "SELECT dsub.userid FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {forum} f ON f.id = cm.instance JOIN {forum_discussion_subs} dsub ON dsub.forum = f.id WHERE cm.id = :instanceid"; $userlist->add_from_sql('userid', $sql, $params); // Read Posts. $sql = "SELECT hasread.userid FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {forum} f ON f.id = cm.instance JOIN {forum_read} hasread ON hasread.forumid = f.id WHERE cm.id = :instanceid"; $userlist->add_from_sql('userid', $sql, $params); // Tracking Preferences. $sql = "SELECT pref.userid FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {forum} f ON f.id = cm.instance JOIN {forum_track_prefs} pref ON pref.forumid = f.id WHERE cm.id = :instanceid"; $userlist->add_from_sql('userid', $sql, $params); // Forum grades. $sql = "SELECT fg.userid FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {forum} f ON f.id = cm.instance JOIN {forum_grades} fg ON fg.forum = f.id WHERE cm.id = :instanceid"; $userlist->add_from_sql('userid', $sql, $params); } /** * Store all user preferences for the plugin. * * @param int $userid The userid of the user whose data is to be exported. */ public static function export_user_preferences(int $userid) { $user = \core_user::get_user($userid); switch ($user->maildigest) { case 1: $digestdescription = get_string('emaildigestcomplete'); break; case 2: $digestdescription = get_string('emaildigestsubjects'); break; case 0: default: $digestdescription = get_string('emaildigestoff'); break; } writer::export_user_preference('mod_forum', 'maildigest', $user->maildigest, $digestdescription); switch ($user->autosubscribe) { case 0: $subscribedescription = get_string('autosubscribeno'); break; case 1: default: $subscribedescription = get_string('autosubscribeyes'); break; } writer::export_user_preference('mod_forum', 'autosubscribe', $user->autosubscribe, $subscribedescription); switch ($user->trackforums) { case 0: $trackforumdescription = get_string('trackforumsno'); break; case 1: default: $trackforumdescription = get_string('trackforumsyes'); break; } writer::export_user_preference('mod_forum', 'trackforums', $user->trackforums, $trackforumdescription); $markasreadonnotification = get_user_preferences('markasreadonnotification', null, $user->id); if (null !== $markasreadonnotification) { switch ($markasreadonnotification) { case 0: $markasreadonnotificationdescription = get_string('markasreadonnotificationno', 'mod_forum'); break; case 1: default: $markasreadonnotificationdescription = get_string('markasreadonnotificationyes', 'mod_forum'); break; } writer::export_user_preference('mod_forum', 'markasreadonnotification', $markasreadonnotification, $markasreadonnotificationdescription); } $vaultfactory = \mod_forum\local\container::get_vault_factory(); $discussionlistvault = $vaultfactory->get_discussions_in_forum_vault(); $discussionlistsortorder = get_user_preferences('forum_discussionlistsortorder', $discussionlistvault::SORTORDER_LASTPOST_DESC, $user->id); switch ($discussionlistsortorder) { case $discussionlistvault::SORTORDER_LASTPOST_DESC: $discussionlistsortorderdescription = get_string('discussionlistsortbylastpostdesc', 'mod_forum'); break; case $discussionlistvault::SORTORDER_LASTPOST_ASC: $discussionlistsortorderdescription = get_string('discussionlistsortbylastpostasc', 'mod_forum'); break; case $discussionlistvault::SORTORDER_CREATED_DESC: $discussionlistsortorderdescription = get_string('discussionlistsortbycreateddesc', 'mod_forum'); break; case $discussionlistvault::SORTORDER_CREATED_ASC: $discussionlistsortorderdescription = get_string('discussionlistsortbycreatedasc', 'mod_forum'); break; case $discussionlistvault::SORTORDER_REPLIES_DESC: $discussionlistsortorderdescription = get_string('discussionlistsortbyrepliesdesc', 'mod_forum'); break; case $discussionlistvault::SORTORDER_REPLIES_ASC: $discussionlistsortorderdescription = get_string('discussionlistsortbyrepliesasc', 'mod_forum'); break; case $discussionlistvault::SORTORDER_DISCUSSION_DESC: $discussionlistsortorderdescription = get_string('discussionlistsortbydiscussiondesc', 'mod_forum'); break; case $discussionlistvault::SORTORDER_DISCUSSION_ASC: $discussionlistsortorderdescription = get_string('discussionlistsortbydiscussionasc', 'mod_forum'); break; case $discussionlistvault::SORTORDER_STARTER_DESC: $discussionlistsortorderdescription = get_string('discussionlistsortbystarterdesc', 'mod_forum'); break; case $discussionlistvault::SORTORDER_STARTER_ASC: $discussionlistsortorderdescription = get_string('discussionlistsortbystarterasc', 'mod_forum'); break; case $discussionlistvault::SORTORDER_GROUP_DESC: $discussionlistsortorderdescription = get_string('discussionlistsortbygroupdesc', 'mod_forum'); break; case $discussionlistvault::SORTORDER_GROUP_ASC: $discussionlistsortorderdescription = get_string('discussionlistsortbygroupasc', 'mod_forum'); break; } writer::export_user_preference('mod_forum', 'forum_discussionlistsortorder', $discussionlistsortorder, $discussionlistsortorderdescription); } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { global $DB; if (empty($contextlist)) { return; } $user = $contextlist->get_user(); $userid = $user->id; list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); $params = $contextparams; // Digested forums. $sql = "SELECT c.id AS contextid, dig.maildigest AS maildigest FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid JOIN {forum} f ON f.id = cm.instance JOIN {forum_digests} dig ON dig.forum = f.id WHERE ( dig.userid = :userid AND c.id {$contextsql} ) "; $params['userid'] = $userid; $digests = $DB->get_records_sql_menu($sql, $params); // Forum subscriptions. $sql = "SELECT c.id AS contextid, sub.userid AS subscribed FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid JOIN {forum} f ON f.id = cm.instance JOIN {forum_subscriptions} sub ON sub.forum = f.id WHERE ( sub.userid = :userid AND c.id {$contextsql} ) "; $params['userid'] = $userid; $subscriptions = $DB->get_records_sql_menu($sql, $params); // Tracked forums. $sql = "SELECT c.id AS contextid, pref.userid AS tracked FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid JOIN {forum} f ON f.id = cm.instance JOIN {forum_track_prefs} pref ON pref.forumid = f.id WHERE ( pref.userid = :userid AND c.id {$contextsql} ) "; $params['userid'] = $userid; $tracked = $DB->get_records_sql_menu($sql, $params); // Forum grades. $sql = "SELECT c.id AS contextid, fg.grade AS grade, f.grade_forum AS gradetype FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid JOIN {forum} f ON f.id = cm.instance JOIN {forum_grades} fg ON fg.forum = f.id WHERE ( fg.userid = :userid AND c.id {$contextsql} ) "; $params['userid'] = $userid; $grades = $DB->get_records_sql_menu($sql, $params); $sql = "SELECT c.id AS contextid, f.*, cm.id AS cmid FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid JOIN {forum} f ON f.id = cm.instance WHERE ( c.id {$contextsql} ) "; $params += $contextparams; // Keep a mapping of forumid to contextid. $mappings = []; $forums = $DB->get_recordset_sql($sql, $params); foreach ($forums as $forum) { $mappings[$forum->id] = $forum->contextid; $context = \context::instance_by_id($mappings[$forum->id]); // Store the main forum data. $data = request_helper::get_context_data($context, $user); writer::with_context($context) ->export_data([], $data); request_helper::export_context_files($context, $user); // Store relevant metadata about this forum instance. if (isset($digests[$forum->contextid])) { static::export_digest_data($userid, $forum, $digests[$forum->contextid]); } if (isset($subscriptions[$forum->contextid])) { static::export_subscription_data($userid, $forum, $subscriptions[$forum->contextid]); } if (isset($tracked[$forum->contextid])) { static::export_tracking_data($userid, $forum, $tracked[$forum->contextid]); } if (isset($grades[$forum->contextid])) { static::export_grading_data($userid, $forum, $grades[$forum->contextid]); } } $forums->close(); if (!empty($mappings)) { // Store all discussion data for this forum. static::export_discussion_data($userid, $mappings); // Store all post data for this forum. static::export_all_posts($userid, $mappings); } } /** * Store all information about all discussions that we have detected this user to have access to. * * @param int $userid The userid of the user whose data is to be exported. * @param array $mappings A list of mappings from forumid => contextid. * @return array Which forums had data written for them. */ protected static function export_discussion_data(int $userid, array $mappings) { global $DB; // Find all of the discussions, and discussion subscriptions for this forum. list($foruminsql, $forumparams) = $DB->get_in_or_equal(array_keys($mappings), SQL_PARAMS_NAMED); $sql = "SELECT d.*, g.name as groupname, dsub.preference FROM {forum} f JOIN {forum_discussions} d ON d.forum = f.id LEFT JOIN {groups} g ON g.id = d.groupid LEFT JOIN {forum_discussion_subs} dsub ON dsub.discussion = d.id AND dsub.userid = :dsubuserid LEFT JOIN {forum_posts} p ON p.discussion = d.id WHERE f.id {$foruminsql} AND ( d.userid = :discussionuserid OR p.userid = :postuserid OR dsub.id IS NOT NULL ) "; $params = [ 'postuserid' => $userid, 'discussionuserid' => $userid, 'dsubuserid' => $userid, ]; $params += $forumparams; // Keep track of the forums which have data. $forumswithdata = []; $discussions = $DB->get_recordset_sql($sql, $params); foreach ($discussions as $discussion) { // No need to take timestart into account as the user has some involvement already. // Ignore discussion timeend as it should not block access to user data. $forumswithdata[$discussion->forum] = true; $context = \context::instance_by_id($mappings[$discussion->forum]); // Store related metadata for this discussion. static::export_discussion_subscription_data($userid, $context, $discussion); $discussiondata = (object) [ 'name' => format_string($discussion->name, true), 'pinned' => transform::yesno((bool) $discussion->pinned), 'timemodified' => transform::datetime($discussion->timemodified), 'usermodified' => transform::datetime($discussion->usermodified), 'creator_was_you' => transform::yesno($discussion->userid == $userid), ]; // Store the discussion content. writer::with_context($context) ->export_data(static::get_discussion_area($discussion), $discussiondata); // Forum discussions do not have any files associately directly with them. } $discussions->close(); return $forumswithdata; } /** * Store all information about all posts that we have detected this user to have access to. * * @param int $userid The userid of the user whose data is to be exported. * @param array $mappings A list of mappings from forumid => contextid. * @return array Which forums had data written for them. */ protected static function export_all_posts(int $userid, array $mappings) { global $DB; $commonsql = "SELECT p.discussion AS id, f.id AS forumid, d.name, d.groupid FROM {forum} f JOIN {forum_discussions} d ON d.forum = f.id JOIN {forum_posts} p ON p.discussion = d.id"; // All discussions with posts authored by the user or containing private replies to the user. list($foruminsql1, $forumparams1) = $DB->get_in_or_equal(array_keys($mappings), SQL_PARAMS_NAMED); $sql1 = "{$commonsql} WHERE f.id {$foruminsql1} AND (p.userid = :postuserid OR p.privatereplyto = :privatereplyrecipient)"; // All discussions with the posts marked as read by the user. list($foruminsql2, $forumparams2) = $DB->get_in_or_equal(array_keys($mappings), SQL_PARAMS_NAMED); $sql2 = "{$commonsql} JOIN {forum_read} fr ON fr.postid = p.id WHERE f.id {$foruminsql2} AND fr.userid = :readuserid"; // All discussions with ratings provided by the user. list($foruminsql3, $forumparams3) = $DB->get_in_or_equal(array_keys($mappings), SQL_PARAMS_NAMED); $ratingsql = \core_rating\privacy\provider::get_sql_join('rat', 'mod_forum', 'post', 'p.id', $userid, true); $sql3 = "{$commonsql} {$ratingsql->join} WHERE f.id {$foruminsql3} AND {$ratingsql->userwhere}"; $sql = "SELECT * FROM ({$sql1} UNION {$sql2} UNION {$sql3}) united GROUP BY id, forumid, name, groupid"; $params = [ 'postuserid' => $userid, 'readuserid' => $userid, 'privatereplyrecipient' => $userid, ]; $params += $forumparams1; $params += $forumparams2; $params += $forumparams3; $params += $ratingsql->params; $discussions = $DB->get_records_sql($sql, $params); foreach ($discussions as $discussion) { $context = \context::instance_by_id($mappings[$discussion->forumid]); static::export_all_posts_in_discussion($userid, $context, $discussion); } } /** * Store all information about all posts that we have detected this user to have access to. * * @param int $userid The userid of the user whose data is to be exported. * @param \context $context The instance of the forum context. * @param \stdClass $discussion The discussion whose data is being exported. */ protected static function export_all_posts_in_discussion(int $userid, \context $context, \stdClass $discussion) { global $DB, $USER; $discussionid = $discussion->id; // Find all of the posts, and post subscriptions for this forum. $ratingsql = \core_rating\privacy\provider::get_sql_join('rat', 'mod_forum', 'post', 'p.id', $userid); $sql = "SELECT p.*, d.forum AS forumid, fr.firstread, fr.lastread, fr.id AS readflag, rat.id AS hasratings FROM {forum_discussions} d JOIN {forum_posts} p ON p.discussion = d.id LEFT JOIN {forum_read} fr ON fr.postid = p.id AND fr.userid = :readuserid {$ratingsql->join} AND {$ratingsql->userwhere} WHERE d.id = :discussionid AND ( p.privatereplyto = 0 OR p.privatereplyto = :privatereplyrecipient OR p.userid = :privatereplyauthor ) "; $params = [ 'discussionid' => $discussionid, 'readuserid' => $userid, 'privatereplyrecipient' => $userid, 'privatereplyauthor' => $userid, ]; $params += $ratingsql->params; // Keep track of the forums which have data. $structure = (object) [ 'children' => [], ]; $posts = $DB->get_records_sql($sql, $params); foreach ($posts as $post) { $post->hasdata = (isset($post->hasdata)) ? $post->hasdata : false; $post->hasdata = $post->hasdata || !empty($post->hasratings); $post->hasdata = $post->hasdata || $post->readflag; $post->hasdata = $post->hasdata || ($post->userid == $USER->id); $post->hasdata = $post->hasdata || ($post->privatereplyto == $USER->id); if (0 == $post->parent) { $structure->children[$post->id] = $post; } else { if (empty($posts[$post->parent]->children)) { $posts[$post->parent]->children = []; } $posts[$post->parent]->children[$post->id] = $post; } // Set all parents. if ($post->hasdata) { $curpost = $post; while ($curpost->parent != 0) { $curpost = $posts[$curpost->parent]; $curpost->hasdata = true; } } } $discussionarea = static::get_discussion_area($discussion); $discussionarea[] = get_string('posts', 'mod_forum'); static::export_posts_in_structure($userid, $context, $discussionarea, $structure); } /** * Export all posts in the provided structure. * * @param int $userid The userid of the user whose data is to be exported. * @param \context $context The instance of the forum context. * @param array $parentarea The subcontext of the parent. * @param \stdClass $structure The post structure and all of its children */ protected static function export_posts_in_structure(int $userid, \context $context, $parentarea, \stdClass $structure) { foreach ($structure->children as $post) { if (!$post->hasdata) { // This tree has no content belonging to the user. Skip it and all children. continue; } $postarea = array_merge($parentarea, static::get_post_area($post)); // Store the post content. static::export_post_data($userid, $context, $postarea, $post); if (isset($post->children)) { // Now export children of this post. static::export_posts_in_structure($userid, $context, $postarea, $post); } } } /** * Export all data in the post. * * @param int $userid The userid of the user whose data is to be exported. * @param \context $context The instance of the forum context. * @param array $postarea The subcontext of the parent. * @param \stdClass $post The post structure and all of its children */ protected static function export_post_data(int $userid, \context $context, $postarea, $post) { // Store related metadata. static::export_read_data($userid, $context, $postarea, $post); $postdata = (object) [ 'subject' => format_string($post->subject, true), 'created' => transform::datetime($post->created), 'modified' => transform::datetime($post->modified), 'author_was_you' => transform::yesno($post->userid == $userid), ]; if (!empty($post->privatereplyto)) { $postdata->privatereply = transform::yesno(true); } $postdata->message = writer::with_context($context) ->rewrite_pluginfile_urls($postarea, 'mod_forum', 'post', $post->id, $post->message); $postdata->message = format_text($postdata->message, $post->messageformat, (object) [ 'para' => false, 'trusted' => $post->messagetrust, 'context' => $context, ]); writer::with_context($context) // Store the post. ->export_data($postarea, $postdata) // Store the associated files. ->export_area_files($postarea, 'mod_forum', 'post', $post->id); if ($post->userid == $userid) { // Store all ratings against this post as the post belongs to the user. All ratings on it are ratings of their content. \core_rating\privacy\provider::export_area_ratings($userid, $context, $postarea, 'mod_forum', 'post', $post->id, false); // Store all tags against this post as the tag belongs to the user. \core_tag\privacy\provider::export_item_tags($userid, $context, $postarea, 'mod_forum', 'forum_posts', $post->id); // Export all user data stored for this post from the plagiarism API. $coursecontext = $context->get_course_context(); \core_plagiarism\privacy\provider::export_plagiarism_user_data($userid, $context, $postarea, [ 'cmid' => $context->instanceid, 'course' => $coursecontext->instanceid, 'forum' => $post->forumid, 'discussionid' => $post->discussion, 'postid' => $post->id, ]); } // Check for any ratings that the user has made on this post. \core_rating\privacy\provider::export_area_ratings($userid, $context, $postarea, 'mod_forum', 'post', $post->id, $userid, true ); } /** * Store data about daily digest preferences * * @param int $userid The userid of the user whose data is to be exported. * @param \stdClass $forum The forum whose data is being exported. * @param int $maildigest The mail digest setting for this forum. * @return bool Whether any data was stored. */ protected static function export_digest_data(int $userid, \stdClass $forum, int $maildigest) { if (null !== $maildigest) { // The user has a specific maildigest preference for this forum. $a = (object) [ 'forum' => format_string($forum->name, true), ]; switch ($maildigest) { case 0: $a->type = get_string('emaildigestoffshort', 'mod_forum'); break; case 1: $a->type = get_string('emaildigestcompleteshort', 'mod_forum'); break; case 2: $a->type = get_string('emaildigestsubjectsshort', 'mod_forum'); break; } writer::with_context(\context_module::instance($forum->cmid)) ->export_metadata([], 'digestpreference', $maildigest, get_string('privacy:digesttypepreference', 'mod_forum', $a)); return true; } return false; } /** * Store data about whether the user subscribes to forum. * * @param int $userid The userid of the user whose data is to be exported. * @param \stdClass $forum The forum whose data is being exported. * @param int $subscribed if the user is subscribed * @return bool Whether any data was stored. */ protected static function export_subscription_data(int $userid, \stdClass $forum, int $subscribed) { if (null !== $subscribed) { // The user is subscribed to this forum. writer::with_context(\context_module::instance($forum->cmid)) ->export_metadata([], 'subscriptionpreference', 1, get_string('privacy:subscribedtoforum', 'mod_forum')); return true; } return false; } /** * Store data about whether the user subscribes to this particular discussion. * * @param int $userid The userid of the user whose data is to be exported. * @param \context_module $context The instance of the forum context. * @param \stdClass $discussion The discussion whose data is being exported. * @return bool Whether any data was stored. */ protected static function export_discussion_subscription_data(int $userid, \context_module $context, \stdClass $discussion) { $area = static::get_discussion_area($discussion); if (null !== $discussion->preference) { // The user has a specific subscription preference for this discussion. $a = (object) []; switch ($discussion->preference) { case \mod_forum\subscriptions::FORUM_DISCUSSION_UNSUBSCRIBED: $a->preference = get_string('unsubscribed', 'mod_forum'); break; default: $a->preference = get_string('subscribed', 'mod_forum'); break; } writer::with_context($context) ->export_metadata( $area, 'subscriptionpreference', $discussion->preference, get_string('privacy:discussionsubscriptionpreference', 'mod_forum', $a) ); return true; } return true; } /** * Store forum read-tracking data about a particular forum. * * This is whether a forum has read-tracking enabled or not. * * @param int $userid The userid of the user whose data is to be exported. * @param \stdClass $forum The forum whose data is being exported. * @param int $tracke if the user is subscribed * @return bool Whether any data was stored. */ protected static function export_tracking_data(int $userid, \stdClass $forum, int $tracked) { if (null !== $tracked) { // The user has a main preference to track all forums, but has opted out of this one. writer::with_context(\context_module::instance($forum->cmid)) ->export_metadata([], 'trackreadpreference', 0, get_string('privacy:readtrackingdisabled', 'mod_forum')); return true; } return false; } protected static function export_grading_data(int $userid, \stdClass $forum, int $grade) { global $USER; if (null !== $grade) { $context = \context_module::instance($forum->cmid); $exportpath = array_merge([], [get_string('privacy:metadata:forum_grades', 'mod_forum')]); $gradingmanager = get_grading_manager($context, 'mod_forum', 'forum'); $controller = $gradingmanager->get_active_controller(); // Check for advanced grading and retrieve that information. if (isset($controller)) { $gradeduser = \core_user::get_user($userid); // Fetch the gradeitem instance. $gradeitem = gradeitem::instance($controller->get_component(), $context, $controller->get_area()); $grade = $gradeitem->get_grade_for_user($gradeduser, $USER); $controllercontext = $controller->get_context(); \core_grading\privacy\provider::export_item_data($controllercontext, $grade->id, $exportpath); } else { self::export_grade_data($grade, $context, $forum, $exportpath); } // The user has a grade for this forum. writer::with_context(\context_module::instance($forum->cmid)) ->export_metadata($exportpath, 'gradingenabled', 1, get_string('privacy:metadata:forum_grades:grade', 'mod_forum')); return true; } return false; } protected static function export_grade_data(int $grade, \context $context, \stdClass $forum, array $path) { $gradedata = (object)[ 'forum' => $forum->name, 'grade' => $grade, ]; writer::with_context($context) ->export_data($path, $gradedata); } /** * Store read-tracking information about a particular forum post. * * @param int $userid The userid of the user whose data is to be exported. * @param \context_module $context The instance of the forum context. * @param array $postarea The subcontext for this post. * @param \stdClass $post The post whose data is being exported. * @return bool Whether any data was stored. */ protected static function export_read_data(int $userid, \context_module $context, array $postarea, \stdClass $post) { if (null !== $post->firstread) { $a = (object) [ 'firstread' => $post->firstread, 'lastread' => $post->lastread, ]; writer::with_context($context) ->export_metadata( $postarea, 'postread', (object) [ 'firstread' => $post->firstread, 'lastread' => $post->lastread, ], get_string('privacy:postwasread', 'mod_forum', $a) ); return true; } return false; } /** * Delete all data for all users in the specified context. * * @param context $context The specific context to delete data for. */ public static function delete_data_for_all_users_in_context(\context $context) { global $DB; // Check that this is a context_module. if (!$context instanceof \context_module) { return; } // Get the course module. if (!$cm = get_coursemodule_from_id('forum', $context->instanceid)) { return; } $forumid = $cm->instance; $DB->delete_records('forum_track_prefs', ['forumid' => $forumid]); $DB->delete_records('forum_subscriptions', ['forum' => $forumid]); $DB->delete_records('forum_grades', ['forum' => $forumid]); $DB->delete_records('forum_read', ['forumid' => $forumid]); $DB->delete_records('forum_digests', ['forum' => $forumid]); // Delete advanced grading information. $gradingmanager = get_grading_manager($context, 'mod_forum', 'forum'); $controller = $gradingmanager->get_active_controller(); if (isset($controller)) { \core_grading\privacy\provider::delete_instance_data($context); } $DB->delete_records('forum_grades', ['forum' => $forumid]); // Delete all discussion items. $DB->delete_records_select( 'forum_queue', "discussionid IN (SELECT id FROM {forum_discussions} WHERE forum = :forum)", [ 'forum' => $forumid, ] ); $DB->delete_records_select( 'forum_posts', "discussion IN (SELECT id FROM {forum_discussions} WHERE forum = :forum)", [ 'forum' => $forumid, ] ); $DB->delete_records('forum_discussion_subs', ['forum' => $forumid]); $DB->delete_records('forum_discussions', ['forum' => $forumid]); // Delete all files from the posts. $fs = get_file_storage(); $fs->delete_area_files($context->id, 'mod_forum', 'post'); $fs->delete_area_files($context->id, 'mod_forum', 'attachment'); // Delete all ratings in the context. \core_rating\privacy\provider::delete_ratings($context, 'mod_forum', 'post'); // Delete all Tags. \core_tag\privacy\provider::delete_item_tags($context, 'mod_forum', 'forum_posts'); } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { global $DB; $user = $contextlist->get_user(); $userid = $user->id; foreach ($contextlist as $context) { // Get the course module. $cm = $DB->get_record('course_modules', ['id' => $context->instanceid]); $forum = $DB->get_record('forum', ['id' => $cm->instance]); $DB->delete_records('forum_track_prefs', [ 'forumid' => $forum->id, 'userid' => $userid, ]); $DB->delete_records('forum_subscriptions', [ 'forum' => $forum->id, 'userid' => $userid, ]); $DB->delete_records('forum_read', [ 'forumid' => $forum->id, 'userid' => $userid, ]); $DB->delete_records('forum_digests', [ 'forum' => $forum->id, 'userid' => $userid, ]); // Delete all discussion items. $DB->delete_records_select( 'forum_queue', "userid = :userid AND discussionid IN (SELECT id FROM {forum_discussions} WHERE forum = :forum)", [ 'userid' => $userid, 'forum' => $forum->id, ] ); $DB->delete_records('forum_discussion_subs', [ 'forum' => $forum->id, 'userid' => $userid, ]); // Handle any advanced grading method data first. $grades = $DB->get_records('forum_grades', ['forum' => $forum->id, 'userid' => $user->id]); $gradingmanager = get_grading_manager($context, 'forum_grades', 'forum'); $controller = $gradingmanager->get_active_controller(); foreach ($grades as $grade) { // Delete advanced grading information. if (isset($controller)) { \core_grading\privacy\provider::delete_instance_data($context, $grade->id); } } // Advanced grading methods have been cleared, lets clear our module now. $DB->delete_records('forum_grades', [ 'forum' => $forum->id, 'userid' => $userid, ]); // Do not delete discussion or forum posts. // Instead update them to reflect that the content has been deleted. $postsql = "userid = :userid AND discussion IN (SELECT id FROM {forum_discussions} WHERE forum = :forum)"; $postidsql = "SELECT fp.id FROM {forum_posts} fp WHERE {$postsql}"; $postparams = [ 'forum' => $forum->id, 'userid' => $userid, ]; // Update the subject. $DB->set_field_select('forum_posts', 'subject', '', $postsql, $postparams); // Update the message and its format. $DB->set_field_select('forum_posts', 'message', '', $postsql, $postparams); $DB->set_field_select('forum_posts', 'messageformat', FORMAT_PLAIN, $postsql, $postparams); // Mark the post as deleted. $DB->set_field_select('forum_posts', 'deleted', 1, $postsql, $postparams); // Note: Do _not_ delete ratings of other users. Only delete ratings on the users own posts. // Ratings are aggregate fields and deleting the rating of this post will have an effect on the rating // of any post. \core_rating\privacy\provider::delete_ratings_select($context, 'mod_forum', 'post', "IN ($postidsql)", $postparams); // Delete all Tags. \core_tag\privacy\provider::delete_item_tags_select($context, 'mod_forum', 'forum_posts', "IN ($postidsql)", $postparams); // Delete all files from the posts. $fs = get_file_storage(); $fs->delete_area_files_select($context->id, 'mod_forum', 'post', "IN ($postidsql)", $postparams); $fs->delete_area_files_select($context->id, 'mod_forum', 'attachment', "IN ($postidsql)", $postparams); } } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { global $DB; $context = $userlist->get_context(); $cm = $DB->get_record('course_modules', ['id' => $context->instanceid]); $forum = $DB->get_record('forum', ['id' => $cm->instance]); list($userinsql, $userinparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); $params = array_merge(['forumid' => $forum->id], $userinparams); $DB->delete_records_select('forum_track_prefs', "forumid = :forumid AND userid {$userinsql}", $params); $DB->delete_records_select('forum_subscriptions', "forum = :forumid AND userid {$userinsql}", $params); $DB->delete_records_select('forum_read', "forumid = :forumid AND userid {$userinsql}", $params); $DB->delete_records_select( 'forum_queue', "userid {$userinsql} AND discussionid IN (SELECT id FROM {forum_discussions} WHERE forum = :forumid)", $params ); $DB->delete_records_select('forum_discussion_subs', "forum = :forumid AND userid {$userinsql}", $params); // Do not delete discussion or forum posts. // Instead update them to reflect that the content has been deleted. $postsql = "userid {$userinsql} AND discussion IN (SELECT id FROM {forum_discussions} WHERE forum = :forumid)"; $postidsql = "SELECT fp.id FROM {forum_posts} fp WHERE {$postsql}"; // Update the subject. $DB->set_field_select('forum_posts', 'subject', '', $postsql, $params); // Update the subject and its format. $DB->set_field_select('forum_posts', 'message', '', $postsql, $params); $DB->set_field_select('forum_posts', 'messageformat', FORMAT_PLAIN, $postsql, $params); // Mark the post as deleted. $DB->set_field_select('forum_posts', 'deleted', 1, $postsql, $params); // Note: Do _not_ delete ratings of other users. Only delete ratings on the users own posts. // Ratings are aggregate fields and deleting the rating of this post will have an effect on the rating // of any post. \core_rating\privacy\provider::delete_ratings_select($context, 'mod_forum', 'post', "IN ($postidsql)", $params); // Delete all Tags. \core_tag\privacy\provider::delete_item_tags_select($context, 'mod_forum', 'forum_posts', "IN ($postidsql)", $params); // Delete all files from the posts. $fs = get_file_storage(); $fs->delete_area_files_select($context->id, 'mod_forum', 'post', "IN ($postidsql)", $params); $fs->delete_area_files_select($context->id, 'mod_forum', 'attachment', "IN ($postidsql)", $params); list($sql, $params) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); $params['forum'] = $forum->id; // Delete advanced grading information. $grades = $DB->get_records_select('forum_grades', "forum = :forum AND userid $sql", $params); $gradeids = array_keys($grades); $gradingmanager = get_grading_manager($context, 'mod_forum', 'forum'); $controller = $gradingmanager->get_active_controller(); if (isset($controller)) { // Careful here, if no gradeids are provided then all data is deleted for the context. if (!empty($gradeids)) { \core_grading\privacy\provider::delete_data_for_instances($context, $gradeids); } } } } PK-B\RL#classes/privacy/subcontext_info.phpnu[. /** * Privacy Subsystem implementation for mod_forum. * * @package mod_forum * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\privacy; use \core_privacy\request\approved_contextlist; use \core_privacy\request\writer; use \core_privacy\metadata\item_collection; defined('MOODLE_INTERNAL') || die(); /** * Subcontext helper trait. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ trait subcontext_info { /** * Get the discussion part of the subcontext. * * @param \stdClass $discussion The discussion * @return array */ protected static function get_discussion_area(\stdClass $discussion): Array { $pathparts = []; if (!empty($discussion->groupname)) { $pathparts[] = get_string('groups'); $pathparts[] = $discussion->groupname; } $parts = [ $discussion->id, $discussion->name, ]; $discussionname = implode('-', $parts); $pathparts[] = get_string('discussions', 'mod_forum'); $pathparts[] = $discussionname; return $pathparts; } /** * Get the post part of the subcontext. * * @param \stdClass $post The post. * @return array */ protected static function get_post_area(\stdClass $post): Array { $parts = [ $post->created, $post->subject, $post->id, ]; $area[] = implode('-', $parts); return $area; } /** * Get the parent subcontext for the supplied forum, discussion, and post combination. * * @param \stdClass $post The post. * @return array */ protected static function get_post_area_for_parent(\stdClass $post) { global $DB; $subcontext = []; if ($parent = $DB->get_record('forum_posts', ['id' => $post->parent], 'id, created, subject')) { $subcontext = array_merge($subcontext, static::get_post_area($parent)); } $subcontext = array_merge($subcontext, static::get_post_area($post)); return $subcontext; } /** * Get the subcontext for the supplied forum, discussion, and post combination. * * @param \stdClass $forum The forum. * @param \stdClass $discussion The discussion * @param \stdClass $post The post. * @return array */ protected static function get_subcontext($forum, $discussion = null, $post = null) { $subcontext = []; if (null !== $discussion) { $subcontext += self::get_discussion_area($discussion); if (null !== $post) { $subcontext[] = get_string('posts', 'mod_forum'); $subcontext = array_merge($subcontext, static::get_post_area_for_parent($post)); } } return $subcontext; } } PK-B\X2Cclasses/dates.phpnu[. /** * Contains the class for fetching the important dates in mod_forum for a given module instance and a user. * * @package mod_forum * @copyright 2021 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types=1); namespace mod_forum; use core\activity_dates; /** * Class for fetching the important dates in mod_forum for a given module instance and a user. * * @copyright 2021 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class dates extends activity_dates { /** * Returns a list of important dates in mod_forum * * @return array */ protected function get_dates(): array { $duedate = $this->cm->customdata['duedate'] ?? null; $dates = []; if ($duedate) { $dates[] = [ 'dataid' => 'duedate', 'label' => get_string('activitydate:due', 'mod_forum'), 'timestamp' => (int) $duedate, ]; } return $dates; } } PK-B\*{D{D+classes/local/renderers/discussion_list.phpnu[. /** * Discussion list renderer. * * @package mod_forum * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\renderers; defined('MOODLE_INTERNAL') || die(); use mod_forum\grades\forum_gradeitem; use mod_forum\local\entities\forum as forum_entity; use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory; use mod_forum\local\factories\exporter as exporter_factory; use mod_forum\local\factories\vault as vault_factory; use mod_forum\local\factories\url as url_factory; use mod_forum\local\managers\capability as capability_manager; use mod_forum\local\vaults\discussion_list as discussion_list_vault; use renderer_base; use stdClass; use core\output\notification; use mod_forum\local\data_mappers\legacy\forum; use mod_forum\local\factories\builder as builder_factory; require_once($CFG->dirroot . '/mod/forum/lib.php'); /** * The discussion list renderer. * * @package mod_forum * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class discussion_list { /** @var forum_entity The forum being rendered */ private $forum; /** @var stdClass The DB record for the forum being rendered */ private $forumrecord; /** @var renderer_base The renderer used to render the view */ private $renderer; /** @var legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory */ private $legacydatamapperfactory; /** @var exporter_factory $exporterfactory Exporter factory */ private $exporterfactory; /** @var vault_factory $vaultfactory Vault factory */ private $vaultfactory; /** @var capability_manager $capabilitymanager Capability manager */ private $capabilitymanager; /** @var url_factory $urlfactory URL factory */ private $urlfactory; /** @var array $notifications List of notification HTML */ private $notifications; /** @var builder_factory $builderfactory Builder factory */ private $builderfactory; /** @var callable $postprocessfortemplate Function to process exported posts before template rendering */ private $postprocessfortemplate; /** @var string $template The template to use when displaying */ private $template; /** @var gradeitem The gradeitem instance associated with this forum */ private $forumgradeitem; /** * Constructor for a new discussion list renderer. * * @param forum_entity $forum The forum entity to be rendered * @param renderer_base $renderer The renderer used to render the view * @param legacy_data_mapper_factory $legacydatamapperfactory The factory used to fetch a legacy record * @param exporter_factory $exporterfactory The factory used to fetch exporter instances * @param vault_factory $vaultfactory The factory used to fetch the vault instances * @param builder_factory $builderfactory The factory used to fetch the builder instances * @param capability_manager $capabilitymanager The managed used to check capabilities on the forum * @param url_factory $urlfactory The factory used to create URLs in the forum * @param string $template * @param notification[] $notifications A list of any notifications to be displayed within the page * @param callable|null $postprocessfortemplate Callback function to process discussion lists for templates */ public function __construct( forum_entity $forum, renderer_base $renderer, legacy_data_mapper_factory $legacydatamapperfactory, exporter_factory $exporterfactory, vault_factory $vaultfactory, builder_factory $builderfactory, capability_manager $capabilitymanager, url_factory $urlfactory, forum_gradeitem $forumgradeitem, string $template, array $notifications = [], ?callable $postprocessfortemplate = null ) { $this->forum = $forum; $this->renderer = $renderer; $this->legacydatamapperfactory = $legacydatamapperfactory; $this->exporterfactory = $exporterfactory; $this->vaultfactory = $vaultfactory; $this->builderfactory = $builderfactory; $this->capabilitymanager = $capabilitymanager; $this->urlfactory = $urlfactory; $this->notifications = $notifications; $this->postprocessfortemplate = $postprocessfortemplate; $this->template = $template; $this->forumgradeitem = $forumgradeitem; $forumdatamapper = $this->legacydatamapperfactory->get_forum_data_mapper(); $this->forumrecord = $forumdatamapper->to_legacy_object($forum); } /** * Render for the specified user. * * @param stdClass $user The user to render for * @param cm_info $cm The course module info for this discussion list * @param int $groupid The group to render * @param int $sortorder The sort order to use when selecting the discussions in the list * @param int $pageno The zero-indexed page number to use * @param int $pagesize The number of discussions to show on the page * @param int $displaymode The discussion display mode * @param bool $enablediscussioncreation To show the discussion button. * @return string The rendered content for display */ public function render( stdClass $user, \cm_info $cm, ?int $groupid, ?int $sortorder, ?int $pageno, ?int $pagesize, ?int $displaymode = null, bool $enablediscussioncreation = true ): string { global $PAGE; $forum = $this->forum; $course = $forum->get_course_record(); $forumexporter = $this->exporterfactory->get_forum_exporter( $user, $this->forum, $groupid ); $pagesize = $this->get_page_size($pagesize); $pageno = $this->get_page_number($pageno); // Count all forum discussion posts. $alldiscussionscount = mod_forum_count_all_discussions($forum, $user, $groupid); // Get all forum discussion summaries. $discussions = mod_forum_get_discussion_summaries($forum, $user, $groupid, $sortorder, $pageno, $pagesize); $capabilitymanager = $this->capabilitymanager; $hasanyactions = false; $hasanyactions = $hasanyactions || $capabilitymanager->can_favourite_discussion($user); $hasanyactions = $hasanyactions || $capabilitymanager->can_pin_discussions($user); $hasanyactions = $hasanyactions || $capabilitymanager->can_manage_forum($user); $forumview = [ 'forum' => (array) $forumexporter->export($this->renderer), 'contextid' => $forum->get_context()->id, 'cmid' => $cm->id, 'groupid' => $groupid, 'name' => format_string($forum->get_name()), 'courseid' => $course->id, 'coursename' => format_string($course->shortname), 'experimentaldisplaymode' => $displaymode == FORUM_MODE_NESTED_V2, 'gradingcomponent' => $this->forumgradeitem->get_grading_component_name(), 'gradingcomponentsubtype' => $this->forumgradeitem->get_grading_component_subtype(), 'sendstudentnotifications' => $forum->should_notify_students_default_when_grade_for_forum(), 'gradeonlyactiveusers' => $this->forumgradeitem->should_grade_only_active_users(), 'hasanyactions' => $hasanyactions, 'groupchangemenu' => groups_print_activity_menu( $cm, $this->urlfactory->get_forum_view_url_from_forum($forum), true ), 'hasmore' => ($alldiscussionscount > $pagesize), 'notifications' => $this->get_notifications($user, $groupid), 'settings' => [ 'excludetext' => true, 'togglemoreicon' => true, 'excludesubscription' => true ], 'totaldiscussioncount' => $alldiscussionscount, 'userid' => $user->id, 'visiblediscussioncount' => count($discussions), 'enablediscussioncreation' => $enablediscussioncreation, ]; if ($forumview['forum']['capabilities']['create']) { $forumview['newdiscussionhtml'] = $this->get_discussion_form($user, $cm, $groupid); } if (!$discussions) { return $this->renderer->render_from_template($this->template, $forumview); } if ($this->postprocessfortemplate !== null) { // We've got some post processing to do! $exportedposts = ($this->postprocessfortemplate) ($discussions, $user, $forum); } $baseurl = new \moodle_url($PAGE->url, ['o' => $sortorder, 's' => $pagesize]); $forumview = array_merge( $forumview, [ 'pagination' => $this->renderer->render(new \paging_bar($alldiscussionscount, $pageno, $pagesize, $baseurl, 'p')), ], $exportedposts ); $firstdiscussion = reset($discussions); $forumview['firstgradeduserid'] = $firstdiscussion->get_latest_post_author()->get_id(); return $this->renderer->render_from_template($this->template, $forumview); } /** * Add new discussion button to the action bar for tertiary nav. * * @param stdClass $user The user object. * @param int|null $groupid The group id. * @return string rendered HTML string */ public function render_new_discussion(stdClass $user, ?int $groupid): string { $forumexporter = $this->exporterfactory->get_forum_exporter( $user, $this->forum, $groupid ); $forumview = [ 'forum' => (array) $forumexporter->export($this->renderer), ]; return $this->renderer->render_from_template('mod_forum/forum_new_discussion_actionbar', $forumview); } /** * Get the mod_forum_post_form. This is the default boiler plate from mod_forum/post_form.php with the inpage flag caveat * * @param stdClass $user The user the form is being generated for * @param \cm_info $cm * @param int $groupid The groupid if any * * @return string The rendered html */ private function get_discussion_form(stdClass $user, \cm_info $cm, ?int $groupid) { $forum = $this->forum; $forumrecord = $this->legacydatamapperfactory->get_forum_data_mapper()->to_legacy_object($forum); $modcontext = \context_module::instance($cm->id); $coursecontext = \context_course::instance($forum->get_course_id()); $post = (object) [ 'course' => $forum->get_course_id(), 'forum' => $forum->get_id(), 'discussion' => 0, // Ie discussion # not defined yet. 'parent' => 0, 'subject' => '', 'userid' => $user->id, 'message' => '', 'messageformat' => editors_get_preferred_format(), 'messagetrust' => 0, 'groupid' => $groupid, ]; $thresholdwarning = forum_check_throttling($forumrecord, $cm); $formparams = array( 'course' => $forum->get_course_record(), 'cm' => $cm, 'coursecontext' => $coursecontext, 'modcontext' => $modcontext, 'forum' => $forumrecord, 'post' => $post, 'subscribe' => \mod_forum\subscriptions::is_subscribed($user->id, $forumrecord, null, $cm), 'thresholdwarning' => $thresholdwarning, 'inpagereply' => true, 'edit' => 0 ); $posturl = new \moodle_url('/mod/forum/post.php'); $mformpost = new \mod_forum_post_form($posturl, $formparams, 'post', '', array('id' => 'mformforum')); $discussionsubscribe = \mod_forum\subscriptions::get_user_default_subscription($forumrecord, $coursecontext, $cm, null); $params = array('reply' => 0, 'forum' => $forumrecord->id, 'edit' => 0) + (isset($post->groupid) ? array('groupid' => $post->groupid) : array()) + array( 'userid' => $post->userid, 'parent' => $post->parent, 'discussion' => $post->discussion, 'course' => $forum->get_course_id(), 'discussionsubscribe' => $discussionsubscribe ); $mformpost->set_data($params); return $mformpost->render(); } /** * Fetch the page size to use when displaying the page. * * @param int $pagesize The number of discussions to show on the page * @return int The normalised page size */ private function get_page_size(?int $pagesize): int { if (null === $pagesize || $pagesize <= 0) { $pagesize = discussion_list_vault::PAGESIZE_DEFAULT; } return $pagesize; } /** * Fetch the current page number (zero-indexed). * * @param int $pageno The zero-indexed page number to use * @return int The normalised page number */ private function get_page_number(?int $pageno): int { if (null === $pageno || $pageno < 0) { $pageno = 0; } return $pageno; } /** * Get the list of notification for display. * * @param stdClass $user The viewing user * @param int|null $groupid The forum's group id * @return array */ private function get_notifications(stdClass $user, ?int $groupid): array { $notifications = $this->notifications; $forum = $this->forum; $capabilitymanager = $this->capabilitymanager; if ($forum->is_cutoff_date_reached()) { $notifications[] = (new notification( get_string('cutoffdatereached', 'forum'), notification::NOTIFY_INFO ))->set_show_closebutton(); } if ($forum->has_blocking_enabled()) { $notifications[] = (new notification( get_string('thisforumisthrottled', 'forum', [ 'blockafter' => $forum->get_block_after(), 'blockperiod' => get_string('secondstotime' . $forum->get_block_period()) ]), notification::NOTIFY_INFO ))->set_show_closebutton(); } if ($forum->is_in_group_mode() && !$capabilitymanager->can_access_all_groups($user)) { if ($groupid === null) { $isvisiblegroupsmode = $forum->get_effective_group_mode() == VISIBLEGROUPS; $isgroupmember = !empty(groups_get_user_groups($forum->get_course_id(), $user->id)[0]); if ( !$capabilitymanager->can_post_to_my_groups($user) && $isvisiblegroupsmode && $isgroupmember ) { $notifications[] = (new notification( get_string('cannotadddiscussionall', 'mod_forum'), \core\output\notification::NOTIFY_WARNING ))->set_show_closebutton(); } else { $notifications[] = (new notification( get_string('cannotadddiscussiongroup', 'mod_forum'), \core\output\notification::NOTIFY_WARNING ))->set_show_closebutton(); } } else if (!$capabilitymanager->can_access_group($user, $groupid)) { $notifications[] = (new notification( get_string('cannotadddiscussion', 'mod_forum'), \core\output\notification::NOTIFY_WARNING ))->set_show_closebutton(); } } if ('qanda' === $forum->get_type() && !$capabilitymanager->can_manage_forum($user)) { $notifications[] = (new notification( get_string('qandanotify', 'forum'), notification::NOTIFY_INFO ))->set_show_closebutton()->set_extra_classes(['mt-3']); } if ('eachuser' === $forum->get_type()) { $notifications[] = (new notification( get_string('allowsdiscussions', 'forum'), notification::NOTIFY_INFO) )->set_show_closebutton(); } return array_map(function($notification) { return $notification->export_for_template($this->renderer); }, $notifications); } } PK-B\'AA!classes/local/renderers/posts.phpnu[. /** * Posts renderer. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\renderers; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\builders\exported_posts as exported_posts_builder; use renderer_base; use stdClass; /** * Posts renderer class. * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class posts { /** @var renderer_base $renderer Renderer base */ private $renderer; /** @var exported_posts_builder $exportedpostsbuilder Builder for building exported posts */ private $exportedpostsbuilder; /** @var string $template The template to render */ private $template; /** @var callable $postprocessfortemplate Function to process exported posts before template rendering */ private $postprocessfortemplate; /** * Constructor. * * @param renderer_base $renderer Renderer base * @param exported_posts_builder $exportedpostsbuilder Builder for building exported posts * @param string $template The template to render * @param callable $postprocessfortemplate Function to process exported posts before template rendering */ public function __construct( renderer_base $renderer, exported_posts_builder $exportedpostsbuilder, string $template, ?callable $postprocessfortemplate = null ) { $this->renderer = $renderer; $this->exportedpostsbuilder = $exportedpostsbuilder; $this->template = $template; $this->postprocessfortemplate = $postprocessfortemplate; } /** * Render the given posts for the forums and discussions. * * @param stdClass $user The user viewing the posts * @param forum_entity[] $forums A list of all forums for these posts * @param discussion_entity[] $discussions A list of all discussions for these posts * @param post_entity[] $posts The posts to render * @return string */ public function render( stdClass $user, array $forums, array $discussions, array $posts ): string { // Format the forums and discussion to make them more easily accessed later. $forums = array_reduce($forums, function($carry, $forum) { $carry[$forum->get_id()] = $forum; return $carry; }, []); $discussions = array_reduce($discussions, function($carry, $discussion) { $carry[$discussion->get_id()] = $discussion; return $carry; }, []); $exportedposts = $this->exportedpostsbuilder->build( $user, $forums, $discussions, $posts ); if ($this->postprocessfortemplate !== null) { // We've got some post processing to do! $exportedposts = ($this->postprocessfortemplate)($exportedposts, $forums, $discussions, $user); } return $this->renderer->render_from_template( $this->template, ['posts' => array_values($exportedposts)] ); } } PK-B\PmII&classes/local/renderers/discussion.phpnu[. /** * Discussion renderer. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\renderers; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\discussion as discussion_entity; use mod_forum\local\entities\forum as forum_entity; use mod_forum\local\entities\post as post_entity; use mod_forum\local\entities\sorter as sorter_entity; use mod_forum\local\factories\entity as entity_factory; use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory; use mod_forum\local\factories\exporter as exporter_factory; use mod_forum\local\factories\url as url_factory; use mod_forum\local\factories\vault as vault_factory; use mod_forum\local\managers\capability as capability_manager; use mod_forum\local\renderers\posts as posts_renderer; use forum_portfolio_caller; use core\output\notification; use context; use context_module; use html_writer; use moodle_exception; use moodle_page; use moodle_url; use rating_manager; use renderer_base; use single_button; use single_select; use stdClass; use url_select; require_once($CFG->dirroot . '/mod/forum/lib.php'); require_once($CFG->dirroot . '/mod/forum/locallib.php'); /** * Discussion renderer class. * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class discussion { /** @var forum_entity $forum The forum that the discussion belongs to */ private $forum; /** @var discussion_entity $discussion The discussion entity */ private $discussion; /** @var stdClass $discussionrecord Legacy discussion record */ private $discussionrecord; /** @var stdClass $forumrecord Legacy forum record */ private $forumrecord; /** @var int $displaymode The display mode to render the discussion in */ private $displaymode; /** @var renderer_base $renderer Renderer base */ private $renderer; /** @var posts_renderer $postsrenderer A posts renderer */ private $postsrenderer; /** @var moodle_page $page The page this discussion is being rendered for */ private $page; /** @var legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory */ private $legacydatamapperfactory; /** @var exporter_factory $exporterfactory Exporter factory */ private $exporterfactory; /** @var vault_factory $vaultfactory Vault factory */ private $vaultfactory; /** @var url_factory $urlfactory URL factory */ private $urlfactory; /** @var entity_factory $entityfactory Entity factory */ private $entityfactory; /** @var capability_manager $capabilitymanager Capability manager */ private $capabilitymanager; /** @var rating_manager $ratingmanager Rating manager */ private $ratingmanager; /** @var moodle_url $baseurl The base URL for the discussion */ private $baseurl; /** @var array $notifications List of HTML notifications to display */ private $notifications; /** @var sorter_entity $exportedpostsorter Sorter for the exported posts */ private $exportedpostsorter; /** @var callable $postprocessfortemplate Function to process exported posts before template rendering */ private $postprocessfortemplate; /** * Constructor. * * @param forum_entity $forum The forum that the discussion belongs to * @param discussion_entity $discussion The discussion entity * @param int $displaymode The display mode to render the discussion in * @param renderer_base $renderer Renderer base * @param posts_renderer $postsrenderer A posts renderer * @param moodle_page $page The page this discussion is being rendered for * @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory * @param exporter_factory $exporterfactory Exporter factory * @param vault_factory $vaultfactory Vault factory * @param url_factory $urlfactory URL factory * @param entity_factory $entityfactory Entity factory * @param capability_manager $capabilitymanager Capability manager * @param rating_manager $ratingmanager Rating manager * @param sorter_entity $exportedpostsorter Sorter for the exported posts * @param moodle_url $baseurl The base URL for the discussion * @param array $notifications List of HTML notifications to display * @param callable|null $postprocessfortemplate Post processing for template callback */ public function __construct( forum_entity $forum, discussion_entity $discussion, int $displaymode, renderer_base $renderer, posts_renderer $postsrenderer, moodle_page $page, legacy_data_mapper_factory $legacydatamapperfactory, exporter_factory $exporterfactory, vault_factory $vaultfactory, url_factory $urlfactory, entity_factory $entityfactory, capability_manager $capabilitymanager, rating_manager $ratingmanager, sorter_entity $exportedpostsorter, moodle_url $baseurl, array $notifications = [], ?callable $postprocessfortemplate = null ) { $this->forum = $forum; $this->discussion = $discussion; $this->displaymode = $displaymode; $this->renderer = $renderer; $this->postsrenderer = $postsrenderer; $this->page = $page; $this->baseurl = $baseurl; $this->legacydatamapperfactory = $legacydatamapperfactory; $this->exporterfactory = $exporterfactory; $this->vaultfactory = $vaultfactory; $this->urlfactory = $urlfactory; $this->entityfactory = $entityfactory; $this->capabilitymanager = $capabilitymanager; $this->ratingmanager = $ratingmanager; $this->notifications = $notifications; $this->exportedpostsorter = $exportedpostsorter; $this->postprocessfortemplate = $postprocessfortemplate; $forumdatamapper = $this->legacydatamapperfactory->get_forum_data_mapper(); $this->forumrecord = $forumdatamapper->to_legacy_object($forum); $discussiondatamapper = $this->legacydatamapperfactory->get_discussion_data_mapper(); $this->discussionrecord = $discussiondatamapper->to_legacy_object($discussion); } /** * Render the discussion for the given user in the specified display mode. * * @param stdClass $user The user viewing the discussion * @param post_entity $firstpost The first post in the discussion * @param array $replies List of replies to the first post * @return string HTML for the discussion */ public function render( stdClass $user, post_entity $firstpost, array $replies ): string { global $CFG; $displaymode = $this->displaymode; $capabilitymanager = $this->capabilitymanager; $urlfactory = $this->urlfactory; $entityfactory = $this->entityfactory; // Make sure we can render. if (!$capabilitymanager->can_view_discussions($user)) { throw new moodle_exception('noviewdiscussionspermission', 'mod_forum'); } $posts = array_merge([$firstpost], array_values($replies)); if ($this->postprocessfortemplate !== null) { $exporteddiscussion = ($this->postprocessfortemplate) ($this->discussion, $user, $this->forum); } else { $exporteddiscussion = $this->get_exported_discussion($user); } $hasanyactions = false; $hasanyactions = $hasanyactions || $capabilitymanager->can_favourite_discussion($user); $hasanyactions = $hasanyactions || $capabilitymanager->can_pin_discussions($user); $hasanyactions = $hasanyactions || $capabilitymanager->can_manage_forum($user); $exporteddiscussion = array_merge($exporteddiscussion, [ 'notifications' => $this->get_notifications($user), 'html' => [ 'hasanyactions' => $hasanyactions, 'posts' => $this->postsrenderer->render($user, [$this->forum], [$this->discussion], $posts), 'modeselectorform' => $this->get_display_mode_selector_html($displaymode, $user), 'subscribe' => null, 'movediscussion' => null, 'pindiscussion' => null, 'neighbourlinks' => $this->get_neighbour_links_html(), 'exportdiscussion' => !empty($CFG->enableportfolios) ? $this->get_export_discussion_html($user) : null ], 'settingsselector' => true, ]); $capabilities = (array) $exporteddiscussion['capabilities']; if ($capabilities['move']) { $exporteddiscussion['html']['movediscussion'] = $this->get_move_discussion_html(); } if (!empty($user->id)) { $loggedinuser = $entityfactory->get_author_from_stdClass($user); $exporteddiscussion['loggedinuser'] = [ 'firstname' => $loggedinuser->get_first_name(), 'fullname' => $loggedinuser->get_full_name(), 'profileimageurl' => ($urlfactory->get_author_profile_image_url($loggedinuser, null))->out(false) ]; } $exporteddiscussion['throttlingwarningmsg'] = ''; $cmrecord = $this->forum->get_course_module_record(); if (($warningobj = forum_check_throttling($this->forumrecord, $cmrecord)) && $warningobj->canpost) { $throttlewarnnotification = (new notification( get_string($warningobj->errorcode, $warningobj->module, $warningobj->additional) ))->set_show_closebutton(); $exporteddiscussion['throttlingwarningmsg'] = $throttlewarnnotification->get_message(); } if ($this->displaymode === FORUM_MODE_NESTED_V2) { $template = 'mod_forum/forum_discussion_nested_v2'; } else { $template = 'mod_forum/forum_discussion'; } return $this->renderer->render_from_template($template, $exporteddiscussion); } /** * Get the groups details for all groups available to the forum. * * @return stdClass[] */ private function get_groups_available_in_forum(): array { $course = $this->forum->get_course_record(); $coursemodule = $this->forum->get_course_module_record(); return groups_get_all_groups($course->id, 0, $coursemodule->groupingid); } /** * Get the exported discussion. * * @param stdClass $user The user viewing the discussion * @return array */ private function get_exported_discussion(stdClass $user): array { $discussionexporter = $this->exporterfactory->get_discussion_exporter( $user, $this->forum, $this->discussion, $this->get_groups_available_in_forum() ); return (array) $discussionexporter->export($this->renderer); } /** * Get the HTML for the display mode selector. * * @param int $displaymode The current display mode * @param stdClass $user The current user * @return string */ private function get_display_mode_selector_html(int $displaymode, stdClass $user): string { $baseurl = $this->baseurl; $select = new single_select( $baseurl, 'mode', forum_get_layout_modes(get_user_preferences('forum_useexperimentalui', false, $user)), $displaymode, null, 'mode' ); $select->set_label(get_string('displaymode', 'forum'), ['class' => 'accesshide']); return $this->renderer->render($select); } /** * Get the HTML to render the move discussion selector and button. * * @return string */ private function get_move_discussion_html(): ?string { global $DB; $forum = $this->forum; $discussion = $this->discussion; $courseid = $forum->get_course_id(); // Popup menu to move discussions to other forums. The discussion in a // single discussion forum can't be moved. $modinfo = get_fast_modinfo($courseid); if (isset($modinfo->instances['forum'])) { $forummenu = []; // Check forum types and eliminate simple discussions. $forumcheck = $DB->get_records('forum', ['course' => $courseid], '', 'id, type'); foreach ($modinfo->instances['forum'] as $forumcm) { if (!$forumcm->uservisible || !has_capability('mod/forum:startdiscussion', context_module::instance($forumcm->id))) { continue; } $section = $forumcm->sectionnum; $sectionname = get_section_name($courseid, $section); if (empty($forummenu[$section])) { $forummenu[$section] = [$sectionname => []]; } $forumidcompare = $forumcm->instance != $forum->get_id(); $forumtypecheck = $forumcheck[$forumcm->instance]->type !== 'single'; if ($forumidcompare and $forumtypecheck) { $url = "/mod/forum/discuss.php?d={$discussion->get_id()}&move=$forumcm->instance&sesskey=".sesskey(); $forummenu[$section][$sectionname][$url] = format_string($forumcm->name); } } if (!empty($forummenu)) { $html = '
'; $movebutton = get_string('move'); if ($this->displaymode === FORUM_MODE_NESTED_V2) { // Move discussion selector will be rendered on the settings drawer. We won't output the button in this mode. $movebutton = null; } $select = new url_select($forummenu, '', ['/mod/forum/discuss.php?d=' . $discussion->get_id() => get_string("movethisdiscussionto", "forum")], 'forummenu', $movebutton); $select->set_label(get_string('movethisdiscussionlabel', 'mod_forum'), [ 'class' => 'visually-hidden', ]); $html .= $this->renderer->render($select); $html .= "
"; return $html; } } return null; } /** * Get the HTML to render the export discussion button. * * @param stdClass $user The user viewing the discussion * @return string|null */ private function get_export_discussion_html(stdClass $user): ?string { global $CFG; if (!$this->capabilitymanager->can_export_discussions($user)) { return null; } $button = new \portfolio_add_button(); $button->set_callback_options('forum_portfolio_caller', ['discussionid' => $this->discussion->get_id()], 'mod_forum'); $button = $button->to_html(PORTFOLIO_ADD_FULL_FORM, get_string('exportdiscussion', 'mod_forum')); return $button ?: null; } /** * Get a list of notification HTML to render in the page. * * @param stdClass $user The user viewing the discussion * @return string[] */ private function get_notifications($user): array { $notifications = $this->notifications; $discussion = $this->discussion; $forum = $this->forum; if ($forum->is_cutoff_date_reached()) { $notifications[] = (new notification( get_string('cutoffdatereached', 'forum'), notification::NOTIFY_INFO ))->set_show_closebutton(); } else if ($forum->get_type() != 'single') { // Due date is already shown at the top of the page for single simple discussion forums. if ($forum->is_due_date_reached()) { $notifications[] = (new notification( get_string('thisforumisdue', 'forum', userdate($forum->get_due_date())), notification::NOTIFY_INFO ))->set_show_closebutton(); } else if ($forum->has_due_date()) { $notifications[] = (new notification( get_string('thisforumhasduedate', 'forum', userdate($forum->get_due_date())), notification::NOTIFY_INFO ))->set_show_closebutton(); } } if ($forum->is_discussion_locked($discussion)) { $notifications[] = (new notification( get_string('discussionlocked', 'forum'), notification::NOTIFY_INFO )) ->set_extra_classes(['discussionlocked']) ->set_show_closebutton(); } if ($forum->get_type() == 'qanda') { if ($this->capabilitymanager->must_post_before_viewing_discussion($user, $discussion)) { $notifications[] = (new notification( get_string('qandanotify', 'forum') ))->set_show_closebutton(true)->set_extra_classes(['mt-3']); } } if ($forum->has_blocking_enabled()) { $notifications[] = (new notification( get_string('thisforumisthrottled', 'forum', [ 'blockafter' => $forum->get_block_after(), 'blockperiod' => get_string('secondstotime' . $forum->get_block_period()) ]), notification::NOTIFY_INFO ))->set_show_closebutton(); } return array_map(function($notification) { return $notification->export_for_template($this->renderer); }, $notifications); } /** * Get HTML to display the neighbour links. * * @return string */ private function get_neighbour_links_html(): string { $forum = $this->forum; $coursemodule = $forum->get_course_module_record(); $neighbours = forum_get_discussion_neighbours($coursemodule, $this->discussionrecord, $this->forumrecord); return $this->renderer->neighbouring_discussion_navigation($neighbours['prev'], $neighbours['next']); } } PK-B\'Rpp classes/local/exporters/post.phpnu[. /** * Post exporter class. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\exporters; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\post as post_entity; use mod_forum\local\entities\discussion as discussion_entity; use mod_forum\local\exporters\author as author_exporter; use mod_forum\local\factories\exporter as exporter_factory; use core\external\exporter; use core_files\external\stored_file_exporter; use context; use core_tag_tag; use renderer_base; use stdClass; require_once($CFG->dirroot . '/mod/forum/lib.php'); /** * Post exporter class. * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class post extends exporter { /** @var post_entity $post The post to export */ private $post; /** * Constructor. * * @param post_entity $post The post to export * @param array $related List of related data */ public function __construct(post_entity $post, array $related = []) { $this->post = $post; return parent::__construct([], $related); } /** * Return the list of additional properties. * * @return array */ protected static function define_other_properties() { $attachmentdefinition = stored_file_exporter::read_properties_definition(); $attachmentdefinition['urls'] = [ 'type' => [ 'export' => [ 'type' => PARAM_URL, 'description' => 'The URL used to export the attachment', 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ] ] ]; $attachmentdefinition['html'] = [ 'type' => [ 'plagiarism' => [ 'type' => PARAM_RAW, 'description' => 'The HTML source for the Plagiarism Response', 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], ] ]; return [ 'id' => ['type' => PARAM_INT], 'subject' => ['type' => PARAM_TEXT], 'replysubject' => ['type' => PARAM_TEXT], 'message' => ['type' => PARAM_RAW], 'messageformat' => ['type' => PARAM_INT], 'author' => ['type' => author_exporter::read_properties_definition()], 'discussionid' => ['type' => PARAM_INT], 'hasparent' => ['type' => PARAM_BOOL], 'parentid' => [ 'type' => PARAM_INT, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'timecreated' => [ 'type' => PARAM_INT, 'default' => null, 'null' => NULL_ALLOWED ], 'timemodified' => [ 'type' => PARAM_INT, 'default' => null, 'null' => NULL_ALLOWED ], 'unread' => [ 'type' => PARAM_BOOL, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'isdeleted' => ['type' => PARAM_BOOL], 'isprivatereply' => ['type' => PARAM_BOOL], 'haswordcount' => ['type' => PARAM_BOOL], 'wordcount' => [ 'type' => PARAM_INT, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'charcount' => [ 'type' => PARAM_INT, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'capabilities' => [ 'type' => [ 'view' => [ 'type' => PARAM_BOOL, 'null' => NULL_ALLOWED, 'description' => 'Whether the user can view the post', ], 'edit' => [ 'type' => PARAM_BOOL, 'null' => NULL_ALLOWED, 'description' => 'Whether the user can edit the post', ], 'delete' => [ 'type' => PARAM_BOOL, 'null' => NULL_ALLOWED, 'description' => 'Whether the user can delete the post', ], 'split' => [ 'type' => PARAM_BOOL, 'null' => NULL_ALLOWED, 'description' => 'Whether the user can split the post', ], 'reply' => [ 'type' => PARAM_BOOL, 'null' => NULL_ALLOWED, 'description' => 'Whether the user can reply to the post', ], 'selfenrol' => [ 'type' => PARAM_BOOL, 'null' => NULL_ALLOWED, 'description' => 'Whether the user can self enrol into the course', ], 'export' => [ 'type' => PARAM_BOOL, 'null' => NULL_ALLOWED, 'description' => 'Whether the user can export the post', ], 'controlreadstatus' => [ 'type' => PARAM_BOOL, 'null' => NULL_ALLOWED, 'description' => 'Whether the user can control the read status of the post', ], 'canreplyprivately' => [ 'type' => PARAM_BOOL, 'null' => NULL_ALLOWED, 'description' => 'Whether the user can post a private reply', ] ] ], 'urls' => [ 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED, 'type' => [ 'view' => [ 'description' => 'The URL used to view the post', 'type' => PARAM_URL, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'viewisolated' => [ 'description' => 'The URL used to view the post in isolation', 'type' => PARAM_URL, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'viewparent' => [ 'description' => 'The URL used to view the parent of the post', 'type' => PARAM_URL, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'edit' => [ 'description' => 'The URL used to edit the post', 'type' => PARAM_URL, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'delete' => [ 'description' => 'The URL used to delete the post', 'type' => PARAM_URL, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'split' => [ 'description' => 'The URL used to split the discussion ' . 'with the selected post being the first post in the new discussion', 'type' => PARAM_URL, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'reply' => [ 'description' => 'The URL used to reply to the post', 'type' => PARAM_URL, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'export' => [ 'description' => 'The URL used to export the post', 'type' => PARAM_URL, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'markasread' => [ 'description' => 'The URL used to mark the post as read', 'type' => PARAM_URL, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'markasunread' => [ 'description' => 'The URL used to mark the post as unread', 'type' => PARAM_URL, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'discuss' => [ 'type' => PARAM_URL, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ] ] ], 'attachments' => [ 'multiple' => true, 'type' => $attachmentdefinition ], 'messageinlinefiles' => [ 'optional' => true, 'multiple' => true, 'type' => stored_file_exporter::read_properties_definition(), ], 'tags' => [ 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED, 'multiple' => true, 'type' => [ 'id' => [ 'type' => PARAM_INT, 'description' => 'The ID of the Tag', 'null' => NULL_NOT_ALLOWED, ], 'tagid' => [ 'type' => PARAM_INT, 'description' => 'The tagid', 'null' => NULL_NOT_ALLOWED, ], 'isstandard' => [ 'type' => PARAM_BOOL, 'description' => 'Whether this is a standard tag', 'null' => NULL_NOT_ALLOWED, ], 'displayname' => [ 'type' => PARAM_TEXT, 'description' => 'The display name of the tag', 'null' => NULL_NOT_ALLOWED, ], 'flag' => [ 'type' => PARAM_BOOL, 'description' => 'Wehther this tag is flagged', 'null' => NULL_NOT_ALLOWED, ], 'urls' => [ 'description' => 'URLs associated with the tag', 'null' => NULL_NOT_ALLOWED, 'type' => [ 'view' => [ 'type' => PARAM_URL, 'description' => 'The URL to view the tag', 'null' => NULL_NOT_ALLOWED, ], ] ] ] ], 'html' => [ 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED, 'type' => [ 'rating' => [ 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED, 'type' => PARAM_RAW, 'description' => 'The HTML source to rate the post', ], 'taglist' => [ 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED, 'type' => PARAM_RAW, 'description' => 'The HTML source to view the list of tags', ], 'authorsubheading' => [ 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED, 'type' => PARAM_RAW, 'description' => 'The HTML source to view the author details', ], ] ] ]; } /** * Get the additional values to inject while exporting. * * @param renderer_base $output The renderer. * @return array Keys are the property names, values are their values. */ protected function get_other_values(renderer_base $output) { $post = $this->post; $authorgroups = $this->related['authorgroups']; $forum = $this->related['forum']; $discussion = $this->related['discussion']; $author = $this->related['author']; $authorcontextid = $this->related['authorcontextid']; $user = $this->related['user']; $readreceiptcollection = $this->related['readreceiptcollection']; $rating = $this->related['rating']; $tags = $this->related['tags']; $attachments = $this->related['attachments']; $inlineattachments = $this->related['messageinlinefiles']; $includehtml = $this->related['includehtml']; $isdeleted = $post->is_deleted(); $isprivatereply = $post->is_private_reply(); $hasrating = $rating != null; $hastags = !empty($tags); $discussionid = $post->get_discussion_id(); $parentid = $post->get_parent_id(); $capabilitymanager = $this->related['capabilitymanager']; $canview = $capabilitymanager->can_view_post($user, $discussion, $post); $canedit = $capabilitymanager->can_edit_post($user, $discussion, $post); $candelete = $capabilitymanager->can_delete_post($user, $discussion, $post); $cansplit = $capabilitymanager->can_split_post($user, $discussion, $post); $canreply = $capabilitymanager->can_reply_to_post($user, $discussion, $post); $canexport = $capabilitymanager->can_export_post($user, $post); $cancontrolreadstatus = $capabilitymanager->can_manually_control_post_read_status($user); $canselfenrol = $capabilitymanager->can_self_enrol($user); $canreplyprivately = $capabilitymanager->can_reply_privately_to_post($user, $post); $urlfactory = $this->related['urlfactory']; $viewurl = $canview ? $urlfactory->get_view_post_url_from_post($post) : null; $viewisolatedurl = $canview ? $urlfactory->get_view_isolated_post_url_from_post($post) : null; $viewparenturl = $post->has_parent() ? $urlfactory->get_view_post_url_from_post_id($discussionid, $parentid) : null; $editurl = $canedit ? $urlfactory->get_edit_post_url_from_post($forum, $post) : null; $deleteurl = $candelete ? $urlfactory->get_delete_post_url_from_post($post) : null; $spliturl = $cansplit ? $urlfactory->get_split_discussion_at_post_url_from_post($post) : null; $replyurl = $canreply || $canselfenrol ? $urlfactory->get_reply_to_post_url_from_post($post) : null; $exporturl = $canexport ? $urlfactory->get_export_post_url_from_post($post) : null; $markasreadurl = $cancontrolreadstatus ? $urlfactory->get_mark_post_as_read_url_from_post($post) : null; $markasunreadurl = $cancontrolreadstatus ? $urlfactory->get_mark_post_as_unread_url_from_post($post) : null; $discussurl = $canview ? $urlfactory->get_discussion_view_url_from_post($post) : null; $authorexporter = new author_exporter( $author, $authorcontextid, $authorgroups, $canview, $this->related ); $exportedauthor = $authorexporter->export($output); // Only bother loading the content if the user can see it. $loadcontent = $canview && !$isdeleted; $exportattachments = $loadcontent && !empty($attachments); $exportinlineattachments = $loadcontent && !empty($inlineattachments); if ($loadcontent) { $subject = $post->get_subject(); $timecreated = $this->get_start_time($discussion, $post); $message = $this->get_message($post); } else { $subject = $isdeleted ? get_string('forumsubjectdeleted', 'forum') : get_string('forumsubjecthidden', 'forum'); $message = $isdeleted ? get_string('forumbodydeleted', 'forum') : get_string('forumbodyhidden', 'forum'); $timecreated = null; } $showwordcount = $forum->should_display_word_count(); if ($showwordcount) { $wordcount = $post->get_wordcount() ?? count_words($message); $charcount = $post->get_charcount() ?? count_letters($message); } else { $wordcount = null; $charcount = null; } return [ 'id' => $post->get_id(), 'subject' => $subject, 'replysubject' => $subject, 'message' => $message, 'messageformat' => $post->get_message_format(), 'author' => $exportedauthor, 'discussionid' => $post->get_discussion_id(), 'hasparent' => $post->has_parent(), 'parentid' => $post->has_parent() ? $post->get_parent_id() : null, 'timecreated' => $timecreated, 'timemodified' => $post->get_time_modified(), 'unread' => ($loadcontent && $readreceiptcollection) ? !$readreceiptcollection->has_user_read_post($user, $post) : null, 'isdeleted' => $isdeleted, 'isprivatereply' => $isprivatereply, 'haswordcount' => $showwordcount, 'wordcount' => $wordcount, 'charcount' => $charcount, 'capabilities' => [ 'view' => $canview, 'edit' => $canedit, 'delete' => $candelete, 'split' => $cansplit, 'reply' => $canreply, 'export' => $canexport, 'controlreadstatus' => $cancontrolreadstatus, 'canreplyprivately' => $canreplyprivately, 'selfenrol' => $canselfenrol ], 'urls' => [ 'view' => $viewurl ? $viewurl->out(false) : null, 'viewisolated' => $viewisolatedurl ? $viewisolatedurl->out(false) : null, 'viewparent' => $viewparenturl ? $viewparenturl->out(false) : null, 'edit' => $editurl ? $editurl->out(false) : null, 'delete' => $deleteurl ? $deleteurl->out(false) : null, 'split' => $spliturl ? $spliturl->out(false) : null, 'reply' => $replyurl ? $replyurl->out(false) : null, 'export' => $exporturl && $exporturl ? $exporturl->out(false) : null, 'markasread' => $markasreadurl ? $markasreadurl->out(false) : null, 'markasunread' => $markasunreadurl ? $markasunreadurl->out(false) : null, 'discuss' => $discussurl ? $discussurl->out(false) : null, ], 'attachments' => ($exportattachments) ? $this->export_attachments($attachments, $post, $output, $canexport) : [], 'messageinlinefiles' => ($exportinlineattachments) ? $this->export_inline_attachments($inlineattachments, $post, $output) : [], 'tags' => ($loadcontent && $hastags) ? $this->export_tags($tags) : [], 'html' => $includehtml ? [ 'rating' => ($loadcontent && $hasrating) ? $output->render($rating) : null, 'taglist' => ($loadcontent && $hastags) ? $output->tag_list($tags) : null, 'authorsubheading' => ($loadcontent) ? $this->get_author_subheading_html($exportedauthor, $timecreated) : null ] : null ]; } /** * Returns a list of objects that are related. * * @return array */ protected static function define_related() { return [ 'capabilitymanager' => 'mod_forum\local\managers\capability', 'readreceiptcollection' => 'mod_forum\local\entities\post_read_receipt_collection?', 'urlfactory' => 'mod_forum\local\factories\url', 'forum' => 'mod_forum\local\entities\forum', 'discussion' => 'mod_forum\local\entities\discussion', 'author' => 'mod_forum\local\entities\author', 'authorcontextid' => 'int?', 'user' => 'stdClass', 'context' => 'context', 'authorgroups' => 'stdClass[]', 'attachments' => '\stored_file[]?', 'messageinlinefiles' => '\stored_file[]?', 'tags' => '\core_tag_tag[]?', 'rating' => 'rating?', 'includehtml' => 'bool' ]; } /** * This method returns the parameters for the post's message to * use with the function \core_external\util::format_text(). * * @return array */ protected function get_format_parameters_for_message() { return [ 'component' => 'mod_forum', 'filearea' => 'post', 'itemid' => $this->post->get_id(), 'options' => [ 'para' => false, 'trusted' => $this->post->is_message_trusted() ] ]; } /** * Get the message text from a post. * * @param post_entity $post The post * @return string */ private function get_message(post_entity $post): string { global $CFG; $message = $post->get_message(); if (!empty($CFG->enableplagiarism)) { require_once($CFG->libdir . '/plagiarismlib.php'); $forum = $this->related['forum']; $message .= plagiarism_get_links([ 'userid' => $post->get_author_id(), 'content' => $message, 'cmid' => $forum->get_course_module_record()->id, 'course' => $forum->get_course_id(), 'forum' => $forum->get_id() ]); } return $message; } /** * Get the exported attachments for a post. * * @param stored_file[] $attachments The list of attachments for the post * @param post_entity $post The post being exported * @param renderer_base $output Renderer base * @param bool $canexport If the user can export the post (relates to portfolios not exporters like this class) * @return array */ private function export_attachments(array $attachments, post_entity $post, renderer_base $output, bool $canexport): array { global $CFG; $urlfactory = $this->related['urlfactory']; $enableplagiarism = $CFG->enableplagiarism; $forum = $this->related['forum']; $context = $this->related['context']; if ($enableplagiarism) { require_once($CFG->libdir . '/plagiarismlib.php' ); } return array_map(function($attachment) use ( $output, $enableplagiarism, $canexport, $context, $forum, $post, $urlfactory ) { $exporter = new stored_file_exporter($attachment, ['context' => $context]); $exportedattachment = $exporter->export($output); $exporturl = $canexport ? $urlfactory->get_export_attachment_url_from_post_and_attachment($post, $attachment) : null; if ($enableplagiarism) { $plagiarismhtml = plagiarism_get_links([ 'userid' => $post->get_author_id(), 'file' => $attachment, 'cmid' => $forum->get_course_module_record()->id, 'course' => $forum->get_course_id(), 'forum' => $forum->get_id() ]); } else { $plagiarismhtml = null; } $exportedattachment->urls = [ 'export' => $exporturl ? $exporturl->out(false) : null ]; $exportedattachment->html = [ 'plagiarism' => $plagiarismhtml ]; return $exportedattachment; }, $attachments); } /** * Get the exported inline attachments for a post. * * @param array $inlineattachments The list of inline attachments for the post * @param post_entity $post The post being exported * @param renderer_base $output Renderer base * @return array */ private function export_inline_attachments(array $inlineattachments, post_entity $post, renderer_base $output): array { return array_map(function($attachment) use ( $output, $post ) { $exporter = new stored_file_exporter($attachment, ['context' => $this->related['context']]); return $exporter->export($output);; }, $inlineattachments); } /** * Export the list of tags. * * @param core_tag_tag[] $tags List of tags to export * @return array */ private function export_tags(array $tags): array { $user = $this->related['user']; $context = $this->related['context']; $capabilitymanager = $this->related['capabilitymanager']; $canmanagetags = $capabilitymanager->can_manage_tags($user); return array_values(array_map(function($tag) use ($context, $canmanagetags) { $viewurl = core_tag_tag::make_url($tag->tagcollid, $tag->rawname, 0, $context->id); return [ 'id' => $tag->taginstanceid, 'tagid' => $tag->id, 'isstandard' => $tag->isstandard, 'displayname' => $tag->get_display_name(), 'flag' => $canmanagetags && !empty($tag->flag), 'urls' => [ 'view' => $viewurl->out(false) ] ]; }, $tags)); } /** * Get the HTML to display as a subheading in a post. * * @param stdClass $exportedauthor The exported author object * @param int $timecreated The post time created timestamp if it's to be displayed * @return string */ private function get_author_subheading_html(stdClass $exportedauthor, int $timecreated): string { $fullname = $exportedauthor->fullname; $profileurl = $exportedauthor->urls['profile'] ?? null; $name = $profileurl ? "{$fullname}" : $fullname; $date = userdate_htmltime($timecreated, get_string('strftimedaydatetime', 'core_langconfig')); return get_string('bynameondate', 'mod_forum', ['name' => $name, 'date' => $date]); } /** * Get the start time for a post. * * @param discussion_entity $discussion entity * @param post_entity $post entity * @return int The start time (timestamp) for a post */ private function get_start_time(discussion_entity $discussion, post_entity $post) { global $CFG; $posttime = $post->get_time_created(); $discussiontime = $discussion->get_time_start(); if (!empty($CFG->forum_enabletimedposts) && ($discussiontime > $posttime)) { return $discussiontime; } return $posttime; } } PK-B\}i^j!j!!classes/local/exporters/forum.phpnu[. /** * Forum Exporter. * * @package mod_forum * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\exporters; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\forum as forum_entity; use mod_forum\local\exporters\post as post_exporter; use core\external\exporter; use renderer_base; use stdClass; /** * Forum class. * * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class forum extends exporter { /** @var forum_entity The entity relating to the forum being displayed */ private $forum; /** * Constructor for the forum exporter. * * @param forum_entity $forum The forum being displayed * @param array $related The related objects */ public function __construct(forum_entity $forum, $related = []) { $this->forum = $forum; return parent::__construct([], $related); } /** * Return the list of additional properties. * * @return array */ protected static function define_other_properties() { return [ 'id' => ['type' => PARAM_INT], 'name' => ['type' => PARAM_RAW], 'state' => [ 'type' => [ 'groupmode' => ['type' => PARAM_INT], 'gradingenabled' => ['type' => PARAM_BOOL], ], ], 'userstate' => [ 'type' => [ 'tracked' => ['type' => PARAM_INT], ], ], 'capabilities' => [ 'type' => [ 'viewdiscussions' => ['type' => PARAM_BOOL], 'create' => ['type' => PARAM_BOOL], 'subscribe' => ['type' => PARAM_BOOL], 'grade' => ['type' => PARAM_BOOL], ] ], 'urls' => [ 'type' => [ 'create' => ['type' => PARAM_URL], 'markasread' => ['type' => PARAM_URL], 'view' => ['type' => PARAM_URL], 'sortrepliesasc' => ['type' => PARAM_URL], 'sortrepliesdesc' => ['type' => PARAM_URL], 'sortlastpostasc' => ['type' => PARAM_URL], 'sortlastpostdesc' => ['type' => PARAM_URL], 'sortcreatedasc' => ['type' => PARAM_URL], 'sortcreateddesc' => ['type' => PARAM_URL], 'sortdiscussionasc' => ['type' => PARAM_URL], 'sortdiscussiondesc' => ['type' => PARAM_URL], 'sortstarterasc' => ['type' => PARAM_URL], 'sortstarterdesc' => ['type' => PARAM_URL], 'sortgroupasc' => ['type' => PARAM_URL], 'sortgroupdesc' => ['type' => PARAM_URL], ], ], ]; } /** * Get the additional values to inject while exporting. * * @param renderer_base $output The renderer. * @return array Keys are the property names, values are their values. */ protected function get_other_values(renderer_base $output) { $capabilitymanager = $this->related['capabilitymanager']; $urlfactory = $this->related['urlfactory']; $user = $this->related['user']; $currentgroup = $this->related['currentgroup']; $vaultfactory = $this->related['vaultfactory']; $discussionvault = $vaultfactory->get_discussions_in_forum_vault(); return [ 'id' => $this->forum->get_id(), 'name' => $this->forum->get_name(), 'state' => [ 'groupmode' => $this->forum->get_effective_group_mode(), 'gradingenabled' => $this->forum->is_grading_enabled() ], 'userstate' => [ 'tracked' => forum_tp_is_tracked($this->get_forum_record(), $this->related['user']), ], 'capabilities' => [ 'viewdiscussions' => $capabilitymanager->can_view_discussions($user), 'create' => $capabilitymanager->can_create_discussions($user, $currentgroup), 'selfenrol' => $capabilitymanager->can_self_enrol($user), 'subscribe' => $capabilitymanager->can_subscribe_to_forum($user), 'grade' => $capabilitymanager->can_grade($user), ], 'urls' => [ 'create' => $urlfactory->get_discussion_create_url($this->forum)->out(false), 'markasread' => $urlfactory->get_mark_all_discussions_as_read_url($this->forum)->out(false), 'view' => $urlfactory->get_forum_view_url_from_forum($this->forum)->out(false), 'sortrepliesasc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null, $discussionvault::SORTORDER_REPLIES_ASC)->out(false), 'sortrepliesdesc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null, $discussionvault::SORTORDER_REPLIES_DESC)->out(false), 'sortlastpostasc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null, $discussionvault::SORTORDER_LASTPOST_ASC)->out(false), 'sortlastpostdesc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null, $discussionvault::SORTORDER_LASTPOST_DESC)->out(false), 'sortcreatedasc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null, $discussionvault::SORTORDER_CREATED_ASC)->out(false), 'sortcreateddesc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null, $discussionvault::SORTORDER_CREATED_DESC)->out(false), 'sortdiscussionasc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null, $discussionvault::SORTORDER_DISCUSSION_ASC)->out(false), 'sortdiscussiondesc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null, $discussionvault::SORTORDER_DISCUSSION_DESC)->out(false), 'sortstarterasc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null, $discussionvault::SORTORDER_STARTER_ASC)->out(false), 'sortstarterdesc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null, $discussionvault::SORTORDER_STARTER_DESC)->out(false), 'sortgroupasc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null, $discussionvault::SORTORDER_GROUP_ASC)->out(false), 'sortgroupdesc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null, $discussionvault::SORTORDER_GROUP_DESC)->out(false), ], ]; } /** * Returns a list of objects that are related. * * @return array */ protected static function define_related() { return [ 'legacydatamapperfactory' => 'mod_forum\local\factories\legacy_data_mapper', 'capabilitymanager' => 'mod_forum\local\managers\capability', 'urlfactory' => 'mod_forum\local\factories\url', 'user' => 'stdClass', 'currentgroup' => 'int?', 'vaultfactory' => 'mod_forum\local\factories\vault' ]; } /** * Get the legacy forum record for this forum. * * @return stdClass */ private function get_forum_record(): stdClass { $forumdbdatamapper = $this->related['legacydatamapperfactory']->get_forum_data_mapper(); return $forumdbdatamapper->to_legacy_object($this->forum); } } PK-B\.classes/local/exporters/discussion_summary.phpnu[. /** * Discussion summary exporter class. * * @package mod_forum * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\exporters; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\discussion_summary as discussion_summary_entity; use core\external\exporter; use renderer_base; /** * Discussion summary exporter class. * * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class discussion_summary extends exporter { /** @var discussion_summary_entity The discussion summary information */ private $summary; /** @var stdClass[] The group information for each author */ private $groupsbyid; /** @var stdClass[] The group information for each author */ private $groupsbyauthorid; /** @var int The number of replies to the discussion */ private $replycount; /** @var int number of unread posts if the user is tracking these */ private $unreadcount; /** @var int The latest post id in the discussion */ private $latestpostid; /** @var int|null The context id for the author of the first post */ private $firstpostauthorcontextid; /** @var int|null The context id for the author of the latest post */ private $latestpostauthorcontextid; /** * Constructor. * * @param discussion_summary_entity $summary The discussion summary information * @param stdClass[] $groupsbyid The group information for each author * @param stdClass[] $groupsbyauthorid The group information for each author * @param int $replycount The number of replies to the discussion * @param int $unreadcount number of unread posts if the user is tracking these * @param int $latestpostid The latest post id in the discussion * @param int|null $firstpostauthorcontextid The context id for the author of the first post * @param int|null $latestpostauthorcontextid The context id for the author of the latest post * @param array $related The related objects */ public function __construct( discussion_summary_entity $summary, array $groupsbyid, array $groupsbyauthorid, int $replycount, int $unreadcount, int $latestpostid, ?int $firstpostauthorcontextid, ?int $latestpostauthorcontextid, array $related = [] ) { $this->summary = $summary; $this->groupsbyid = $groupsbyid; $this->groupsbyauthorid = $groupsbyauthorid; $this->replycount = $replycount; $this->unreadcount = $unreadcount; $this->latestpostid = $latestpostid; $this->firstpostauthorcontextid = $firstpostauthorcontextid; $this->latestpostauthorcontextid = $latestpostauthorcontextid; return parent::__construct([], $related); } /** * Return the list of additional properties. * * @return array */ protected static function define_other_properties() { return [ 'id' => ['type' => PARAM_INT], 'discussion' => [ 'type' => discussion::read_properties_definition(), ], 'replies' => ['type' => PARAM_INT], 'unread' => ['type' => PARAM_INT], 'firstpostauthor' => [ 'type' => author::read_properties_definition(), ], 'latestpostauthor' => [ 'type' => author::read_properties_definition(), ], 'latestpostid' => ['type' => PARAM_INT], ]; } /** * Get the additional values to inject while exporting. * * @param renderer_base $output The renderer. * @return array Keys are the property names, values are their values. */ protected function get_other_values(renderer_base $output) { $capabilitymanager = $this->related['capabilitymanager']; $forum = $this->related['forum']; $user = $this->related['user']; $latestpostauthor = $this->related['latestauthor']; $discussion = $this->summary->get_discussion(); $related = (array) (object) $this->related; $related['latestpostid'] = $this->latestpostid; $related['groupsbyid'] = $this->groupsbyid; $discussionexporter = new discussion($discussion, $related); $related = [ 'urlfactory' => $this->related['urlfactory'], 'context' => $this->related['forum']->get_context(), 'forum' => $forum, ]; $firstpostauthor = new author( $this->summary->get_first_post_author(), $this->firstpostauthorcontextid, $this->groupsbyauthorid[$this->summary->get_first_post_author()->get_id()], $capabilitymanager->can_view_post( $user, $discussion, $this->summary->get_first_post() ), $related ); $latestpostauthor = new author( $latestpostauthor ?? $this->summary->get_latest_post_author(), $this->latestpostauthorcontextid, [], $capabilitymanager->can_view_post( $user, $discussion, $this->summary->get_first_post() ), $related ); return [ 'id' => $discussion->get_id(), 'discussion' => $discussionexporter->export($output), 'replies' => $this->replycount, 'unread' => $this->unreadcount, 'firstpostauthor' => $firstpostauthor->export($output), 'latestpostauthor' => $latestpostauthor->export($output), 'latestpostid' => $this->latestpostid, ]; } /** * Returns a list of objects that are related. * * @return array */ protected static function define_related() { return [ 'legacydatamapperfactory' => 'mod_forum\local\factories\legacy_data_mapper', 'context' => 'context', 'forum' => 'mod_forum\local\entities\forum', 'capabilitymanager' => 'mod_forum\local\managers\capability', 'urlfactory' => 'mod_forum\local\factories\url', 'user' => 'stdClass', 'favouriteids' => 'int[]?', 'latestauthor' => 'mod_forum\local\entities\author?' ]; } } PK-B\F1!classes/local/exporters/posts.phpnu[. /** * Posts exporter class. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\exporters; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\author as author_entity; use mod_forum\local\entities\post as post_entity; use mod_forum\local\exporters\post as post_exporter; use core\external\exporter; use renderer_base; require_once($CFG->dirroot . '/mod/forum/lib.php'); /** * Posts exporter class. * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class posts extends exporter { /** @var post_entity[] $posts List of posts to export */ private $posts; /** @var author_entity[] $authorsbyid List of authors for the posts indexed by author id */ private $authorsbyid; /** @var int[] $authorcontextids List of authors context ids indexed by author id */ private $authorcontextids; /** @var array $attachmentsbypostid List of attachments indexed by post id */ private $attachmentsbypostid; /** @var array $inlineattachmentsbypostid List of inline attachments indexed by post id */ private $inlineattachmentsbypostid; /** @var array $groupsbyauthorid List of author's groups indexed by author id */ private $groupsbyauthorid; /** @var array $tagsbypostid List of tags indexed by post id */ private $tagsbypostid; /** @var array $ratingbypostid List of ratings indexed by post id */ private $ratingbypostid; /** * Constructor. * * @param post_entity[] $posts List of posts to export * @param author_entity[] $authorsbyid List of authors for the posts indexed by author id * @param int[] $authorcontextids List of authors context ids indexed by author id * @param array $attachmentsbypostid List of attachments indexed by post id * @param array $groupsbyauthorid List of author's groups indexed by author id * @param array $tagsbypostid List of tags indexed by post id * @param array $ratingbypostid List of ratings indexed by post id * @param array $related The related objects for exporting * @param array $inlineattachmentsbypostid List of inline attachments indexed by post id */ public function __construct( array $posts, array $authorsbyid = [], array $authorcontextids = [], array $attachmentsbypostid = [], array $groupsbyauthorid = [], array $tagsbypostid = [], array $ratingbypostid = [], array $related = [], array $inlineattachmentsbypostid = [] ) { $this->posts = $posts; $this->authorsbyid = $authorsbyid; $this->authorcontextids = $authorcontextids; $this->attachmentsbypostid = $attachmentsbypostid; $this->inlineattachmentsbypostid = $inlineattachmentsbypostid; $this->groupsbyauthorid = $groupsbyauthorid; $this->tagsbypostid = $tagsbypostid; $this->ratingbypostid = $ratingbypostid; return parent::__construct([], $related); } /** * Return the list of additional properties. * * @return array */ protected static function define_other_properties() { return [ 'posts' => [ 'type' => post_exporter::read_properties_definition(), 'multiple' => true ] ]; } /** * Get the additional values to inject while exporting. * * @param renderer_base $output The renderer. * @return array Keys are the property names, values are their values. */ protected function get_other_values(renderer_base $output) { $related = $this->related; $authorsbyid = $this->authorsbyid; $authorcontextids = $this->authorcontextids; $attachmentsbypostid = $this->attachmentsbypostid; $inlineattachmentsbypostid = $this->inlineattachmentsbypostid; $groupsbyauthorid = $this->groupsbyauthorid; $tagsbypostid = $this->tagsbypostid; $ratingbypostid = $this->ratingbypostid; $exportedposts = array_map( function($post) use ( $related, $authorsbyid, $authorcontextids, $attachmentsbypostid, $groupsbyauthorid, $tagsbypostid, $ratingbypostid, $output, $inlineattachmentsbypostid ) { $authorid = $post->get_author_id(); $postid = $post->get_id(); $author = isset($authorsbyid[$authorid]) ? $authorsbyid[$authorid] : []; $authorcontextid = isset($authorcontextids[$authorid]) ? $authorcontextids[$authorid] : null; $attachments = isset($attachmentsbypostid[$postid]) ? $attachmentsbypostid[$postid] : []; $inlineattachments = isset($inlineattachmentsbypostid[$postid]) ? $inlineattachmentsbypostid[$postid] : []; $authorgroups = isset($groupsbyauthorid[$authorid]) ? $groupsbyauthorid[$authorid] : []; $tags = isset($tagsbypostid[$postid]) ? $tagsbypostid[$postid] : []; $rating = isset($ratingbypostid[$postid]) ? $ratingbypostid[$postid] : null; $exporter = new post_exporter($post, array_merge($related, [ 'author' => $author, 'authorcontextid' => $authorcontextid, 'attachments' => $attachments, 'messageinlinefiles' => $inlineattachments, 'authorgroups' => $authorgroups, 'tags' => $tags, 'rating' => $rating ])); return $exporter->export($output); }, $this->posts ); return [ 'posts' => $exportedposts ]; } /** * Returns a list of objects that are related. * * @return array */ protected static function define_related() { return [ 'capabilitymanager' => 'mod_forum\local\managers\capability', 'urlfactory' => 'mod_forum\local\factories\url', 'forum' => 'mod_forum\local\entities\forum', 'discussion' => 'mod_forum\local\entities\discussion', 'readreceiptcollection' => 'mod_forum\local\entities\post_read_receipt_collection?', 'user' => 'stdClass', 'context' => 'context', 'includehtml' => 'bool' ]; } } PK-B\O. . !classes/local/exporters/group.phpnu[. /** * Course Group exporter. * * @package mod_forum * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\exporters; defined('MOODLE_INTERNAL') || die(); use core\external\exporter; use renderer_base; use stdClass; require_once($CFG->dirroot . '/mod/forum/lib.php'); /** * Group exporter. * * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class group extends exporter { /** @var stdClass $group Group */ private $group; /** * Constructor. * * @param stdClass $group The group to export * @param array $related The related data for the export. */ public function __construct(stdClass $group, array $related = []) { $this->group = $group; return parent::__construct([], $related); } /** * Return the list of additional properties. * * @return array */ protected static function define_other_properties() { return [ 'id' => [ 'type' => PARAM_INT, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'urls' => [ 'type' => [ 'image' => [ 'description' => 'The URL for the group image', 'type' => PARAM_URL, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ] ], ], ]; } /** * Get the additional values to inject while exporting. * * @param renderer_base $output The renderer. * @return array Keys are the property names, values are their values. */ protected function get_other_values(renderer_base $output) { return [ 'id' => $group->id, 'urls' => [ 'image' => $imageurl ? $imageurl->out(false) : null ] ]; } /** * Returns a list of objects that are related. * * @return array */ protected static function define_related() { return [ 'urlmanager' => 'mod_forum\local\managers\url', 'context' => 'context' ]; } } PK-B\^,,&classes/local/exporters/discussion.phpnu[. /** * Discussion exporter class. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\exporters; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\discussion as discussion_entity; use mod_forum\local\exporters\post as post_exporter; use mod_forum\local\factories\exporter as exporter_factory; use core\external\exporter; use renderer_base; /** * Discussion exporter class. * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class discussion extends exporter { /** @var discussion_entity $discussion Discussion to export */ private $discussion; /** * Constructor. * * @param discussion_entity $discussion Discussion to export * @param array $related The related export data */ public function __construct(discussion_entity $discussion, array $related = []) { $this->discussion = $discussion; return parent::__construct([], $related); } /** * Return the list of additional properties. * * @return array */ protected static function define_other_properties() { return [ 'id' => ['type' => PARAM_INT], 'forumid' => ['type' => PARAM_INT], 'pinned' => ['type' => PARAM_BOOL], 'locked' => ['type' => PARAM_BOOL], 'istimelocked' => ['type' => PARAM_BOOL], 'name' => ['type' => PARAM_TEXT], 'firstpostid' => ['type' => PARAM_INT], 'group' => [ 'optional' => true, 'type' => [ 'name' => ['type' => PARAM_TEXT], 'urls' => [ 'type' => [ 'picture' => [ 'optional' => true, 'type' => PARAM_URL, ], 'userlist' => [ 'optional' => true, 'type' => PARAM_URL, ], ], ], ], ], 'times' => [ 'type' => [ 'modified' => ['type' => PARAM_INT], 'start' => ['type' => PARAM_INT], 'end' => ['type' => PARAM_INT], 'locked' => ['type' => PARAM_INT], ], ], 'userstate' => [ 'type' => [ 'subscribed' => ['type' => PARAM_BOOL], 'favourited' => ['type' => PARAM_BOOL], ], ], 'capabilities' => [ 'type' => [ 'subscribe' => ['type' => PARAM_BOOL], 'move' => ['type' => PARAM_BOOL], 'pin' => ['type' => PARAM_BOOL], 'post' => ['type' => PARAM_BOOL], 'manage' => ['type' => PARAM_BOOL], 'favourite' => ['type' => PARAM_BOOL] ] ], 'urls' => [ 'type' => [ 'view' => ['type' => PARAM_URL], 'viewlatest' => [ 'optional' => true, 'type' => PARAM_URL ], 'viewfirstunread' => [ 'optional' => true, 'type' => PARAM_URL, ], 'markasread' => ['type' => PARAM_URL], 'subscribe' => ['type' => PARAM_URL], 'pin' => [ 'optional' => true, 'type' => PARAM_URL, ], ], ], 'timed' => [ 'type' => [ 'istimed' => [ 'type' => PARAM_BOOL, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'visible' => [ 'type' => PARAM_BOOL, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ] ] ] ]; } /** * Get the additional values to inject while exporting. * * @param renderer_base $output The renderer. * @return array Keys are the property names, values are their values. */ protected function get_other_values(renderer_base $output) { $capabilitymanager = $this->related['capabilitymanager']; $urlfactory = $this->related['urlfactory']; $favouriteids = isset($this->related['favouriteids']) ? $this->related['favouriteids'] : []; $forum = $this->related['forum']; $forumrecord = $this->get_forum_record(); $user = $this->related['user']; $discussion = $this->discussion; $groupdata = null; if ($discussion->has_group()) { $groupsbyid = $this->related['groupsbyid']; $group = $groupsbyid[$discussion->get_group_id()] ?? null; // We may not have received the group if the caller doesn't want to include it in the export // or if it's been deleted and the discussion record hasn't been updated. if ($group) { $groupdata = [ 'name' => format_string($group->name, true, ['context' => $this->related['context']]), 'urls' => [], ]; // If not hiding the group picture, and the group has a picture then use it. Fallback to generic group image. if ($url = get_group_picture_url($group, $forum->get_course_id(), true)) { $groupdata['urls']['picture'] = $url; } else { $groupdata['urls']['picture'] = $output->image_url('g/g1')->out(false); } if ($capabilitymanager->can_view_participants($user, $discussion)) { $groupdata['urls']['userlist'] = (new \moodle_url('/user/index.php', [ 'id' => $forum->get_course_id(), 'group' => $group->id, ])); } } } $viewfirstunreadurl = $urlfactory->get_discussion_view_first_unread_post_url_from_discussion($discussion); $data = [ 'id' => $discussion->get_id(), 'forumid' => $forum->get_id(), 'pinned' => $discussion->is_pinned(), 'locked' => $forum->is_discussion_locked($discussion), 'istimelocked' => $forum->is_discussion_time_locked($discussion), 'name' => format_string($discussion->get_name(), true, [ 'context' => $this->related['context'] ]), 'firstpostid' => $discussion->get_first_post_id(), 'times' => [ 'modified' => $discussion->get_time_modified(), 'start' => $discussion->get_time_start(), 'end' => $discussion->get_time_end(), 'locked' => $discussion->get_locked() ], 'userstate' => [ 'subscribed' => \mod_forum\subscriptions::is_subscribed($user->id, $forumrecord, $discussion->get_id()), 'favourited' => in_array($discussion->get_id(), $favouriteids) ? true : false, ], 'capabilities' => [ 'subscribe' => $capabilitymanager->can_subscribe_to_discussion($user, $discussion), 'move' => $capabilitymanager->can_move_discussion($user, $discussion), 'pin' => $capabilitymanager->can_pin_discussion($user, $discussion), 'post' => $capabilitymanager->can_post_in_discussion($user, $discussion), 'manage' => $capabilitymanager->can_manage_forum($user), 'favourite' => $capabilitymanager->can_favourite_discussion($user) // Defaulting to true until we get capabilities sorted ], 'urls' => [ 'view' => $urlfactory->get_discussion_view_url_from_discussion($discussion)->out(false), 'viewfirstunread' => $viewfirstunreadurl->out(false), 'markasread' => $urlfactory->get_mark_discussion_as_read_url_from_discussion($forum, $discussion)->out(false), 'subscribe' => $urlfactory->get_discussion_subscribe_url($discussion)->out(false) ] ]; if (!empty($this->related['latestpostid'])) { $data['urls']['viewlatest'] = $urlfactory->get_discussion_view_latest_post_url_from_discussion( $discussion, $this->related['latestpostid'] )->out(false); } if ($capabilitymanager->can_pin_discussions($user)) { $data['urls']['pin'] = $urlfactory->get_pin_discussion_url_from_discussion($discussion)->out(false); } if ($groupdata) { $data['group'] = $groupdata; } $canviewhiddentimedposts = $capabilitymanager->can_view_hidden_posts($user); $canalwaysseetimedpost = $user->id == $discussion->get_user_id() || $canviewhiddentimedposts; $data['timed']['istimed'] = $canalwaysseetimedpost ? $discussion->is_timed_discussion() : null; $data['timed']['visible'] = $canalwaysseetimedpost ? $discussion->is_timed_discussion_visible() : null; return $data; } /** * Get the legacy forum record from the forum entity. * * @return stdClass */ private function get_forum_record() { $forumdbdatamapper = $this->related['legacydatamapperfactory']->get_forum_data_mapper(); return $forumdbdatamapper->to_legacy_object($this->related['forum']); } /** * Returns a list of objects that are related. * * @return array */ protected static function define_related() { return [ 'legacydatamapperfactory' => 'mod_forum\local\factories\legacy_data_mapper', 'context' => 'context', 'forum' => 'mod_forum\local\entities\forum', 'capabilitymanager' => 'mod_forum\local\managers\capability', 'urlfactory' => 'mod_forum\local\factories\url', 'user' => 'stdClass', 'groupsbyid' => 'stdClass[]', 'latestpostid' => 'int?', 'favouriteids' => 'int[]?' ]; } } PK-B\z*""0classes/local/exporters/discussion_summaries.phpnu[. /** * Discussion summaries exporter. * * @package mod_forum * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\exporters; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\discussion as discussion_entity; use mod_forum\local\exporters\post as post_exporter; use core\external\exporter; use renderer_base; /** * Discussion summaries exporter. * * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class discussion_summaries extends exporter { /** @var discussion_summary_entity[] The list of discussion summaries to export */ private $discussions; /** @var stdClass[] The group information for each author */ private $groupsbyid; /** @var stdClass[] The group information for each author */ private $groupsbyauthorid; /** @var int[] Discussion reply counts indexed by dicussion id */ private $discussionreplycount; /** @var int[] Discussion unread counts indexed by dicussion id */ private $discussionunreadcount; /** @var array The latest post in each discussion */ private $latestpostids; /** @var int[] The context ids for the first and latest post authors (indexed by author id) */ private $postauthorcontextids; /** * Constructor. * * @param discussion_summary_entity[] $discussion The list of discussion summaries to export * @param stdClass[] $groupsbyid The group information for each author * @param stdClass[] $groupsbyauthorid The group information for each author * @param int[] $discussionreplycount Discussion reply counts indexed by dicussion id * @param int[] $discussionunreadcount Discussion unread counts indexed by dicussion id * @param int[] $latestpostids List of latest post ids indexed by discussion id * @param int[] $postauthorcontextids The context ids for the first and latest post authors (indexed by author id) * @param array $related The related */ public function __construct( array $discussions, array $groupsbyid, array $groupsbyauthorid, array $discussionreplycount, array $discussionunreadcount, array $latestpostids, array $postauthorcontextids, array $related = [] ) { $this->discussions = $discussions; $this->groupsbyid = $groupsbyid; $this->groupsbyauthorid = $groupsbyauthorid; $this->discussionreplycount = $discussionreplycount; $this->discussionunreadcount = $discussionunreadcount; $this->latestpostids = $latestpostids; $this->postauthorcontextids = $postauthorcontextids; return parent::__construct([], $related); } /** * Return the list of additional properties. * * @return array */ protected static function define_other_properties() { return [ 'summaries' => [ 'type' => discussion_summary::read_properties_definition(), 'multiple' => true ], 'state' => [ 'type' => [ 'hasdiscussions' => ['type' => PARAM_BOOL], ], ], ]; } /** * Get the additional values to inject while exporting. * * @param renderer_base $output The renderer. * @return array Keys are the property names, values are their values. */ protected function get_other_values(renderer_base $output) { $exporteddiscussions = []; $related = $this->related; $latestauthors = $this->related['latestauthors']; foreach ($this->discussions as $discussion) { $discussionid = $discussion->get_discussion()->get_id(); $replycount = isset($this->discussionreplycount[$discussionid]) ? $this->discussionreplycount[$discussionid] : 0; $unreadcount = isset($this->discussionunreadcount[$discussionid]) ? $this->discussionunreadcount[$discussionid] : 0; $latestpostid = isset($this->latestpostids[$discussionid]) ? $this->latestpostids[$discussionid] : 0; $latestauthor = $latestauthors[$discussionid] ?? null; $related['latestauthor'] = $latestauthor; $exporter = new discussion_summary( $discussion, $this->groupsbyid, $this->groupsbyauthorid, $replycount, $unreadcount, $latestpostid, $this->postauthorcontextids[$discussion->get_first_post_author()->get_id()], $this->postauthorcontextids[$latestauthor->get_id()], $related ); $exporteddiscussions[] = $exporter->export($output); } return [ 'summaries' => $exporteddiscussions, 'state' => [ 'hasdiscussions' => !empty($exporteddiscussions), ], ]; } /** * Returns a list of objects that are related. * * @return array */ protected static function define_related() { return [ 'legacydatamapperfactory' => 'mod_forum\local\factories\legacy_data_mapper', 'context' => 'context', 'forum' => 'mod_forum\local\entities\forum', 'capabilitymanager' => 'mod_forum\local\managers\capability', 'urlfactory' => 'mod_forum\local\factories\url', 'user' => 'stdClass', 'favouriteids' => 'int[]?', 'latestauthors' => 'mod_forum\local\entities\author[]?' ]; } } PK-B\iUeOO"classes/local/exporters/author.phpnu[. /** * Author exporter. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\exporters; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\author as author_entity; use mod_forum\local\exporters\group as group_exporter; use core\external\exporter; use renderer_base; require_once($CFG->dirroot . '/mod/forum/lib.php'); /** * Author exporter. * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class author extends exporter { /** @var author_entity $author Author entity */ private $author; /** @var int|null $authorcontextid The context id for the author entity */ private $authorcontextid; /** @var array $authorgroups List of groups that the author belongs to */ private $authorgroups; /** @var bool $canview Should the author be anonymised? */ private $canview; /** * Constructor. * * @param author_entity $author The author entity to export * @param int|null $authorcontextid The context id for the author entity to export (null if the user doesn't have one) * @param stdClass[] $authorgroups The list of groups that the author belongs to * @param bool $canview Can the requesting user view this author or should it be anonymised? * @param array $related The related data for the export. */ public function __construct( author_entity $author, ?int $authorcontextid, array $authorgroups = [], bool $canview = true, array $related = [] ) { $this->author = $author; $this->authorcontextid = $authorcontextid; $this->authorgroups = $authorgroups; $this->canview = $canview; return parent::__construct([], $related); } /** * Return the list of additional properties. * * @return array */ protected static function define_other_properties() { return [ 'id' => [ 'type' => PARAM_INT, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'fullname' => [ 'type' => PARAM_TEXT, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'isdeleted' => [ 'type' => PARAM_BOOL, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'groups' => [ 'multiple' => true, 'optional' => true, 'type' => [ 'id' => ['type' => PARAM_INT], 'name' => ['type' => PARAM_TEXT], 'urls' => [ 'type' => [ 'image' => [ 'type' => PARAM_URL, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ] ] ] ] ], 'urls' => [ 'type' => [ 'profile' => [ 'description' => 'The URL for the use profile page', 'type' => PARAM_URL, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], 'profileimage' => [ 'description' => 'The URL for the use profile image', 'type' => PARAM_URL, 'optional' => true, 'default' => null, 'null' => NULL_ALLOWED ], ] ] ]; } /** * Get the additional values to inject while exporting. * * @param renderer_base $output The renderer. * @return array Keys are the property names, values are their values. */ protected function get_other_values(renderer_base $output) { $author = $this->author; $authorcontextid = $this->authorcontextid; $urlfactory = $this->related['urlfactory']; $context = $this->related['context']; $forum = $this->related['forum']; if ($this->canview) { if ($author->is_deleted()) { return [ 'id' => $author->get_id(), 'fullname' => get_string('deleteduser', 'mod_forum'), 'isdeleted' => true, 'groups' => [], 'urls' => [ 'profile' => ($urlfactory->get_author_profile_url($author, $forum->get_course_id()))->out(false), 'profileimage' => ($urlfactory->get_author_profile_image_url($author, $authorcontextid))->out(false) ] ]; } else { $groups = array_map(function($group) use ($urlfactory, $context, $output) { $groupurl = null; $imageurl = get_group_picture_url($group, $group->courseid, true); if (course_can_view_participants($context)) { $groupurl = $urlfactory->get_author_group_url($group); } return [ 'id' => $group->id, 'name' => format_string($group->name, true, ['context' => $context]), 'urls' => [ 'image' => $imageurl ? $imageurl->out(false) : null, 'group' => $groupurl ? $groupurl->out(false) : null ] ]; }, $this->authorgroups); return [ 'id' => $author->get_id(), 'fullname' => $author->get_full_name(), 'isdeleted' => false, 'groups' => $groups, 'urls' => [ 'profile' => ($urlfactory->get_author_profile_url($author, $forum->get_course_id()))->out(false), 'profileimage' => ($urlfactory->get_author_profile_image_url($author, $authorcontextid))->out(false) ] ]; } } else { // The author should be anonymised. return [ 'id' => null, 'fullname' => get_string('forumauthorhidden', 'mod_forum'), 'isdeleted' => null, 'groups' => [], 'urls' => [ 'profile' => null, 'profileimage' => null ] ]; } } /** * Returns a list of objects that are related. * * @return array */ protected static function define_related() { return [ 'urlfactory' => 'mod_forum\local\factories\url', 'context' => 'context', 'forum' => 'mod_forum\local\entities\forum', ]; } } PK-B\?.classes/local/builders/exported_discussion.phpnu[. /** * Exported discussion builder class. * * @package mod_forum * @copyright 2019 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\builders; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\discussion as discussion_entity; use mod_forum\local\entities\forum as forum_entity; use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory; use mod_forum\local\factories\exporter as exporter_factory; use mod_forum\local\factories\vault as vault_factory; use rating_manager; use renderer_base; use stdClass; /** * Exported discussion builder class * * This class is an implementation of the builder pattern (loosely). It is responsible * for taking a set of related forums, discussions, and posts and generate the exported * version of the discussion. * * It encapsulates the complexity involved with exporting discussions. All of the relevant * additional resources will be loaded by this class in order to ensure the exporting * process can happen. * * See this doc for more information on the builder pattern: * https://designpatternsphp.readthedocs.io/en/latest/Creational/Builder/README.html * * @copyright 2019 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class exported_discussion { /** @var renderer_base $renderer Core renderer */ private $renderer; /** @var legacy_data_mapper_factory $legacydatamapperfactory Data mapper factory */ private $legacydatamapperfactory; /** @var exporter_factory $exporterfactory Exporter factory */ private $exporterfactory; /** @var vault_factory $vaultfactory Vault factory */ private $vaultfactory; /** @var rating_manager $ratingmanager Rating manager */ private $ratingmanager; /** * Constructor. * * @param renderer_base $renderer Core renderer * @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory * @param exporter_factory $exporterfactory Exporter factory * @param vault_factory $vaultfactory Vault factory * @param rating_manager $ratingmanager Rating manager */ public function __construct( renderer_base $renderer, legacy_data_mapper_factory $legacydatamapperfactory, exporter_factory $exporterfactory, vault_factory $vaultfactory, rating_manager $ratingmanager ) { $this->renderer = $renderer; $this->legacydatamapperfactory = $legacydatamapperfactory; $this->exporterfactory = $exporterfactory; $this->vaultfactory = $vaultfactory; $this->ratingmanager = $ratingmanager; } /** * Build any additional variables for the exported discussion for a given set of discussions. * * This will typically be used for a list of discussions in the same forum. * * @param stdClass $user The user to export the posts for. * @param forum_entity $forum The forum that each of the $discussions belong to * @param discussion_entity $discussion A list of all discussions that each of the $posts belong to * @return stdClass[] List of exported posts in the same order as the $posts array. */ public function build( stdClass $user, forum_entity $forum, discussion_entity $discussion ): array { $favouriteids = []; if ($this->is_favourited($discussion, $forum->get_context(), $user)) { $favouriteids[] = $discussion->get_id(); } $groupsbyid = $this->get_groups_available_in_forum($forum); $discussionexporter = $this->exporterfactory->get_discussion_exporter( $user, $forum, $discussion, $groupsbyid, $favouriteids ); return (array) $discussionexporter->export($this->renderer); } /** * Get the groups details for all groups available to the forum. * @param forum_entity $forum The forum entity * @return stdClass[] */ private function get_groups_available_in_forum($forum): array { $course = $forum->get_course_record(); $coursemodule = $forum->get_course_module_record(); return groups_get_all_groups($course->id, 0, $coursemodule->groupingid); } /** * Check whether the provided discussion has been favourited by the user. * * @param discussion_entity $discussion The discussion record * @param \context_module $forumcontext Forum context * @param \stdClass $user The user to check the favourite against * * @return bool Whether or not the user has favourited the discussion */ public function is_favourited(discussion_entity $discussion, \context_module $forumcontext, \stdClass $user) { if (!isloggedin()) { return false; } $usercontext = \context_user::instance($user->id); $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext); return $ufservice->favourite_exists('mod_forum', 'discussions', $discussion->get_id(), $forumcontext); } } PK-B\D 008classes/local/builders/exported_discussion_summaries.phpnu[. /** * Exported discussion summaries builder class. * * @package mod_forum * @copyright 2019 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\builders; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\discussion as discussion_entity; use mod_forum\local\entities\forum as forum_entity; use mod_forum\local\entities\post as post_entity; use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory; use mod_forum\local\factories\exporter as exporter_factory; use mod_forum\local\factories\vault as vault_factory; use mod_forum\local\factories\manager as manager_factory; use rating_manager; use renderer_base; use stdClass; /** * Exported discussion summaries builder class. * * This class is an implementation of the builder pattern (loosely). It is responsible * for taking a set of related forums, discussions, and posts and generate the exported * version of the discussion summaries. * * It encapsulates the complexity involved with exporting discussions summaries. All of the relevant * additional resources will be loaded by this class in order to ensure the exporting * process can happen. * * See this doc for more information on the builder pattern: * https://designpatternsphp.readthedocs.io/en/latest/Creational/Builder/README.html * * @package mod_forum * @copyright 2019 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class exported_discussion_summaries { /** @var renderer_base $renderer Core renderer */ private $renderer; /** @var legacy_data_mapper_factory $legacydatamapperfactory Data mapper factory */ private $legacydatamapperfactory; /** @var exporter_factory $exporterfactory Exporter factory */ private $exporterfactory; /** @var vault_factory $vaultfactory Vault factory */ private $vaultfactory; /** @var manager_factory $managerfactory Manager factory */ private $managerfactory; /** @var rating_manager $ratingmanager Rating manager */ private $ratingmanager; /** * Constructor. * * @param renderer_base $renderer Core renderer * @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory * @param exporter_factory $exporterfactory Exporter factory * @param vault_factory $vaultfactory Vault factory * @param manager_factory $managerfactory Manager factory */ public function __construct( renderer_base $renderer, legacy_data_mapper_factory $legacydatamapperfactory, exporter_factory $exporterfactory, vault_factory $vaultfactory, manager_factory $managerfactory ) { $this->renderer = $renderer; $this->legacydatamapperfactory = $legacydatamapperfactory; $this->exporterfactory = $exporterfactory; $this->vaultfactory = $vaultfactory; $this->managerfactory = $managerfactory; $this->ratingmanager = $managerfactory->get_rating_manager(); } /** * Build the exported discussion summaries for a given set of discussions. * * This will typically be used for a list of discussions in the same forum. * * @param stdClass $user The user to export the posts for. * @param forum_entity $forum The forum that each of the $discussions belong to * @param discussion_summary_entity[] $discussions A list of all discussion summaries to export * @return stdClass[] List of exported posts in the same order as the $posts array. */ public function build( stdClass $user, forum_entity $forum, array $discussions ): array { $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $canseeanyprivatereply = $capabilitymanager->can_view_any_private_reply($user); $discussionids = array_keys($discussions); $postvault = $this->vaultfactory->get_post_vault(); $posts = $postvault->get_from_discussion_ids($user, $discussionids, $canseeanyprivatereply); $groupsbyid = $this->get_groups_available_in_forum($forum); $groupsbyauthorid = $this->get_author_groups_from_posts($posts, $forum); $replycounts = $postvault->get_reply_count_for_discussion_ids($user, $discussionids, $canseeanyprivatereply); $latestposts = $postvault->get_latest_posts_for_discussion_ids($user, $discussionids, $canseeanyprivatereply); $latestauthors = $this->get_latest_posts_authors($latestposts); $latestpostsids = array_map(function($post) { return $post->get_id(); }, $latestposts); $postauthorids = array_unique(array_reduce($discussions, function($carry, $summary) use ($latestposts){ $firstpostauthorid = $summary->get_first_post_author()->get_id(); $discussion = $summary->get_discussion(); $lastpostauthorid = $latestposts[$discussion->get_id()]->get_author_id(); return array_merge($carry, [$firstpostauthorid, $lastpostauthorid]); }, [])); $postauthorcontextids = $this->get_author_context_ids($postauthorids); $unreadcounts = []; $favourites = $this->get_favourites($user); $forumdatamapper = $this->legacydatamapperfactory->get_forum_data_mapper(); $forumrecord = $forumdatamapper->to_legacy_object($forum); if (forum_tp_can_track_forums($forumrecord)) { $unreadcounts = $postvault->get_unread_count_for_discussion_ids($user, $discussionids, $canseeanyprivatereply); } $summaryexporter = $this->exporterfactory->get_discussion_summaries_exporter( $user, $forum, $discussions, $groupsbyid, $groupsbyauthorid, $replycounts, $unreadcounts, $latestpostsids, $postauthorcontextids, $favourites, $latestauthors ); $exportedposts = (array) $summaryexporter->export($this->renderer); $firstposts = $postvault->get_first_post_for_discussion_ids($discussionids); array_walk($exportedposts['summaries'], function($summary) use ($firstposts, $latestposts) { $summary->discussion->times['created'] = (int) $firstposts[$summary->discussion->firstpostid]->get_time_created(); $summary->discussion->times['modified'] = (int) $latestposts[$summary->discussion->id]->get_time_created(); }); // Pass the current, preferred sort order for the discussions list. $discussionlistvault = $this->vaultfactory->get_discussions_in_forum_vault(); $sortorder = get_user_preferences('forum_discussionlistsortorder', $discussionlistvault::SORTORDER_LASTPOST_DESC); $sortoptions = array( 'islastpostdesc' => $sortorder == $discussionlistvault::SORTORDER_LASTPOST_DESC, 'islastpostasc' => $sortorder == $discussionlistvault::SORTORDER_LASTPOST_ASC, 'isrepliesdesc' => $sortorder == $discussionlistvault::SORTORDER_REPLIES_DESC, 'isrepliesasc' => $sortorder == $discussionlistvault::SORTORDER_REPLIES_ASC, 'iscreateddesc' => $sortorder == $discussionlistvault::SORTORDER_CREATED_DESC, 'iscreatedasc' => $sortorder == $discussionlistvault::SORTORDER_CREATED_ASC, 'isdiscussiondesc' => $sortorder == $discussionlistvault::SORTORDER_DISCUSSION_DESC, 'isdiscussionasc' => $sortorder == $discussionlistvault::SORTORDER_DISCUSSION_ASC, 'isstarterdesc' => $sortorder == $discussionlistvault::SORTORDER_STARTER_DESC, 'isstarterasc' => $sortorder == $discussionlistvault::SORTORDER_STARTER_ASC, 'isgroupdesc' => $sortorder == $discussionlistvault::SORTORDER_GROUP_DESC, 'isgroupasc' => $sortorder == $discussionlistvault::SORTORDER_GROUP_ASC, ); $exportedposts['state']['sortorder'] = $sortoptions; return $exportedposts; } /** * Get a list of all favourited discussions. * * @param stdClass $user The user we are getting favourites for * @return int[] A list of favourited itemids */ private function get_favourites(stdClass $user): array { $ids = []; if (isloggedin()) { $usercontext = \context_user::instance($user->id); $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext); $favourites = $ufservice->find_favourites_by_type('mod_forum', 'discussions'); foreach ($favourites as $favourite) { $ids[] = $favourite->itemid; } } return $ids; } /** * Returns a mapped array of discussionid to the authors of the latest post * * @param array $latestposts Mapped array of discussion to latest posts. * @return array Array of authors mapped to the discussion */ private function get_latest_posts_authors($latestposts) { $authors = $this->vaultfactory->get_author_vault()->get_authors_for_posts($latestposts); $mappedauthors = array_reduce($latestposts, function($carry, $item) use ($authors) { $carry[$item->get_discussion_id()] = $authors[$item->get_author_id()]; return $carry; }, []); return $mappedauthors; } /** * Get the groups details for all groups available to the forum. * @param forum_entity $forum The forum entity * @return stdClass[] */ private function get_groups_available_in_forum($forum): array { $course = $forum->get_course_record(); $coursemodule = $forum->get_course_module_record(); return groups_get_all_groups($course->id, 0, $coursemodule->groupingid); } /** * Get the author's groups for a list of posts. * * @param post_entity[] $posts The list of posts * @param forum_entity $forum The forum entity * @return array Author groups indexed by author id */ private function get_author_groups_from_posts(array $posts, $forum): array { $course = $forum->get_course_record(); $coursemodule = $forum->get_course_module_record(); $authorids = array_reduce($posts, function($carry, $post) { $carry[$post->get_author_id()] = true; return $carry; }, []); $authorgroups = groups_get_all_groups($course->id, array_keys($authorids), $coursemodule->groupingid, 'g.*, gm.id, gm.groupid, gm.userid'); $authorgroups = array_reduce($authorgroups, function($carry, $group) { // Clean up data returned from groups_get_all_groups. $userid = $group->userid; $groupid = $group->groupid; unset($group->userid); unset($group->groupid); $group->id = $groupid; if (!isset($carry[$userid])) { $carry[$userid] = [$group]; } else { $carry[$userid][] = $group; } return $carry; }, []); foreach (array_diff(array_keys($authorids), array_keys($authorgroups)) as $authorid) { $authorgroups[$authorid] = []; } return $authorgroups; } /** * Get the user context ids for each of the authors. * * @param int[] $authorids The list of author ids to fetch context ids for. * @return int[] Context ids indexed by author id */ private function get_author_context_ids(array $authorids): array { $authorvault = $this->vaultfactory->get_author_vault(); return $authorvault->get_context_ids_for_author_ids($authorids); } } PK-B\$LGZGZ)classes/local/builders/exported_posts.phpnu[. /** * Exported post builder class. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\builders; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\discussion as discussion_entity; use mod_forum\local\entities\forum as forum_entity; use mod_forum\local\entities\post as post_entity; use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory; use mod_forum\local\factories\exporter as exporter_factory; use mod_forum\local\factories\vault as vault_factory; use mod_forum\local\factories\manager as manager_factory; use core_tag_tag; use moodle_exception; use renderer_base; use stdClass; /** * Exported post builder class. * * This class is an implementation of the builder pattern (loosely). It is responsible * for taking a set of related forums, discussions, and posts and generate the exported * version of the posts. * * It encapsulates the complexity involved with exporting posts. All of the relevant * additional resources will be loaded by this class in order to ensure the exporting * process can happen. * * See this doc for more information on the builder pattern: * https://designpatternsphp.readthedocs.io/en/latest/Creational/Builder/README.html * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class exported_posts { /** @var renderer_base $renderer Core renderer */ private $renderer; /** @var legacy_data_mapper_factory $legacydatamapperfactory Data mapper factory */ private $legacydatamapperfactory; /** @var exporter_factory $exporterfactory Exporter factory */ private $exporterfactory; /** @var vault_factory $vaultfactory Vault factory */ private $vaultfactory; /** @var rating_manager $ratingmanager Rating manager */ private $ratingmanager; /** @var manager_factory $managerfactory Manager factory */ private $managerfactory; /** * Constructor. * * @param renderer_base $renderer Core renderer * @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory * @param exporter_factory $exporterfactory Exporter factory * @param vault_factory $vaultfactory Vault factory * @param manager_factory $managerfactory Manager factory */ public function __construct( renderer_base $renderer, legacy_data_mapper_factory $legacydatamapperfactory, exporter_factory $exporterfactory, vault_factory $vaultfactory, manager_factory $managerfactory ) { $this->renderer = $renderer; $this->legacydatamapperfactory = $legacydatamapperfactory; $this->exporterfactory = $exporterfactory; $this->vaultfactory = $vaultfactory; $this->managerfactory = $managerfactory; $this->ratingmanager = $managerfactory->get_rating_manager(); } /** * Build the exported posts for a given set of forums, discussions, and posts. * * This will typically be used for a list of posts in the same discussion/forum however * it does support exporting any arbitrary list of posts as long as the caller also provides * a unique list of all discussions for the list of posts and all forums for the list of discussions. * * Increasing the number of different forums being processed will increase the processing time * due to processing multiple contexts (for things like capabilities, files, etc). The code attempts * to load the additional resources as efficiently as possible but there is no way around some of * the additional overhead. * * Note: Some posts will be removed as part of the build process according to capabilities. * A one-to-one mapping should not be expected. * * @param stdClass $user The user to export the posts for. * @param forum_entity[] $forums A list of all forums that each of the $discussions belong to * @param discussion_entity[] $discussions A list of all discussions that each of the $posts belong to * @param post_entity[] $posts The list of posts to export. * @param bool $includeinlineattachments Whether inline attachments should be included or not. * @return stdClass[] List of exported posts in the same order as the $posts array. */ public function build( stdClass $user, array $forums, array $discussions, array $posts, bool $includeinlineattachments = false ): array { // Format the forums and discussion to make them more easily accessed later. $forums = array_reduce($forums, function($carry, $forum) { $carry[$forum->get_id()] = $forum; return $carry; }, []); $discussions = array_reduce($discussions, function($carry, $discussion) { $carry[$discussion->get_id()] = $discussion; return $carry; }, []); // Group the posts by discussion and forum so that we can load the resources in // batches to improve performance. $groupedposts = $this->group_posts_by_discussion($forums, $discussions, $posts); // Load all of the resources we need in order to export the posts. $authorsbyid = $this->get_authors_for_posts($posts); $authorcontextids = $this->get_author_context_ids(array_keys($authorsbyid)); $attachmentsbypostid = $this->get_attachments_for_posts($groupedposts); $inlineattachments = []; if ($includeinlineattachments) { $inlineattachments = $this->get_inline_attachments_for_posts($groupedposts); } $groupsbycourseandauthorid = $this->get_author_groups_from_posts($groupedposts); $tagsbypostid = $this->get_tags_from_posts($posts); $ratingbypostid = $this->get_ratings_from_posts($user, $groupedposts); $readreceiptcollectionbyforumid = $this->get_read_receipts_from_posts($user, $groupedposts); $exportedposts = []; // Export each set of posts per discussion because it's the largest chunks we can // break them into due to constraints on capability checks. foreach ($groupedposts as $grouping) { [ 'forum' => $forum, 'discussion' => $discussion, 'posts' => $groupedposts ] = $grouping; // Exclude posts the user cannot see, such as certain posts in Q and A forums. $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $groupedposts = array_filter($groupedposts, function($post) use ($capabilitymanager, $user, $discussion) { return $capabilitymanager->can_view_post($user, $discussion, $post); }); $forumid = $forum->get_id(); $courseid = $forum->get_course_record()->id; $postsexporter = $this->exporterfactory->get_posts_exporter( $user, $forum, $discussion, $groupedposts, $authorsbyid, $authorcontextids, $attachmentsbypostid, $groupsbycourseandauthorid[$courseid], $readreceiptcollectionbyforumid[$forumid] ?? null, $tagsbypostid, $ratingbypostid, true, $inlineattachments ); ['posts' => $exportedgroupedposts] = (array) $postsexporter->export($this->renderer); $exportedposts = array_merge($exportedposts, $exportedgroupedposts); } if (count($forums) == 1 && count($discussions) == 1) { // All of the posts belong to a single discussion in a single forum so // the exported order will match the given $posts array. return $exportedposts; } else { // Since we grouped the posts by discussion and forum the ordering of the // exported posts may be different to the given $posts array so we should // sort it back into the correct order for the caller. return $this->sort_exported_posts($posts, $exportedposts); } } /** * Group the posts by which discussion they belong to in order for them to be processed * in chunks by the exporting. * * Returns a list of groups where each group has a forum, discussion, and list of posts. * E.g. * [ * [ * 'forum' => , * 'discussion' => , * 'posts' => [ * , * , * * ] * ] * ] * * @param forum_entity[] $forums A list of all forums that each of the $discussions belong to, indexed by id. * @param discussion_entity[] $discussions A list of all discussions that each of the $posts belong to, indexed by id. * @param post_entity[] $posts The list of posts to process. * @return array List of grouped posts. Each group has a discussion, forum, and posts. */ private function group_posts_by_discussion(array $forums, array $discussions, array $posts): array { return array_reduce($posts, function($carry, $post) use ($forums, $discussions) { $discussionid = $post->get_discussion_id(); if (!isset($discussions[$discussionid])) { throw new moodle_exception('Unable to find discussion with id ' . $discussionid); } if (isset($carry[$discussionid])) { $carry[$discussionid]['posts'][] = $post; } else { $discussion = $discussions[$discussionid]; $forumid = $discussion->get_forum_id(); if (!isset($forums[$forumid])) { throw new moodle_exception('Unable to find forum with id ' . $forumid); } $carry[$discussionid] = [ 'forum' => $forums[$forumid], 'discussion' => $discussions[$discussionid], 'posts' => [$post] ]; } return $carry; }, []); } /** * Load the list of authors for the given posts. * * The list of authors will be indexed by the author id. * * @param post_entity[] $posts The list of posts to process. * @return author_entity[] */ private function get_authors_for_posts(array $posts): array { $authorvault = $this->vaultfactory->get_author_vault(); return $authorvault->get_authors_for_posts($posts); } /** * Get the user context ids for each of the authors. * * @param int[] $authorids The list of author ids to fetch context ids for. * @return int[] Context ids indexed by author id */ private function get_author_context_ids(array $authorids): array { $authorvault = $this->vaultfactory->get_author_vault(); return $authorvault->get_context_ids_for_author_ids($authorids); } /** * Load the list of all inline attachments for the posts. The list of attachments will be * indexed by the post id. * * @param array $groupedposts List of posts grouped by discussions. * @return stored_file[] */ private function get_inline_attachments_for_posts(array $groupedposts): array { $inlineattachmentsbypostid = []; $postattachmentvault = $this->vaultfactory->get_post_attachment_vault(); $postsbyforum = array_reduce($groupedposts, function($carry, $grouping) { ['forum' => $forum, 'posts' => $posts] = $grouping; $forumid = $forum->get_id(); if (!isset($carry[$forumid])) { $carry[$forumid] = [ 'forum' => $forum, 'posts' => [] ]; } $carry[$forumid]['posts'] = array_merge($carry[$forumid]['posts'], $posts); return $carry; }, []); foreach ($postsbyforum as $grouping) { ['forum' => $forum, 'posts' => $posts] = $grouping; $inlineattachments = $postattachmentvault->get_inline_attachments_for_posts($forum->get_context(), $posts); // Have to loop in order to maintain the correct indexes since they are numeric. foreach ($inlineattachments as $postid => $attachment) { $inlineattachmentsbypostid[$postid] = $attachment; } } return $inlineattachmentsbypostid; } /** * Load the list of all attachments for the posts. The list of attachments will be * indexed by the post id. * * @param array $groupedposts List of posts grouped by discussions. * @return stored_file[] */ private function get_attachments_for_posts(array $groupedposts): array { $attachmentsbypostid = []; $postattachmentvault = $this->vaultfactory->get_post_attachment_vault(); $postsbyforum = array_reduce($groupedposts, function($carry, $grouping) { ['forum' => $forum, 'posts' => $posts] = $grouping; $forumid = $forum->get_id(); if (!isset($carry[$forumid])) { $carry[$forumid] = [ 'forum' => $forum, 'posts' => [] ]; } $carry[$forumid]['posts'] = array_merge($carry[$forumid]['posts'], $posts); return $carry; }, []); foreach ($postsbyforum as $grouping) { ['forum' => $forum, 'posts' => $posts] = $grouping; $attachments = $postattachmentvault->get_attachments_for_posts($forum->get_context(), $posts); // Have to loop in order to maintain the correct indexes since they are numeric. foreach ($attachments as $postid => $attachment) { $attachmentsbypostid[$postid] = $attachment; } } return $attachmentsbypostid; } /** * Get the groups for each author of the given posts. * * The results are grouped by course and then author id because the groups are * contextually related to the course, e.g. a single author can be part of two different * sets of groups in two different courses. * * @param array $groupedposts List of posts grouped by discussions. * @return array List of groups indexed by forum id and then author id. */ private function get_author_groups_from_posts(array $groupedposts): array { $groupsbyauthorid = []; $authoridsbycourseid = []; // Get the unique list of author ids for each course in the grouped // posts. Grouping by course is the largest grouping we can achieve. foreach ($groupedposts as $grouping) { ['forum' => $forum, 'posts' => $posts] = $grouping; $course = $forum->get_course_record(); $courseid = $course->id; if (!isset($authoridsbycourseid[$courseid])) { $coursemodule = $forum->get_course_module_record(); $authoridsbycourseid[$courseid] = [ 'groupingid' => $coursemodule->groupingid, 'authorids' => [] ]; } $authorids = array_map(function($post) { return $post->get_author_id(); }, $posts); foreach ($authorids as $authorid) { $authoridsbycourseid[$courseid]['authorids'][$authorid] = $authorid; } } // Load each set of groups per course. foreach ($authoridsbycourseid as $courseid => $values) { ['groupingid' => $groupingid, 'authorids' => $authorids] = $values; $authorgroups = groups_get_all_groups( $courseid, array_keys($authorids), $groupingid, 'g.*, gm.id, gm.groupid, gm.userid' ); if (!isset($groupsbyauthorid[$courseid])) { $groupsbyauthorid[$courseid] = []; } foreach ($authorgroups as $group) { // Clean up data returned from groups_get_all_groups. $userid = $group->userid; $groupid = $group->groupid; unset($group->userid); unset($group->groupid); $group->id = $groupid; if (!isset($groupsbyauthorid[$courseid][$userid])) { $groupsbyauthorid[$courseid][$userid] = []; } $groupsbyauthorid[$courseid][$userid][] = $group; } } return $groupsbyauthorid; } /** * Get the list of tags for each of the posts. The tags will be returned in an * array indexed by the post id. * * @param post_entity[] $posts The list of posts to load tags for. * @return array Sets of tags indexed by post id. */ private function get_tags_from_posts(array $posts): array { $postids = array_map(function($post) { return $post->get_id(); }, $posts); return core_tag_tag::get_items_tags('mod_forum', 'forum_posts', $postids); } /** * Get the list of ratings for each post. The ratings are returned in an array * indexed by the post id. * * @param stdClass $user The user viewing the ratings. * @param array $groupedposts List of posts grouped by discussions. * @return array Sets of ratings indexed by post id. */ private function get_ratings_from_posts(stdClass $user, array $groupedposts) { $ratingsbypostid = []; $postsdatamapper = $this->legacydatamapperfactory->get_post_data_mapper(); $postsbyforum = array_reduce($groupedposts, function($carry, $grouping) { ['forum' => $forum, 'posts' => $posts] = $grouping; $forumid = $forum->get_id(); if (!isset($carry[$forumid])) { $carry[$forumid] = [ 'forum' => $forum, 'posts' => [] ]; } $carry[$forumid]['posts'] = array_merge($carry[$forumid]['posts'], $posts); return $carry; }, []); foreach ($postsbyforum as $grouping) { ['forum' => $forum, 'posts' => $posts] = $grouping; if (!$forum->has_rating_aggregate()) { continue; } $items = $postsdatamapper->to_legacy_objects($posts); $ratingoptions = (object) [ 'context' => $forum->get_context(), 'component' => 'mod_forum', 'ratingarea' => 'post', 'items' => $items, 'aggregate' => $forum->get_rating_aggregate(), 'scaleid' => $forum->get_scale(), 'userid' => $user->id, 'assesstimestart' => $forum->get_assess_time_start(), 'assesstimefinish' => $forum->get_assess_time_finish() ]; $rm = $this->ratingmanager; $items = $rm->get_ratings($ratingoptions); foreach ($items as $item) { $ratingsbypostid[$item->id] = empty($item->rating) ? null : $item->rating; } } return $ratingsbypostid; } /** * Get the read receipt collections for the given viewing user and each forum. The * receipt collections will only be loaded for posts in forums that the user is tracking. * * The receipt collections are returned in an array indexed by the forum ids. * * @param stdClass $user The user viewing the posts. * @param array $groupedposts List of posts grouped by discussions. */ private function get_read_receipts_from_posts(stdClass $user, array $groupedposts) { $forumdatamapper = $this->legacydatamapperfactory->get_forum_data_mapper(); $trackedforums = []; $trackedpostids = []; foreach ($groupedposts as $group) { ['forum' => $forum, 'posts' => $posts] = $group; $forumid = $forum->get_id(); if (!isset($trackedforums[$forumid])) { $forumrecord = $forumdatamapper->to_legacy_object($forum); $trackedforums[$forumid] = forum_tp_is_tracked($forumrecord, $user); } if ($trackedforums[$forumid]) { foreach ($posts as $post) { $trackedpostids[] = $post->get_id(); } } } if (empty($trackedpostids)) { return []; } // We can just load a single receipt collection for all tracked posts. $receiptvault = $this->vaultfactory->get_post_read_receipt_collection_vault(); $readreceiptcollection = $receiptvault->get_from_user_id_and_post_ids($user->id, $trackedpostids); $receiptsbyforumid = []; // Assign the collection to all forums that are tracked. foreach ($trackedforums as $forumid => $tracked) { if ($tracked) { $receiptsbyforumid[$forumid] = $readreceiptcollection; } } return $receiptsbyforumid; } /** * Sort the list of exported posts back into the same order as the given posts. * The ordering of the exported posts can often deviate from the given posts due * to the process of exporting them so we need to sort them back into the order * that the calling code expected. * * @param post_entity[] $posts The posts in the expected order. * @param stdClass[] $exportedposts The list of exported posts in any order. * @return stdClass[] Sorted exported posts. */ private function sort_exported_posts(array $posts, array $exportedposts) { $postindexes = []; foreach (array_values($posts) as $index => $post) { $postindexes[$post->get_id()] = $index; } $sortedexportedposts = []; foreach ($exportedposts as $exportedpost) { $index = $postindexes[$exportedpost->id]; $sortedexportedposts[$index] = $exportedpost; } return $sortedexportedposts; } } PK-B\Lmz  *classes/local/data_mappers/legacy/post.phpnu[. /** * Post data mapper. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\data_mappers\legacy; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\post as post_entity; use stdClass; /** * Convert a post entity into an stdClass. * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class post { /** * Convert a list of post entities into stdClasses. * * @param post_entity[] $posts The posts to convert. * @return stdClass[] */ public function to_legacy_objects(array $posts): array { return array_map(function(post_entity $post) { return (object) [ 'id' => $post->get_id(), 'discussion' => $post->get_discussion_id(), 'parent' => $post->get_parent_id(), 'userid' => $post->get_author_id(), 'created' => $post->get_time_created(), 'modified' => $post->get_time_modified(), 'mailed' => $post->has_been_mailed(), 'subject' => $post->get_subject(), 'message' => $post->get_message(), 'messageformat' => $post->get_message_format(), 'messagetrust' => $post->is_message_trusted(), 'attachment' => $post->has_attachments(), 'totalscore' => $post->get_total_score(), 'mailnow' => $post->should_mail_now(), 'deleted' => $post->is_deleted(), 'privatereplyto' => $post->get_private_reply_recipient_id(), 'wordcount' => $post->get_wordcount(), 'charcount' => $post->get_charcount(), ]; }, $posts); } /** * Convert a post entity into an stdClass. * * @param post_entity $post The post to convert. * @return stdClass */ public function to_legacy_object(post_entity $post): stdClass { return $this->to_legacy_objects([$post])[0]; } } PK-B\EtSS+classes/local/data_mappers/legacy/forum.phpnu[. /** * Forum data mapper. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\data_mappers\legacy; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\forum as forum_entity; use stdClass; /** * Convert a forum entity into an stdClass. * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class forum { /** * Convert a list of forum entities into stdClasses. * * @param forum_entity[] $forums The forums to convert. * @return stdClass[] */ public function to_legacy_objects(array $forums): array { return array_map(function(forum_entity $forum) { return (object) [ 'id' => $forum->get_id(), 'course' => $forum->get_course_id(), 'type' => $forum->get_type(), 'name' => $forum->get_name(), 'intro' => $forum->get_intro(), 'introformat' => $forum->get_intro_format(), 'assessed' => $forum->get_rating_aggregate(), 'assesstimestart' => $forum->get_assess_time_start(), 'assesstimefinish' => $forum->get_assess_time_finish(), 'scale' => $forum->get_scale(), 'grade_forum' => $forum->get_grade_for_forum(), 'grade_forum_notify' => $forum->should_notify_students_default_when_grade_for_forum(), 'maxbytes' => $forum->get_max_bytes(), 'maxattachments' => $forum->get_max_attachments(), 'forcesubscribe' => $forum->get_subscription_mode(), 'trackingtype' => $forum->get_tracking_type(), 'rsstype' => $forum->get_rss_type(), 'rssarticles' => $forum->get_rss_articles(), 'timemodified' => $forum->get_time_modified(), 'warnafter' => $forum->get_warn_after(), 'blockafter' => $forum->get_block_after(), 'blockperiod' => $forum->get_block_period(), 'completiondiscussions' => $forum->get_completion_discussions(), 'completionreplies' => $forum->get_completion_replies(), 'completionposts' => $forum->get_completion_posts(), 'displaywordcount' => $forum->should_display_word_count(), 'lockdiscussionafter' => $forum->get_lock_discussions_after(), 'duedate' => $forum->get_due_date(), 'cutoffdate' => $forum->get_cutoff_date() ]; }, $forums); } /** * Convert a forum entity into an stdClass. * * @param forum_entity $forum The forum to convert. * @return stdClass */ public function to_legacy_object(forum_entity $forum): stdClass { return $this->to_legacy_objects([$forum])[0]; } } PK-B\"9 0classes/local/data_mappers/legacy/discussion.phpnu[. /** * Discussion data mapper. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\data_mappers\legacy; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\discussion as discussion_entity; use stdClass; /** * Convert a discussion entity into an stdClass. * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class discussion { /** * Convert a list of discussion entities into stdClasses. * * @param discussion_entity[] $authors The authors to convert. * @return stdClass[] */ public function to_legacy_objects(array $discussions): array { return array_map(function(discussion_entity $discussion) { return (object) [ 'id' => $discussion->get_id(), 'course' => $discussion->get_course_id(), 'forum' => $discussion->get_forum_id(), 'name' => $discussion->get_name(), 'firstpost' => $discussion->get_first_post_id(), 'userid' => $discussion->get_user_id(), 'groupid' => $discussion->get_group_id(), 'assessed' => $discussion->is_assessed(), 'timemodified' => $discussion->get_time_modified(), 'usermodified' => $discussion->get_user_modified(), 'timestart' => $discussion->get_time_start(), 'timeend' => $discussion->get_time_end(), 'pinned' => $discussion->is_pinned(), 'timelocked' => $discussion->get_locked() ]; }, $discussions); } /** * Convert a discussion entity into an stdClass. * * @param discussion_entity $discussion The discussion to convert. * @return stdClass */ public function to_legacy_object(discussion_entity $discussion): stdClass { return $this->to_legacy_objects([$discussion])[0]; } } PK-B\غ_=  ,classes/local/data_mappers/legacy/author.phpnu[. /** * Author data mapper. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\data_mappers\legacy; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\author as author_entity; use stdClass; /** * Convert an author entity into an stdClass. * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class author { /** * Convert a list of author entities into stdClasses. * * @param author_entity[] $authors The authors to convert. * @return stdClass[] */ public function to_legacy_objects(array $authors): array { return array_map(function(author_entity $author) { return (object) [ 'id' => $author->get_id(), 'picture' => $author->get_picture_item_id(), 'firstname' => $author->get_first_name(), 'lastname' => $author->get_last_name(), 'fullname' => $author->get_full_name(), 'email' => $author->get_email(), 'deleted' => $author->is_deleted(), 'middlename' => $author->get_middle_name(), 'firstnamephonetic' => $author->get_first_name_phonetic(), 'lastnamephonetic' => $author->get_last_name_phonetic(), 'alternatename' => $author->get_alternate_name(), 'imagealt' => $author->get_image_alt() ]; }, $authors); } /** * Convert an author entity into an stdClass. * * @param author_entity $author The author to convert. * @return stdClass */ public function to_legacy_object(author_entity $author): stdClass { return $this->to_legacy_objects([$author])[0]; } } PK-B\-.UUclasses/local/vaults/post.phpnu[. /** * Post vault class. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\vaults; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\forum as forum_entity; use mod_forum\local\entities\post as post_entity; use mod_forum\local\factories\entity as entity_factory; use stdClass; /** * Post vault class. * * This should be the only place that accessed the database. * * This class should not return any objects other than post_entity objects. The class * may contain some utility count methods which return integers. * * This uses the repository pattern. See: * https://designpatternsphp.readthedocs.io/en/latest/More/Repository/README.html * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class post extends db_table_vault { /** The table for this vault */ private const TABLE = 'forum_posts'; /** Alias for user id */ private const USER_ID_ALIAS = 'userpictureid'; /** Alias for user fields */ private const USER_ALIAS = 'userrecord'; /** * Get the table alias. * * @return string */ protected function get_table_alias(): string { return 'p'; } /** * Build the SQL to be used in get_records_sql. * * @param string|null $wheresql Where conditions for the SQL * @param string|null $sortsql Order by conditions for the SQL * @param int|null $userid The user ID * @return string */ protected function generate_get_records_sql(?string $wheresql = null, ?string $sortsql = null, ?int $userid = null): string { $table = self::TABLE; $alias = $this->get_table_alias(); $fields = $alias . '.*'; $tables = "{{$table}} {$alias}"; $selectsql = "SELECT {$fields} FROM {$tables}"; $selectsql .= $wheresql ? ' WHERE ' . $wheresql : ''; $selectsql .= $sortsql ? ' ORDER BY ' . $sortsql : ''; return $selectsql; } /** * Convert the DB records into post entities. * * @param array $results The DB records * @return post_entity[] */ protected function from_db_records(array $results) { $entityfactory = $this->get_entity_factory(); return array_map(function(array $result) use ($entityfactory) { ['record' => $record] = $result; return $entityfactory->get_post_from_stdclass($record); }, $results); } /** * Get the post ids for the given discussion. * * @param stdClass $user The user to check the unread count for * @param int $discussionid The discussion to load posts for * @param bool $canseeprivatereplies Whether this user can see all private replies or not * @param string $orderby Order the results * @return post_entity[] */ public function get_from_discussion_id( stdClass $user, int $discussionid, bool $canseeprivatereplies, string $orderby = 'created ASC, id ASC' ): array { return $this->get_from_discussion_ids($user, [$discussionid], $canseeprivatereplies, $orderby); } /** * Get the list of posts for the given discussions. * * @param stdClass $user The user to load posts for. * @param int[] $discussionids The list of discussion ids to load posts for * @param bool $canseeprivatereplies Whether this user can see all private replies or not * @param string $orderby Order the results * @return post_entity[] */ public function get_from_discussion_ids( stdClass $user, array $discussionids, bool $canseeprivatereplies, string $orderby = '' ): array { if (empty($discussionids)) { return []; } return $this->get_from_filters($user, ['discussionids' => $discussionids], $canseeprivatereplies, $orderby); } /** * The method returns posts based on a set of filters. * * @param stdClass $user Only used when restricting private replies * @param array $filters Export filters, valid filters are: * * 'discussionids' => array of discussion ids eg [1,2,3] * 'userids' => array of user ids eg [1,2,3] * 'from' => timestamp to filter posts from this date. * 'to' => timestamp to filter posts till this date. * * @param bool $canseeprivatereplies Whether this user can see all private replies or not * @param string $orderby Order the results * @return post_entity[] */ public function get_from_filters( stdClass $user, array $filters, bool $canseeprivatereplies, string $orderby = '' ): array { if (count($filters) == 0) { return []; } $wheresql = []; $params = []; $alias = $this->get_table_alias(); // Filter by discussion ids. if (!empty($filters['discussionids'])) { list($indiscussionssql, $indiscussionsparams) = $this->get_db()->get_in_or_equal($filters['discussionids'], SQL_PARAMS_NAMED); $wheresql[] = "{$alias}.discussion {$indiscussionssql}"; $params += $indiscussionsparams; } // Filter by user ids. if (!empty($filters['userids'])) { list($inuserssql, $inusersparams) = $this->get_db()->get_in_or_equal($filters['userids'], SQL_PARAMS_NAMED); $wheresql[] = "{$alias}.userid {$inuserssql}"; $params += $inusersparams; } // Filter posts by from and to dates. if (isset($filters['from'])) { $wheresql[] = "{$alias}.created >= :from"; $params['from'] = $filters['from']; } if (isset($filters['to'])) { $wheresql[] = "{$alias}.created < :to"; $params['to'] = $filters['to']; } // We need to build the WHERE here, because get_private_reply_sql returns the query with the AND clause. $wheresql = implode(' AND ', $wheresql); // Build private replies sql. [ 'where' => $privatewhere, 'params' => $privateparams, ] = $this->get_private_reply_sql($user, $canseeprivatereplies); $wheresql .= "{$privatewhere}"; $params += $privateparams; if ($orderby) { $orderbysql = $alias . '.' . $orderby; } else { $orderbysql = ''; } $sql = $this->generate_get_records_sql($wheresql, $orderbysql); $records = $this->get_db()->get_records_sql($sql, $params); return $this->transform_db_records_to_entities($records); } /** * Load a list of replies to the given post. This will load all descendants of the post. * That is, all direct replies and replies to those replies etc. * * The return value will be a flat array of posts in the requested order. * * @param stdClass $user The user to check the unread count for * @param post_entity $post The post to load replies for * @param bool $canseeprivatereplies Whether this user can see all private replies or not * @param string $orderby How to order the replies * @return post_entity[] */ public function get_replies_to_post( stdClass $user, post_entity $post, bool $canseeprivatereplies, string $orderby = 'created ASC, id ASC' ): array { $alias = $this->get_table_alias(); [ 'where' => $privatewhere, 'params' => $privateparams, ] = $this->get_private_reply_sql($user, $canseeprivatereplies); $params = array_merge([ 'discussionid' => $post->get_discussion_id(), 'created' => $post->get_time_created(), 'excludepostid' => $post->get_id(), ], $privateparams); // Unfortunately the best we can do to filter down the query is ignore all posts // that were created before the given post (since they can't be replies). // We also filter to remove private replies if the user cannot vie them. $wheresql = "{$alias}.discussion = :discussionid AND {$alias}.created >= :created {$privatewhere} AND {$alias}.id != :excludepostid"; $orderbysql = $alias . '.' . $orderby; $sql = $this->generate_get_records_sql($wheresql, $orderbysql); $records = $this->get_db()->get_records_sql($sql, $params); $posts = $this->transform_db_records_to_entities($records); $sorter = $this->get_entity_factory()->get_posts_sorter(); // We need to sort all of the values into the replies tree in order to capture // the full list of descendants. $sortedposts = $sorter->sort_into_children($posts); $replies = []; // From the sorted list we can grab the first elements and check if they are replies // to the post we care about. If so we keep them. foreach ($sortedposts as $candidate) { [$candidatepost, $candidatereplies] = $candidate; if ($candidatepost->has_parent() && $candidatepost->get_parent_id() == $post->get_id()) { $replies[] = $candidate; } } if (empty($replies)) { return $replies; } $getreplypostids = function($candidates) use (&$getreplypostids) { $ids = []; foreach ($candidates as $candidate) { [$reply, $replies] = $candidate; $ids = array_merge($ids, [$reply->get_id()], $getreplypostids($replies)); } return $ids; }; // Recursively build a list of the ids of all posts in the full reply tree. $replypostids = $getreplypostids($replies); // Now go back and filter the original result set down to just the posts that // we've flagged as in the reply tree. We need to filter the original set of values // so that we can maintain the requested sort order. return array_values(array_filter($posts, function($post) use ($replypostids) { return in_array($post->get_id(), $replypostids); })); } /** * Get a mapping of replies to the specified discussions. * * @param stdClass $user The user to check the unread count for * @param int[] $discussionids The list of discussions to fetch counts for * @param bool $canseeprivatereplies Whether this user can see all private replies or not * @return int[] The number of replies for each discussion returned in an associative array */ public function get_reply_count_for_discussion_ids(stdClass $user, array $discussionids, bool $canseeprivatereplies): array { if (empty($discussionids)) { return []; } list($insql, $params) = $this->get_db()->get_in_or_equal($discussionids, SQL_PARAMS_NAMED); [ 'where' => $privatewhere, 'params' => $privateparams, ] = $this->get_private_reply_sql($user, $canseeprivatereplies); $sql = "SELECT discussion, COUNT(1) FROM {" . self::TABLE . "} p WHERE p.discussion {$insql} AND p.parent > 0 {$privatewhere} GROUP BY discussion"; return $this->get_db()->get_records_sql_menu($sql, array_merge($params, $privateparams)); } /** * Get a mapping of replies to the specified discussions. * * @param stdClass $user The user to check the unread count for * @param int $postid The post to collect replies to * @param int $discussionid The list of discussions to fetch counts for * @param bool $canseeprivatereplies Whether this user can see all private replies or not * @return int The number of replies for each discussion returned in an associative array */ public function get_reply_count_for_post_id_in_discussion_id( stdClass $user, int $postid, int $discussionid, bool $canseeprivatereplies): int { [ 'where' => $privatewhere, 'params' => $privateparams, ] = $this->get_private_reply_sql($user, $canseeprivatereplies); $alias = $this->get_table_alias(); $table = self::TABLE; $sql = "SELECT {$alias}.id, {$alias}.parent FROM {{$table}} {$alias} WHERE p.discussion = :discussionid {$privatewhere}"; $postparents = $this->get_db()->get_records_sql_menu($sql, array_merge([ 'discussionid' => $discussionid, ], $privateparams)); return $this->count_children_from_parent_recursively($postparents, $postid); } /** * Count the children whose parent matches the current record recursively. * * @param array $postparents The full mapping of posts. * @param int $postid The ID to check for * @return int $count */ private function count_children_from_parent_recursively(array $postparents, int $postid): int { if (!isset($postparents[$postid])) { // Post not found at all. return 0; } $count = 0; foreach ($postparents as $pid => $parentid) { if ($postid == $parentid) { $count += $this->count_children_from_parent_recursively($postparents, $pid) + 1; } } return $count; } /** * Get a mapping of unread post counts for the specified discussions. * * @param stdClass $user The user to fetch counts for * @param int[] $discussionids The list of discussions to fetch counts for * @param bool $canseeprivatereplies Whether this user can see all private replies or not * @return int[] The count of unread posts for each discussion returned in an associative array */ public function get_unread_count_for_discussion_ids(stdClass $user, array $discussionids, bool $canseeprivatereplies): array { global $CFG; if (empty($discussionids)) { return []; } [ 'where' => $privatewhere, 'params' => $privateparams, ] = $this->get_private_reply_sql($user, $canseeprivatereplies); $alias = $this->get_table_alias(); list($insql, $params) = $this->get_db()->get_in_or_equal($discussionids, SQL_PARAMS_NAMED); $sql = "SELECT p.discussion, COUNT(p.id) FROM {" . self::TABLE . "} p LEFT JOIN {forum_read} r ON r.postid = p.id AND r.userid = :userid WHERE p.discussion {$insql} AND p.modified > :cutofftime AND r.id IS NULL {$privatewhere} GROUP BY p.discussion"; $params['userid'] = $user->id; $params['cutofftime'] = floor((new \DateTime()) ->sub(new \DateInterval("P{$CFG->forum_oldpostdays}D")) ->format('U') / 60) * 60; return $this->get_db()->get_records_sql_menu($sql, array_merge($params, $privateparams)); } /** * Get a mapping of the most recent post record in each discussion based on post creation time. * * @param stdClass $user * @param array $discussionids * @param bool $canseeprivatereplies * @return array * @throws \coding_exception * @throws \dml_exception */ public function get_latest_posts_for_discussion_ids( stdClass $user, array $discussionids, bool $canseeprivatereplies): array { if (empty($discussionids)) { return []; } list($insql, $params) = $this->get_db()->get_in_or_equal($discussionids, SQL_PARAMS_NAMED); [ 'where' => $privatewhere, 'params' => $privateparams, ] = $this->get_private_reply_sql($user, $canseeprivatereplies, "mp"); [ 'where' => $privatewhere2, 'params' => $privateparams2, ] = $this->get_private_reply_sql($user, $canseeprivatereplies, "p"); $sql = " SELECT posts.* FROM {" . self::TABLE . "} posts JOIN ( SELECT p.discussion, MAX(p.id) as latestpostid FROM {" . self::TABLE . "} p JOIN ( SELECT mp.discussion, MAX(mp.created) AS created FROM {" . self::TABLE . "} mp WHERE mp.discussion {$insql} {$privatewhere} GROUP BY mp.discussion ) lp ON lp.discussion = p.discussion AND lp.created = p.created WHERE 1=1 {$privatewhere2} GROUP BY p.discussion ) plp on plp.discussion = posts.discussion AND plp.latestpostid = posts.id"; $records = $this->get_db()->get_records_sql($sql, array_merge($params, $privateparams, $privateparams2)); $entities = $this->transform_db_records_to_entities($records); return array_reduce($entities, function($carry, $entity) { $carry[$entity->get_discussion_id()] = $entity; return $carry; }, []); } /** * Get the SQL where and additional parameters to use to restrict posts to private reply posts. * * @param stdClass $user The user to fetch counts for * @param bool $canseeprivatereplies Whether this user can see all private replies or not * @return array The SQL WHERE clause, and parameters to use in the SQL. */ private function get_private_reply_sql(stdClass $user, bool $canseeprivatereplies, $posttablealias = "p") { $params = []; $privatewhere = ''; if (!$canseeprivatereplies) { $privatewhere = " AND ({$posttablealias}.privatereplyto = :{$posttablealias}_privatereplyto OR " . "{$posttablealias}.userid = :{$posttablealias}_privatereplyfrom OR {$posttablealias}.privatereplyto = 0)"; $params[$posttablealias . '_privatereplyto'] = $user->id; $params[$posttablealias . '_privatereplyfrom'] = $user->id; } return [ 'where' => $privatewhere, 'params' => $params, ]; } /** * Get a mapping of the first post in each discussion based on post creation time. * * @param int[] $discussionids The list of discussions to fetch counts for * @return post_entity[] The post object of the first post for each discussions returned in an associative array */ public function get_first_post_for_discussion_ids(array $discussionids): array { if (empty($discussionids)) { return []; } list($insql, $params) = $this->get_db()->get_in_or_equal($discussionids, SQL_PARAMS_NAMED); $sql = " SELECT posts.* FROM {" . self::TABLE . "} posts JOIN ( SELECT p.discussion, MIN(p.id) as firstpostid FROM {" . self::TABLE . "} p JOIN ( SELECT mp.discussion, MIN(mp.created) AS created FROM {" . self::TABLE . "} mp WHERE mp.discussion {$insql} GROUP BY mp.discussion ) lp ON lp.discussion = p.discussion AND lp.created = p.created GROUP BY p.discussion ) plp on plp.discussion = posts.discussion AND plp.firstpostid = posts.id"; $records = $this->get_db()->get_records_sql($sql, $params); return $this->transform_db_records_to_entities($records); } /** * Get the posts for the given user. * * @param int $discussionid The discussion to fetch posts for * @param int $userid The user to fetch posts for * @param bool $canseeprivatereplies Whether this user can see all private replies or not * @param string $orderby Order the results * @return post_entity[] */ public function get_posts_in_discussion_for_user_id( int $discussionid, int $userid, bool $canseeprivatereplies, string $orderby = 'created ASC, id ASC' ): array { $user = $this->get_db()->get_record('user', ['id' => (int)$userid], '*', IGNORE_MISSING); $alias = $this->get_table_alias(); [ 'where' => $privatewhere, 'params' => $privateparams, ] = $this->get_private_reply_sql($user, $canseeprivatereplies); $wheresql = "{$alias}.userid = :authorid AND {$alias}.discussion = :discussionid {$privatewhere}"; $orderbysql = $alias . '.' . $orderby; $sql = $this->generate_get_records_sql($wheresql, $orderbysql); $records = $this->get_db()->get_records_sql($sql, array_merge([ 'authorid' => $userid, 'discussionid' => $discussionid ], $privateparams)); return $this->transform_db_records_to_entities($records); } } PK-B\W(classes/local/vaults/post_attachment.phpnu[. /** * Post attachment vault class. * * @package mod_forum * @copyright 2018 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\vaults; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\post as post_entity; use context; use file_storage; /** * Post attachment vault class. * * This should be the only place that accessed the database. * * This uses the repository pattern. See: * https://designpatternsphp.readthedocs.io/en/latest/More/Repository/README.html * * @copyright 2018 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class post_attachment { /** The component for attachments */ private const COMPONENT = 'mod_forum'; /** Sort the attachments by filename */ private const SORT = 'filename'; /** Don't include directories */ private const INCLUDE_DIRECTORIES = false; /** @var file_storage $filestorage File storage */ private $filestorage; /** * Construct. * * @param file_storage $filestorage File storage */ public function __construct(file_storage $filestorage) { $this->filestorage = $filestorage; } /** * Get the attachments for the given posts. The results are indexed by * post id. * * @param context $context The (forum) context that the posts are in * @param post_entity[] $posts The list of posts to load attachments for * @param string $area The file storage area, can be 'attachment' or 'post' for inline attachments. * @return array Post attachments indexed by post id */ private function get_area_attachments_for_posts(context $context, array $posts, string $area) { $itemids = array_map(function($post) { return $post->get_id(); }, $posts); $files = $this->filestorage->get_area_files( $context->id, self::COMPONENT, $area, $itemids, self::SORT, self::INCLUDE_DIRECTORIES ); $filesbyid = array_reduce($posts, function($carry, $post) { $carry[$post->get_id()] = []; return $carry; }, []); return array_reduce($files, function($carry, $file) { $itemid = $file->get_itemid(); $carry[$itemid] = array_merge($carry[$itemid], [$file]); return $carry; }, $filesbyid); } /** * Get attachment for posts. * * @param context $context The (forum) context that the posts are in * @param post_entity[] $posts The list of posts to load attachments for * @return array Post attachments indexed by post id */ public function get_attachments_for_posts(context $context, array $posts) { return $this->get_area_attachments_for_posts($context, $posts, 'attachment'); } /** * Get inline attachments for posts. * * @param context $context The (forum) context that the posts are in * @param post_entity[] $posts The list of posts to load attachments for * @return array Post attachments indexed by post id */ public function get_inline_attachments_for_posts(context $context, array $posts) { return $this->get_area_attachments_for_posts($context, $posts, 'post'); } } PK-B\&`classes/local/vaults/forum.phpnu[. /** * Forum vault class. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\vaults; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\forum as forum_entity; use mod_forum\local\vaults\preprocessors\extract_context as extract_context_preprocessor; use mod_forum\local\vaults\preprocessors\extract_record as extract_record_preprocessor; use core\dml\table as dml_table; use context_helper; /** * Forum vault class. * * This should be the only place that accessed the database. * * This uses the repository pattern. See: * https://designpatternsphp.readthedocs.io/en/latest/More/Repository/README.html * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class forum extends db_table_vault { /** The table for this vault */ private const TABLE = 'forum'; /** * Get the table alias. * * @return string */ protected function get_table_alias(): string { return 'f'; } /** * Build the SQL to be used in get_records_sql. * * @param string|null $wheresql Where conditions for the SQL * @param string|null $sortsql Order by conditions for the SQL * @param int|null $userid The user ID * @return string */ protected function generate_get_records_sql(?string $wheresql = null, ?string $sortsql = null, ?int $userid = null): string { $db = $this->get_db(); $alias = $this->get_table_alias(); $thistable = new dml_table(self::TABLE, $alias, $alias); $cmtable = new dml_table('course_modules', 'cm', 'cm_'); $coursetable = new dml_table('course', 'c', 'c_'); $fields = implode(', ', [ $thistable->get_field_select(), context_helper::get_preload_record_columns_sql('ctx'), $cmtable->get_field_select(), $coursetable->get_field_select(), ]); $tables = $thistable->get_from_sql(); $tables .= " JOIN {modules} m ON m.name = 'forum'"; $tables .= " JOIN " . $cmtable->get_from_sql() . " ON cm.module = m.id AND cm.instance = {$alias}.id"; $tables .= ' JOIN {context} ctx ON ctx.contextlevel = ' . CONTEXT_MODULE . ' AND ctx.instanceid = cm.id'; $tables .= " JOIN " . $coursetable->get_from_sql() . " ON c.id = {$alias}.course"; $selectsql = 'SELECT ' . $fields . ' FROM ' . $tables; $selectsql .= $wheresql ? ' WHERE ' . $wheresql : ''; $selectsql .= $sortsql ? ' ORDER BY ' . $sortsql : ''; return $selectsql; } /** * Get a list of preprocessors to execute on the DB results before being converted * into entities. * * @return array */ protected function get_preprocessors(): array { return array_merge( parent::get_preprocessors(), [ 'forum' => new extract_record_preprocessor(self::TABLE, $this->get_table_alias()), 'course_module' => new extract_record_preprocessor('course_modules', 'cm_'), 'course' => new extract_record_preprocessor('course', 'c_'), 'context' => new extract_context_preprocessor(), ] ); } /** * Convert the DB records into forum entities. * * @param array $results The DB records * @return forum_entity[] */ protected function from_db_records(array $results): array { $entityfactory = $this->get_entity_factory(); return array_map(function(array $result) use ($entityfactory) { [ 'forum' => $forumrecord, 'course_module' => $coursemodule, 'course' => $course, 'context' => $context, ] = $result; return $entityfactory->get_forum_from_stdclass($forumrecord, $context, $coursemodule, $course); }, $results); } /** * Get the forum for the given course module id. * * @param int $id The course module id * @return forum_entity|null */ public function get_from_course_module_id(int $id): ?forum_entity { $records = $this->get_from_course_module_ids([$id]); return count($records) ? array_shift($records) : null; } /** * Get the forums for the given course module ids * * @param int[] $ids The course module ids * @return forum_entity[] */ public function get_from_course_module_ids(array $ids): array { $alias = $this->get_table_alias(); list($insql, $params) = $this->get_db()->get_in_or_equal($ids); $wheresql = 'cm.id ' . $insql; $sql = $this->generate_get_records_sql($wheresql); $records = $this->get_db()->get_records_sql($sql, $params); return $this->transform_db_records_to_entities($records); } /** * Get the forum entity for the given post id. * * @param int $id The course module id * @return forum_entity|null */ public function get_from_post_id(int $id): ?forum_entity { $alias = $this->get_table_alias(); $thistable = new dml_table(self::TABLE, $alias, $alias); $coursemoduletable = new dml_table('course_modules', 'cm', 'cm_'); $coursetable = new dml_table('course', 'c', 'c_'); $tablefields = $thistable->get_field_select(); $coursemodulefields = $coursemoduletable->get_field_select(); $coursefields = $coursetable->get_field_select(); $fields = implode(', ', [ $tablefields, context_helper::get_preload_record_columns_sql('ctx'), $coursemodulefields, $coursefields, ]); $tables = "{forum_posts} p"; $tables .= " JOIN {forum_discussions} d ON d.id = p.discussion"; $tables .= ' JOIN {' . self::TABLE . "} {$alias} ON {$alias}.id = d.forum"; $tables .= " JOIN {modules} m ON m.name = 'forum'"; $tables .= " JOIN {course_modules} cm ON cm.module = m.id AND cm.instance = {$alias}.id"; $tables .= ' JOIN {context} ctx ON ctx.contextlevel = ' . CONTEXT_MODULE . ' AND ctx.instanceid = cm.id'; $tables .= " JOIN {course} c ON c.id = {$alias}.course"; $sql = "SELECT {$fields} FROM {$tables} WHERE p.id = :postid"; $records = $this->get_db()->get_records_sql($sql, [ 'postid' => $id, ]); $records = $this->transform_db_records_to_entities($records); return count($records) ? array_shift($records) : null; } } PK-B\SI#555classes/local/vaults/post_read_receipt_collection.phpnu[. /** * Post read receipt collection class. * * @package mod_forum * @copyright 2018 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\vaults; defined('MOODLE_INTERNAL') || die(); use stdClass; /** * Post read receipt collection class. * * This should be the only place that accessed the database. * * This uses the repository pattern. See: * https://designpatternsphp.readthedocs.io/en/latest/More/Repository/README.html * * @copyright 2018 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class post_read_receipt_collection extends db_table_vault { /** The table for this vault */ private const TABLE = 'forum_read'; /** * Get the table alias. * * @return string */ protected function get_table_alias(): string { return 'fr'; } /** * Build the SQL to be used in get_records_sql. * * @param string|null $wheresql Where conditions for the SQL * @param string|null $sortsql Order by conditions for the SQL * @param int|null $userid The user ID * @return string */ protected function generate_get_records_sql(?string $wheresql = null, ?string $sortsql = null, ?int $userid = null): string { $selectsql = 'SELECT * FROM {' . self::TABLE . '} ' . $this->get_table_alias(); $selectsql .= $wheresql ? ' WHERE ' . $wheresql : ''; $selectsql .= $sortsql ? ' ORDER BY ' . $sortsql : ''; return $selectsql; } /** * Convert the DB records into post_read_receipt_collection entities. * * @param array $results The DB records * @return post_read_receipt_collection */ protected function from_db_records(array $results) { $entityfactory = $this->get_entity_factory(); $records = array_map(function($result) { return $result['record']; }, $results); return $entityfactory->get_post_read_receipt_collection_from_stdclasses($records); } /** * Load the post_read_receipt_collection for the given user and set * of posts. * * @param int $userid Id of the user to load receipts for * @param int[] $postids List of post ids to load receipts for * @return post_read_receipt_collection */ public function get_from_user_id_and_post_ids(int $userid, array $postids) { $alias = $this->get_table_alias(); [$postidinsql, $params] = $this->get_db()->get_in_or_equal($postids); $params[] = $userid; $wheresql = "{$alias}.postid {$postidinsql}"; $wheresql .= " AND {$alias}.userid = ?"; $sql = $this->generate_get_records_sql($wheresql); $records = $this->get_db()->get_records_sql($sql, $params); return $this->transform_db_records_to_entities($records); } /** * Load the post_read_receipt_collection for the given user and set * of posts. * * @param stdClass $user The user to load receipts for * @param post_entity[] $posts List of posts to load receipts for * @return post_read_receipt_collection */ public function get_from_user_and_posts(stdClass $user, array $posts) { $postids = array_map(function($post) { return $post->get_id(); }, $posts); return $this->get_from_user_id_and_post_ids($user->id, $postids); } } PK-B\H V V(classes/local/vaults/discussion_list.phpnu[. /** * Vault class for a discussion list. * * @package mod_forum * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\vaults; defined('MOODLE_INTERNAL') || die(); use core_group\output\group_details; use mod_forum\local\vaults\preprocessors\extract_record as extract_record_preprocessor; use mod_forum\local\vaults\preprocessors\extract_user as extract_user_preprocessor; use mod_forum\local\renderers\discussion_list as discussion_list_renderer; use core\dml\table as dml_table; use stdClass; /** * Discussion list vault. * * This should be the only place that accessed the database. * * This uses the repository pattern. See: * https://designpatternsphp.readthedocs.io/en/latest/More/Repository/README.html * * @package mod_forum * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class discussion_list extends db_table_vault { /** The table for this vault */ private const TABLE = 'forum_discussions'; /** Alias for first author id */ private const FIRST_AUTHOR_ID_ALIAS = 'fauserpictureid'; /** Alias for author fields */ private const FIRST_AUTHOR_ALIAS = 'fauserrecord'; /** Alias for last author id */ private const LATEST_AUTHOR_ID_ALIAS = 'lauserpictureid'; /** Alias for last author fields */ private const LATEST_AUTHOR_ALIAS = 'lauserrecord'; /** Default limit */ public const PAGESIZE_DEFAULT = 100; /** Sort by newest first */ public const SORTORDER_LASTPOST_DESC = 1; /** Sort by oldest first */ public const SORTORDER_LASTPOST_ASC = 2; /** Sort by created desc */ public const SORTORDER_CREATED_DESC = 3; /** Sort by created asc */ public const SORTORDER_CREATED_ASC = 4; /** Sort by number of replies desc */ public const SORTORDER_REPLIES_DESC = 5; /** Sort by number of replies desc */ public const SORTORDER_REPLIES_ASC = 6; /** Sort by discussion name desc */ public const SORTORDER_DISCUSSION_DESC = 7; /** Sort by discussion name asc */ public const SORTORDER_DISCUSSION_ASC = 8; /** Sort by discussion starter's name desc */ public const SORTORDER_STARTER_DESC = 9; /** Sort by discussion starter's name asc */ public const SORTORDER_STARTER_ASC = 10; /** Sort by group name desc */ public const SORTORDER_GROUP_DESC = 11; /** Sort by group name asc */ public const SORTORDER_GROUP_ASC = 12; /** * Get the table alias. * * @return string */ protected function get_table_alias(): string { return 'd'; } /** * Get the favourite table alias * * @return string */ protected function get_favourite_alias(): string { return 'favalias'; } /** * Build the SQL to be used in get_records_sql. * * @param string|null $wheresql Where conditions for the SQL * @param string|null $sortsql Order by conditions for the SQL * @param int|null $userid The ID of the user we are performing this query for * * @return string */ protected function generate_get_records_sql(?string $wheresql = null, ?string $sortsql = null, ?int $userid = null): string { $alias = $this->get_table_alias(); $includefavourites = $userid ? true : false; $favsql = ''; if ($includefavourites) { list($favsql, $favparams) = $this->get_favourite_sql($userid); foreach ($favparams as $key => $param) { $favsql = str_replace(":$key", "'$param'", $favsql); } } // Fetch: // - Discussion // - First post // - Author // - Most recent editor. $thistable = new dml_table(self::TABLE, $alias, $alias); $posttable = new dml_table('forum_posts', 'fp', 'p_'); $userfieldsapi = \core_user\fields::for_userpic()->including('deleted'); $firstauthorfields = $userfieldsapi->get_sql('fa', false, self::FIRST_AUTHOR_ALIAS, self::FIRST_AUTHOR_ID_ALIAS, false)->selects; $latestuserfields = $userfieldsapi->get_sql('la', false, self::LATEST_AUTHOR_ALIAS, self::LATEST_AUTHOR_ID_ALIAS, false)->selects; $fields = implode(', ', [ $thistable->get_field_select(), $posttable->get_field_select(), $firstauthorfields, $latestuserfields, ]); $sortkeys = [ $this->get_sort_order(self::SORTORDER_REPLIES_DESC, $includefavourites), $this->get_sort_order(self::SORTORDER_REPLIES_ASC, $includefavourites) ]; $issortbyreplies = in_array($sortsql, $sortkeys); $tables = $thistable->get_from_sql(); $tables .= ' JOIN ' . $posttable->get_from_sql() . ' ON fp.id = ' . $alias . '.firstpost'; $tables .= ' JOIN {user} fa ON fa.id = fp.userid'; $tables .= ' JOIN {user} la ON la.id = ' . $alias . '.usermodified'; $tables .= $favsql; if ($issortbyreplies) { // Join the discussion replies. $tables .= ' JOIN ( SELECT rd.id, COUNT(rp.id) as replycount FROM {forum_discussions} rd LEFT JOIN {forum_posts} rp ON rp.discussion = rd.id AND rp.id != rd.firstpost GROUP BY rd.id ) r ON d.id = r.id'; } $groupsortorders = [ $this->get_sort_order(self::SORTORDER_GROUP_DESC, $includefavourites), $this->get_sort_order(self::SORTORDER_GROUP_ASC, $includefavourites) ]; $sortbygroup = in_array($sortsql, $groupsortorders); if ($sortbygroup) { $groupstable = new dml_table('groups', 'g', 'g'); $fields .= ', ' . $groupstable->get_field_select(); // Join groups. $tables .= 'LEFT JOIN {groups} g ON g.id = d.groupid'; } $selectsql = 'SELECT ' . $fields . ' FROM ' . $tables; $selectsql .= $wheresql ? ' WHERE ' . $wheresql : ''; $selectsql .= $sortsql ? ' ORDER BY ' . $sortsql : ''; return $selectsql; } /** * Build the SQL to be used in count_records_sql. * * @param string|null $wheresql Where conditions for the SQL * @return string */ protected function generate_count_records_sql(?string $wheresql = null): string { $alias = $this->get_table_alias(); $db = $this->get_db(); $selectsql = "SELECT COUNT(1) FROM {" . self::TABLE . "} {$alias}"; $selectsql .= $wheresql ? ' WHERE ' . $wheresql : ''; return $selectsql; } /** * Get a list of preprocessors to execute on the DB results before being converted * into entities. * * @return array */ protected function get_preprocessors(): array { return array_merge( parent::get_preprocessors(), [ 'discussion' => new extract_record_preprocessor(self::TABLE, $this->get_table_alias()), 'firstpost' => new extract_record_preprocessor('forum_posts', 'p_'), 'firstpostauthor' => new extract_user_preprocessor(self::FIRST_AUTHOR_ID_ALIAS, self::FIRST_AUTHOR_ALIAS), 'latestpostauthor' => new extract_user_preprocessor(self::LATEST_AUTHOR_ID_ALIAS, self::LATEST_AUTHOR_ALIAS), ] ); } /** * Convert the DB records into discussion list entities. * * @param array $results The DB records * @return discussion_list[] */ protected function from_db_records(array $results) { $entityfactory = $this->get_entity_factory(); return array_map(function(array $result) use ($entityfactory) { [ 'discussion' => $discussion, 'firstpost' => $firstpost, 'firstpostauthor' => $firstpostauthor, 'latestpostauthor' => $latestpostauthor, ] = $result; return $entityfactory->get_discussion_summary_from_stdclass( $discussion, $firstpost, $firstpostauthor, $latestpostauthor ); }, $results); } /** * Get the field to sort by. * * @param int|null $sortmethod * @return string */ protected function get_keyfield(?int $sortmethod): string { global $CFG; switch ($sortmethod) { case self::SORTORDER_CREATED_DESC: case self::SORTORDER_CREATED_ASC: return 'fp.created'; case self::SORTORDER_REPLIES_DESC: case self::SORTORDER_REPLIES_ASC: return 'replycount'; case self::SORTORDER_DISCUSSION_DESC: case self::SORTORDER_DISCUSSION_ASC: return 'dname'; case self::SORTORDER_STARTER_DESC: case self::SORTORDER_STARTER_ASC: // We'll sort by the first name field of the discussion starter's name. // Let's get the full name display config first. $nameformat = $CFG->fullnamedisplay; if ($CFG->fullnamedisplay === 'language') { $nameformat = get_string('fullnamedisplay', '', (object)['firstname' => 'firstname', 'lastname' => 'lastname']); } // Fetch all the available user name fields. $availablefields = order_in_string(\core_user\fields::get_name_fields(), $nameformat); // We'll default to the first name if there's no available name field. $returnfield = 'firstname'; if (!empty($availablefields)) { // Use the first name field. $returnfield = reset($availablefields); } return 'fauserrecord' . $returnfield; case self::SORTORDER_GROUP_DESC: case self::SORTORDER_GROUP_ASC: return 'gname'; default: global $CFG; $alias = $this->get_table_alias(); $field = "{$alias}.timemodified"; if (!empty($CFG->forum_enabletimedposts)) { return "CASE WHEN {$field} < {$alias}.timestart THEN {$alias}.timestart ELSE {$field} END"; } return $field; } } /** * Get the sort direction. * * @param int|null $sortmethod * @return string */ protected function get_sort_direction(?int $sortmethod): string { switch ($sortmethod) { case self::SORTORDER_LASTPOST_ASC: case self::SORTORDER_CREATED_ASC: case self::SORTORDER_REPLIES_ASC: case self::SORTORDER_DISCUSSION_ASC: case self::SORTORDER_STARTER_ASC: case self::SORTORDER_GROUP_ASC: return "ASC"; case self::SORTORDER_LASTPOST_DESC: case self::SORTORDER_CREATED_DESC: case self::SORTORDER_REPLIES_DESC: case self::SORTORDER_DISCUSSION_DESC: case self::SORTORDER_STARTER_DESC: case self::SORTORDER_GROUP_DESC: default: return "DESC"; } } /** * Get the sort order SQL for a sort method. * * @param int|null $sortmethod * @param bool|null $includefavourites * @return string */ private function get_sort_order(?int $sortmethod, bool $includefavourites = true): string { $alias = $this->get_table_alias(); // TODO consider user favourites... $keyfield = $this->get_keyfield($sortmethod); $direction = $this->get_sort_direction($sortmethod); $favouritesort = ''; if ($includefavourites) { $favalias = $this->get_favourite_alias(); // Since we're joining on the favourite table any discussion that isn't favourited will have // null in the favourite columns. Nulls behave differently in the sorting for different databases. // We can ensure consistency between databases by explicitly deprioritising any null favourite field // using a case statement. $favouritesort = ", CASE WHEN {$favalias}.id IS NULL THEN 0 ELSE 1 END DESC"; // After the null favourite fields are deprioritised and appear below the favourited discussions we // need to order the favourited discussions by id so that the most recently favourited discussions // appear at the top of the list. $favouritesort .= ", {$favalias}.itemtype DESC"; } return "{$alias}.pinned DESC $favouritesort , {$keyfield} {$direction}, {$alias}.id {$direction}"; } /** * Fetch any required SQL to respect timed posts. * * @param bool $includehiddendiscussions Whether to include hidden discussions or not * @param int|null $includepostsforuser Which user to include posts for, if any * @return array The SQL and parameters to include */ protected function get_hidden_post_sql(bool $includehiddendiscussions, ?int $includepostsforuser) { $wheresql = ''; $params = []; if (!$includehiddendiscussions) { $now = time(); $wheresql = " AND ((d.timestart <= :timestart AND (d.timeend = 0 OR d.timeend > :timeend))"; $params['timestart'] = $now; $params['timeend'] = $now; if (null !== $includepostsforuser) { $wheresql .= " OR d.userid = :byuser"; $params['byuser'] = $includepostsforuser; } $wheresql .= ")"; } return [ 'wheresql' => $wheresql, 'params' => $params, ]; } /** * Get each discussion, first post, first and last post author for the given forum, considering timed posts, and * pagination. * * @param int $forumid The forum to fetch the discussion set for * @param bool $includehiddendiscussions Whether to include hidden discussions or not * @param int|null $includepostsforuser Which user to include posts for, if any * @param int $sortorder The sort order to use * @param int $limit The number of discussions to fetch * @param int $offset The record offset * @return array The set of data fetched */ public function get_from_forum_id( int $forumid, bool $includehiddendiscussions, ?int $includepostsforuser, ?int $sortorder, int $limit, int $offset ) { $alias = $this->get_table_alias(); $wheresql = "{$alias}.forum = :forumid"; [ 'wheresql' => $hiddensql, 'params' => $hiddenparams ] = $this->get_hidden_post_sql($includehiddendiscussions, $includepostsforuser); $wheresql .= $hiddensql; $params = array_merge($hiddenparams, [ 'forumid' => $forumid, ]); $includefavourites = $includepostsforuser ? true : false; $sql = $this->generate_get_records_sql($wheresql, $this->get_sort_order($sortorder, $includefavourites), $includepostsforuser); $records = $this->get_db()->get_records_sql($sql, $params, $offset, $limit); return $this->transform_db_records_to_entities($records); } /** * Get each discussion, first post, first and last post author for the given forum, and the set of groups to display * considering timed posts, and pagination. * * @param int $forumid The forum to fetch the discussion set for * @param int[] $groupids The list of real groups to filter on * @param bool $includehiddendiscussions Whether to include hidden discussions or not * @param int|null $includepostsforuser Which user to include posts for, if any * @param int $sortorder The sort order to use * @param int $limit The number of discussions to fetch * @param int $offset The record offset * @return array The set of data fetched */ public function get_from_forum_id_and_group_id( int $forumid, array $groupids, bool $includehiddendiscussions, ?int $includepostsforuser, ?int $sortorder, int $limit, int $offset ) { $alias = $this->get_table_alias(); $wheresql = "{$alias}.forum = :forumid AND "; $groupparams = []; if (empty($groupids)) { $wheresql .= "{$alias}.groupid = :allgroupsid"; } else { list($insql, $groupparams) = $this->get_db()->get_in_or_equal($groupids, SQL_PARAMS_NAMED, 'gid'); $wheresql .= "({$alias}.groupid = :allgroupsid OR {$alias}.groupid {$insql})"; } [ 'wheresql' => $hiddensql, 'params' => $hiddenparams ] = $this->get_hidden_post_sql($includehiddendiscussions, $includepostsforuser); $wheresql .= $hiddensql; $params = array_merge($hiddenparams, $groupparams, [ 'forumid' => $forumid, 'allgroupsid' => -1, ]); $includefavourites = $includepostsforuser ? true : false; $sql = $this->generate_get_records_sql($wheresql, $this->get_sort_order($sortorder, $includefavourites), $includepostsforuser); $records = $this->get_db()->get_records_sql($sql, $params, $offset, $limit); return $this->transform_db_records_to_entities($records); } /** * Count the number of discussions in the forum. * * @param int $forumid Id of the forum to count discussions in * @param bool $includehiddendiscussions Include hidden dicussions in the count? * @param int|null $includepostsforuser Include discussions created by this user in the count * (only works if not including hidden discussions). * @return int */ public function get_total_discussion_count_from_forum_id( int $forumid, bool $includehiddendiscussions, ?int $includepostsforuser ) { $alias = $this->get_table_alias(); $wheresql = "{$alias}.forum = :forumid"; [ 'wheresql' => $hiddensql, 'params' => $hiddenparams ] = $this->get_hidden_post_sql($includehiddendiscussions, $includepostsforuser); $wheresql .= $hiddensql; $params = array_merge($hiddenparams, [ 'forumid' => $forumid, ]); return $this->get_db()->count_records_sql($this->generate_count_records_sql($wheresql), $params); } /** * Count the number of discussions in all groups and the list of groups provided. * * @param int $forumid Id of the forum to count discussions in * @param int[] $groupids List of group ids to include in the count (discussions in all groups will always be counted) * @param bool $includehiddendiscussions Include hidden dicussions in the count? * @param int|null $includepostsforuser Include discussions created by this user in the count * (only works if not including hidden discussions). * @return int */ public function get_total_discussion_count_from_forum_id_and_group_id( int $forumid, array $groupids, bool $includehiddendiscussions, ?int $includepostsforuser ) { $alias = $this->get_table_alias(); $wheresql = "{$alias}.forum = :forumid AND "; $groupparams = []; if (empty($groupids)) { $wheresql .= "{$alias}.groupid = :allgroupsid"; } else { list($insql, $groupparams) = $this->get_db()->get_in_or_equal($groupids, SQL_PARAMS_NAMED, 'gid'); $wheresql .= "({$alias}.groupid = :allgroupsid OR {$alias}.groupid {$insql})"; } [ 'wheresql' => $hiddensql, 'params' => $hiddenparams ] = $this->get_hidden_post_sql($includehiddendiscussions, $includepostsforuser); $wheresql .= $hiddensql; $params = array_merge($hiddenparams, $groupparams, [ 'forumid' => $forumid, 'allgroupsid' => -1, ]); return $this->get_db()->count_records_sql($this->generate_count_records_sql($wheresql), $params); } /** * Get the standard favouriting sql. * * @param int $userid The ID of the user we are getting the sql for * @return [$sql, $params] An array comprising of the sql and any associated params */ private function get_favourite_sql(int $userid): array { $usercontext = \context_user::instance($userid); $alias = $this->get_table_alias(); $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext); list($favsql, $favparams) = $ufservice->get_join_sql_by_type('mod_forum', 'discussions', $this->get_favourite_alias(), "$alias.id"); return [$favsql, $favparams]; } } PK-B\ꥲ3classes/local/vaults/preprocessors/extract_user.phpnu[. /** * Extract user vault preprocessor. * * @package mod_forum * @copyright 2018 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\vaults\preprocessors; defined('MOODLE_INTERNAL') || die(); use user_picture; /** * Extract user vault preprocessor. * * Used to separate out the user record * from a list of DB records that have been joined on the user table. * * @copyright 2018 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class extract_user { /** @var string $idalias The alias for the id property of the user */ private $idalias; /** @var string $alias The prefix used for each of the user properties */ private $alias; /** * Constructor. * * @param string $idalias The alias for the id property of the user * @param string $alias The prefix used for each of the user properties */ public function __construct(string $idalias, string $alias) { $this->idalias = $idalias; $this->alias = $alias; } /** * Extract the user records from a list of DB records. * * @param stdClass[] $records The DB records * @return stdClass[] The list of extracted users */ public function execute(array $records): array { $idalias = $this->idalias; $alias = $this->alias; return array_map(function($record) use ($idalias, $alias) { return user_picture::unalias($record, ['deleted'], $idalias, $alias); }, $records); } }PK-B\j6classes/local/vaults/preprocessors/extract_context.phpnu[. /** * Extract context vault preprocessor. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\vaults\preprocessors; defined('MOODLE_INTERNAL') || die(); use context; use context_helper; /** * Extract context vault preprocessor. * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class extract_context { /** * Extract the contexts from a list of records. * * @param stdClass[] $records The list of records which have context properties * @return context[] List of contexts matching the records. */ public function execute(array $records): array { return array_map(function($record) { $contextid = $record->ctxid; context_helper::preload_from_record($record); $context = context::instance_by_id($contextid); return $context; }, $records); } } PK-B\\%5classes/local/vaults/preprocessors/extract_record.phpnu[. /** * Extract record vault preprocessor. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\vaults\preprocessors; defined('MOODLE_INTERNAL') || die(); use moodle_database; use core\dml\table as dml_table; /** * Extract record vault preprocessor. * * Extract record vault preprocessor. Typically used to separate out records * when two tables have been joined together in a query. * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class extract_record { /** @var \core\dml\table $table The table object relating to the table that the records were loaded from */ private $table; /** * Constructor. * * @param string $table The table name where the records were loaded from * @param string $alias The table alias used as the record property prefix */ public function __construct(string $table, string $alias) { $this->table = new dml_table($table, $alias, $alias); } /** * Extract a record embedded in the properties of another record out into a * separate record. * * @param stdClass[] $records The list of records to process * @return stdClass[] The extracted records */ public function execute(array $records): array { return array_map(function($record) { return $this->table->extract_from_result($record); }, $records); } } PK-B\JCaa'classes/local/vaults/db_table_vault.phpnu[. /** * Abstract class for loading records from the DB. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\vaults; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\factories\entity as entity_factory; use moodle_database; /** * Abstract class for loading records from the DB. * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class db_table_vault { /** @var moodle_database $db A moodle database */ private $db; /** @var entity_factory $entityfactory Entity factory */ private $entityfactory; /** @var object $legacyfactory Entity->legacy factory */ private $legacyfactory; /** * Constructor. * * @param moodle_database $db A moodle database * @param entity_factory $entityfactory Entity factory * @param object $legacyfactory Legacy factory */ public function __construct( moodle_database $db, entity_factory $entityfactory, $legacyfactory ) { $this->db = $db; $this->entityfactory = $entityfactory; $this->legacyfactory = $legacyfactory; } /** * Get the table alias. * * @return string */ abstract protected function get_table_alias(): string; /** * Build the SQL to be used in get_records_sql. * * @param string|null $wheresql Where conditions for the SQL * @param string|null $sortsql Order by conditions for the SQL * @param object|null $user The user object * @return string */ abstract protected function generate_get_records_sql(?string $wheresql = null, ?string $sortsql = null, ?int $userid = null): string; /** * Convert the DB records into entities. The list of records will have been * passed through any preprocessors that may be defined before being given to * this function. * * Each result will from the list will be an associative array where the key * is set to the identifier given to the preprocessor and the result is the value * generated by the preprocessor. * * All results will have a 'record' key who's value is the original DB record. * * @param array $results The DB records */ abstract protected function from_db_records(array $results); /** * Get the list of preprocessors to run on the DB record results. The preprocessors * should be defined using an associative array. The key used to identify the * preprocessor in this list will be used to identify the value of that preprocessor * in the list of results when passed to the from_db_records function. * * @return array */ protected function get_preprocessors(): array { return []; } /** * Get the moodle database. * * @return moodle_database */ protected function get_db(): moodle_database { return $this->db; } /** * Get the entity factory. * * @return entity_factory */ protected function get_entity_factory(): entity_factory { return $this->entityfactory; } /** * Get the legacy factory * * @return object */ protected function get_legacy_factory() { return $this->legacyfactory; } /** * Execute the defined preprocessors on the DB record results and then convert * them into entities. * * @param stdClass[] $records List of DB results * @return array */ protected function transform_db_records_to_entities(array $records) { $preprocessors = $this->get_preprocessors(); $result = array_map(function($record) { return ['record' => $record]; }, $records); $result = array_reduce(array_keys($preprocessors), function($carry, $preprocessor) use ($records, $preprocessors) { $step = $preprocessors[$preprocessor]; $dependencies = $step->execute($records); foreach ($dependencies as $index => $dependency) { // Add the new dependency to the list. $carry[$index] = array_merge($carry[$index], [$preprocessor => $dependency]); } return $carry; }, $result); return $this->from_db_records($result); } /** * Get the entity for the given id. * * @param int $id Identifier for the entity * @return object|null */ public function get_from_id(int $id) { $records = $this->get_from_ids([$id]); return count($records) ? array_shift($records) : null; } /** * Get the list of entities for the given ids. * * @param int[] $ids Identifiers * @return array */ public function get_from_ids(array $ids) { $alias = $this->get_table_alias(); list($insql, $params) = $this->get_db()->get_in_or_equal($ids); $wheresql = $alias . '.id ' . $insql; $sql = $this->generate_get_records_sql($wheresql); $records = $this->get_db()->get_records_sql($sql, $params); return $this->transform_db_records_to_entities($records); } } PK-B\ R`GG#classes/local/vaults/discussion.phpnu[. /** * Discussion vault class. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\vaults; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\container; use mod_forum\local\entities\forum as forum_entity; use mod_forum\local\entities\discussion as discussion_entity; /** * Discussion vault class. * * This should be the only place that accessed the database. * * This uses the repository pattern. See: * https://designpatternsphp.readthedocs.io/en/latest/More/Repository/README.html * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class discussion extends db_table_vault { /** The table for this vault */ private const TABLE = 'forum_discussions'; /** * Get the table alias. * * @return string */ protected function get_table_alias(): string { return 'd'; } /** * Build the SQL to be used in get_records_sql. * * @param string|null $wheresql Where conditions for the SQL * @param string|null $sortsql Order by conditions for the SQL * @param int|null $userid The user ID * @return string */ protected function generate_get_records_sql(?string $wheresql = null, ?string $sortsql = null, ?int $userid = null): string { $selectsql = 'SELECT * FROM {' . self::TABLE . '} ' . $this->get_table_alias(); $selectsql .= $wheresql ? ' WHERE ' . $wheresql : ''; $selectsql .= $sortsql ? ' ORDER BY ' . $sortsql : ''; return $selectsql; } /** * Convert the DB records into discussion entities. * * @param array $results The DB records * @return discussion_entity[] */ protected function from_db_records(array $results) { $entityfactory = $this->get_entity_factory(); return array_map(function(array $result) use ($entityfactory) { [ 'record' => $record, ] = $result; return $entityfactory->get_discussion_from_stdclass($record); }, $results); } /** * Get all discussions in the specified forum. * * @param forum_entity $forum * @return array */ public function get_all_discussions_in_forum(forum_entity $forum, ?string $sort = null): ?array { global $USER; $options = ['forum' => $forum->get_id()]; $managerfactory = container::get_manager_factory(); $capabilitymanager = $managerfactory->get_capability_manager($forum); $select = "forum = :forum"; if ($forum->is_in_group_mode() && !$capabilitymanager->can_access_all_groups($USER)) { $allowedgroups = groups_get_activity_allowed_groups($forum->get_course_module_record()); $allowedgroups = implode(",", array_keys($allowedgroups)); if (!$allowedgroups) { return []; } $select .= " AND groupid IN ($allowedgroups)"; } $records = $this->get_db()->get_records_select(self::TABLE, $select, $options, $sort ?? ''); return $this->transform_db_records_to_entities($records); } /** * Get the first discussion in the specified forum. * * @param forum_entity $forum * @return discussion_entity|null */ public function get_first_discussion_in_forum(forum_entity $forum): ?discussion_entity { $records = $this->get_db()->get_records(self::TABLE, [ 'forum' => $forum->get_id(), ], 'timemodified ASC', '*', 0, 1); $records = $this->transform_db_records_to_entities($records); return count($records) ? array_shift($records) : null; } /** * Get the last discussion in the specified forum. * * @param forum_entity $forum * @return discussion_entity|null */ public function get_last_discussion_in_forum(forum_entity $forum): ?discussion_entity { $records = $this->get_db()->get_records(self::TABLE, [ 'forum' => $forum->get_id(), ], 'timemodified DESC', '*', 0, 1); $records = $this->transform_db_records_to_entities($records); return count($records) ? array_shift($records) : null; } /** * Get the count of the discussions in the specified forum. * * @param forum_entity $forum * @return int */ public function get_count_discussions_in_forum(forum_entity $forum): ?int { return $this->get_db()->count_records(self::TABLE, [ 'forum' => $forum->get_id()]); } /** * Update the discussion * * @param discussion_entity $discussion * @return discussion_entity|null */ public function update_discussion(discussion_entity $discussion): ?discussion_entity { $discussionrecord = $this->get_legacy_factory()->to_legacy_object($discussion); if ($this->get_db()->update_record('forum_discussions', $discussionrecord)) { $records = $this->transform_db_records_to_entities([$discussionrecord]); return count($records) ? array_shift($records) : null; } return null; } } PK-B\Fvclasses/local/vaults/author.phpnu[. /** * Author vault class. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\vaults; defined('MOODLE_INTERNAL') || die(); /** * Author vault class. * * This should be the only place that accessed the database. * * This uses the repository pattern. See: * https://designpatternsphp.readthedocs.io/en/latest/More/Repository/README.html * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class author extends db_table_vault { /** The table for this vault */ private const TABLE = 'user'; /** * Get the table alias. * * @return string */ protected function get_table_alias(): string { return 'a'; } /** * Build the SQL to be used in get_records_sql. * * @param string|null $wheresql Where conditions for the SQL * @param string|null $sortsql Order by conditions for the SQL * @param int|null $userid The user ID * @return string */ protected function generate_get_records_sql(?string $wheresql = null, ?string $sortsql = null, ?int $userid = null): string { $selectsql = 'SELECT * FROM {' . self::TABLE . '} ' . $this->get_table_alias(); $selectsql .= $wheresql ? ' WHERE ' . $wheresql : ''; $selectsql .= $sortsql ? ' ORDER BY ' . $sortsql : ''; return $selectsql; } /** * Convert the DB records into author entities. * * @param array $results The DB records * @return author_entity[] */ protected function from_db_records(array $results) { $entityfactory = $this->get_entity_factory(); return array_map(function(array $result) use ($entityfactory) { [ 'record' => $record, ] = $result; return $entityfactory->get_author_from_stdclass($record); }, $results); } /** * Get the authors for the given posts. * * Returns a distinct list of authors indexed by author id. * * @param post_entity[] $posts The list of posts * @return author_entity[] */ public function get_authors_for_posts(array $posts): array { $authorids = array_reduce($posts, function($carry, $post) { $carry[$post->get_author_id()] = true; return $carry; }, []); $authorids = array_keys($authorids); return $this->get_from_ids($authorids); } /** * Get the context ids for a set of author ids. The results are indexed * by the author id. * * @param int[] $authorids The list of author ids to fetch. * @return int[] Results indexed by author id. */ public function get_context_ids_for_author_ids(array $authorids): array { $db = $this->get_db(); [$insql, $params] = $db->get_in_or_equal($authorids); $sql = "SELECT instanceid, id FROM {context} WHERE contextlevel = ? AND instanceid {$insql}"; $records = $db->get_records_sql($sql, array_merge([CONTEXT_USER], $params)); return array_reduce($authorids, function($carry, $id) use ($records) { $carry[$id] = isset($records[$id]) ? (int) $records[$id]->id : null; return $carry; }, []); } } PK-B\AAclasses/local/container.phpnu[. /** * Container class. * * @package mod_forum * @copyright 2018 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\factories\renderer as renderer_factory; use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory; use mod_forum\local\factories\entity as entity_factory; use mod_forum\local\factories\exporter as exporter_factory; use mod_forum\local\factories\manager as manager_factory; use mod_forum\local\factories\vault as vault_factory; use mod_forum\local\factories\builder as builder_factory; use mod_forum\local\factories\url as url_factory; /** * Container class. * * This class provides helper methods with static configurations to get any * of the factories from the "local" namespace. * * @copyright 2018 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class container { /** * Create the renderer factory. * * @return renderer_factory */ public static function get_renderer_factory(): renderer_factory { global $PAGE; return new renderer_factory( self::get_legacy_data_mapper_factory(), self::get_exporter_factory(), self::get_vault_factory(), self::get_manager_factory(), self::get_entity_factory(), self::get_builder_factory(), self::get_url_factory(), $PAGE ); } /** * Create the legacy data mapper factory. * * @return legacy_data_mapper_factory */ public static function get_legacy_data_mapper_factory(): legacy_data_mapper_factory { return new legacy_data_mapper_factory(); } /** * Create the exporter factory. * * @return exporter_factory */ public static function get_exporter_factory(): exporter_factory { return new exporter_factory( self::get_legacy_data_mapper_factory(), self::get_manager_factory(), self::get_url_factory(), self::get_vault_factory() ); } /** * Create the vault factory. * * @return vault_factory */ public static function get_vault_factory(): vault_factory { global $DB; return new vault_factory( $DB, self::get_entity_factory(), get_file_storage(), self::get_legacy_data_mapper_factory() ); } /** * Create the manager factory. * * @return manager_factory */ public static function get_manager_factory(): manager_factory { return new manager_factory( self::get_legacy_data_mapper_factory() ); } /** * Create the entity factory. * * @return entity_factory */ public static function get_entity_factory(): entity_factory { return new entity_factory(); } /** * Create the builder factory. * * @return builder_factory */ public static function get_builder_factory(): builder_factory { global $PAGE; return new builder_factory( self::get_legacy_data_mapper_factory(), self::get_exporter_factory(), self::get_vault_factory(), self::get_manager_factory(), $PAGE->get_renderer('mod_forum') ); } /** * Create the URL factory. * * @return url_factory */ public static function get_url_factory(): url_factory { return new url_factory( self::get_legacy_data_mapper_factory() ); } } PK-B\Ә!&!&classes/local/entities/post.phpnu[. /** * Post class. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\entities; defined('MOODLE_INTERNAL') || die(); use stdClass; /** * Post class. * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class post { /** @var int $id ID */ private $id; /** @var int $discussionid The id of the discussion this post belongs to */ private $discussionid; /** @var int $parentid The id of the post that this post is replying to. Zero if it isn't a reply. */ private $parentid; /** @var int $authorid The id of user who authored the post */ private $authorid; /** @var int $timecreated Timestamp for when the post was created */ private $timecreated; /** @var int $timemodified Timestamp for when the post last modified */ private $timemodified; /** @var bool $mailed If the post has been mailed */ private $mailed; /** @var string $subject Post subject */ private $subject; /** @var string $message Post message */ private $message; /** @var int $messageformat Format of the post message */ private $messageformat; /** @var bool $messagetrust Is this a trusted message, i.e. created by a trusted user. */ private $messagetrust; /** @var bool $hasattachments Does the post have attachments */ private $hasattachments; /** @var int $totalscore Total score */ private $totalscore; /** @var bool $mailnow Should this post be mailed immediately */ private $mailnow; /** @var bool $deleted Is the post deleted */ private $deleted; /** @var int $privatereplyto The user being privately replied to */ private $privatereplyto; /** @var int $wordcount Number of words in the message */ private $wordcount; /** @var int $charcount Number of chars in the message */ private $charcount; /** * Constructor. * * @param int $id ID * @param int $discussionid The id of the discussion this post belongs to * @param int $parentid The id of the post that this post is replying to. Zero if it isn't a reply. * @param int $authorid The id of user who authored the post * @param int $timecreated Timestamp for when the post was created * @param int $timemodified Timestamp for when the post last modified * @param bool $mailed If the post has been mailed * @param string $subject Post subject * @param string $message Post message * @param int $messageformat Format of the post message * @param bool $messagetrust Is this a trusted message, i.e. created by a trusted user. * @param bool $hasattachments Does the post have attachments * @param int $totalscore Total score * @param bool $mailnow Should this post be mailed immediately * @param bool $deleted Is the post deleted * @param int $privatereplyto Which user this reply is intended for in a private reply situation */ public function __construct( int $id, int $discussionid, int $parentid, int $authorid, int $timecreated, int $timemodified, bool $mailed, string $subject, string $message, int $messageformat, bool $messagetrust, bool $hasattachments, int $totalscore, bool $mailnow, bool $deleted, int $privatereplyto, ?int $wordcount, ?int $charcount ) { $this->id = $id; $this->discussionid = $discussionid; $this->parentid = $parentid; $this->authorid = $authorid; $this->timecreated = $timecreated; $this->timemodified = $timemodified; $this->mailed = $mailed; $this->subject = $subject; $this->message = $message; $this->messageformat = $messageformat; $this->messagetrust = $messagetrust; $this->hasattachments = $hasattachments; $this->totalscore = $totalscore; $this->mailnow = $mailnow; $this->deleted = $deleted; $this->privatereplyto = $privatereplyto; $this->wordcount = $wordcount; $this->charcount = $charcount; } /** * Get the post id. * * @return int */ public function get_id(): int { return $this->id; } /** * Get the discussion id. * * @return int */ public function get_discussion_id(): int { return $this->discussionid; } /** * Get the id of the parent post. Returns zero if this post is not a reply. * * @return int */ public function get_parent_id(): int { return $this->parentid; } /** * Does this post have a parent? I.e. is it a reply? * * @return bool */ public function has_parent(): bool { return $this->get_parent_id() > 0; } /** * Get the id of the user that authored the post. * * @return int */ public function get_author_id(): int { return $this->authorid; } /** * Get the timestamp for when this post was created. * * @return int */ public function get_time_created(): int { return $this->timecreated; } /** * Get the timestamp for when this post was last modified. * * @return int */ public function get_time_modified(): int { return $this->timemodified; } /** * Has this post been mailed? * * @return bool */ public function has_been_mailed(): bool { return $this->mailed; } /** * Get the post subject. * * @return string */ public function get_subject(): string { return $this->subject; } /** * Get the post message. * * @return string */ public function get_message(): string { return $this->message; } /** * Get the post message format. * * @return int */ public function get_message_format(): int { return $this->messageformat; } /** * Is this a trusted message? I.e. was it authored by a trusted user? * * @return bool */ public function is_message_trusted(): bool { return $this->messagetrust; } /** * Does this post have attachments? * * @return bool */ public function has_attachments(): bool { return $this->hasattachments; } /** * Get the total score. * * @return int */ public function get_total_score(): int { return $this->totalscore; } /** * Should this post be mailed now? * * @return bool */ public function should_mail_now(): bool { return $this->mailnow; } /** * Is this post deleted? * * @return bool */ public function is_deleted(): bool { return $this->deleted; } /** * Is this post private? * * @return bool */ public function is_private_reply(): bool { return !empty($this->privatereplyto); } /** * Get the id of the user that this post was intended for. * * @return int */ public function get_private_reply_recipient_id(): int { return $this->privatereplyto; } /** * Get the post's age in seconds. * * @return int */ public function get_age(): int { return time() - $this->get_time_created(); } /** * Check if the given user authored this post. * * @param stdClass $user The user to check. * @return bool */ public function is_owned_by_user(stdClass $user): bool { return $this->get_author_id() == $user->id; } /** * Check if the given post is a private reply intended for the given user. * * @param stdClass $user The user to check. * @return bool */ public function is_private_reply_intended_for_user(stdClass $user): bool { return $this->get_private_reply_recipient_id() == $user->id; } /** * Returns the word count. * * @return int|null */ public function get_wordcount(): ?int { return $this->wordcount; } /** * Returns the char count. * * @return int|null */ public function get_charcount(): ?int { return $this->charcount; } /** * This methods adds/updates forum posts' word count and char count attributes based on $data->message. * * @param \stdClass $record A record ready to be inserted / updated in DB. * @return void. */ public static function add_message_counts(\stdClass $record): void { if (!empty($record->message)) { $record->wordcount = count_words($record->message, $record->messageformat); $record->charcount = count_letters($record->message, $record->messageformat); } } } PK-B\#GG classes/local/entities/forum.phpnu[. /** * Forum class. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\entities; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/forum/lib.php'); require_once($CFG->dirroot . '/rating/lib.php'); use mod_forum\local\entities\discussion as discussion_entity; use context; use stdClass; /** * Forum class. * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class forum { /** @var context $context The forum module context */ private $context; /** @var stdClass $coursemodule The forum course module record */ private $coursemodule; /** @var stdClass $course The forum course record */ private $course; /** @var int $effectivegroupmode The effective group mode */ private $effectivegroupmode; /** @var int $id ID */ private $id; /** @var int $courseid Id of the course this forum is in */ private $courseid; /** @var string $type The forum type, e.g. single, qanda, etc */ private $type; /** @var string $name Name of the forum */ private $name; /** @var string $intro Intro text */ private $intro; /** @var int $introformat Format of the intro text */ private $introformat; /** @var int $assessed The forum rating aggregate */ private $assessed; /** @var int $assesstimestart Timestamp to begin assessment */ private $assesstimestart; /** @var int $assesstimefinish Timestamp to end assessment */ private $assesstimefinish; /** @var int $scale The rating scale */ private $scale; /** @var int $gradeforum The grade for the forum when grading holistically */ private $gradeforum; /** @var bool $gradeforumnotify Whether to notify students when the forum is graded holistically */ private $gradeforumnotify; /** @var int $maxbytes Maximum attachment size */ private $maxbytes; /** @var int $maxattachments Maximum number of attachments */ private $maxattachments; /** @var int $forcesubscribe Does the forum force users to subscribe? */ private $forcesubscribe; /** @var int $trackingtype Tracking type */ private $trackingtype; /** @var int $rsstype RSS type */ private $rsstype; /** @var int $rssarticles RSS articles */ private $rssarticles; /** @var int $timemodified Timestamp when the forum was last modified */ private $timemodified; /** @var int $warnafter Warn after */ private $warnafter; /** @var int $blockafter Block after */ private $blockafter; /** @var int $blockperiod Block period */ private $blockperiod; /** @var int $completiondiscussions Completion discussions */ private $completiondiscussions; /** @var int $completionreplies Completion replies */ private $completionreplies; /** @var int $completionposts Completion posts */ private $completionposts; /** @var bool $displaywordcount Should display word counts in posts */ private $displaywordcount; /** @var bool $lockdiscussionafter Timestamp after which discussions should be locked */ private $lockdiscussionafter; /** @var int $duedate Timestamp that represents the due date for forum posts */ private $duedate; /** @var int $cutoffdate Timestamp after which forum posts will no longer be accepted */ private $cutoffdate; /** * Constructor * * @param context $context The forum module context * @param stdClass $coursemodule The forum course module record * @param stdClass $course The forum course record * @param int $effectivegroupmode The effective group mode * @param int $id ID * @param int $courseid Id of the course this forum is in * @param string $type The forum type, e.g. single, qanda, etc * @param string $name Name of the forum * @param string $intro Intro text * @param int $introformat Format of the intro text * @param int $assessed The forum rating aggregate * @param int $assesstimestart Timestamp to begin assessment * @param int $assesstimefinish Timestamp to end assessment * @param int $scale The rating scale * @param int $gradeforum The holistic grade * @param bool $gradeforumnotify Default for whether to notify students when grade holistically * @param int $maxbytes Maximum attachment size * @param int $maxattachments Maximum number of attachments * @param int $forcesubscribe Does the forum force users to subscribe? * @param int $trackingtype Tracking type * @param int $rsstype RSS type * @param int $rssarticles RSS articles * @param int $timemodified Timestamp when the forum was last modified * @param int $warnafter Warn after * @param int $blockafter Block after * @param int $blockperiod Block period * @param int $completiondiscussions Completion discussions * @param int $completionreplies Completion replies * @param int $completionposts Completion posts * @param bool $displaywordcount Should display word counts in posts * @param int $lockdiscussionafter Timestamp after which discussions should be locked * @param int $duedate Timestamp that represents the due date for forum posts * @param int $cutoffdate Timestamp after which forum posts will no longer be accepted */ public function __construct( context $context, stdClass $coursemodule, stdClass $course, int $effectivegroupmode, int $id, int $courseid, string $type, string $name, string $intro, int $introformat, int $assessed, int $assesstimestart, int $assesstimefinish, int $scale, int $gradeforum, bool $gradeforumnotify, int $maxbytes, int $maxattachments, int $forcesubscribe, int $trackingtype, int $rsstype, int $rssarticles, int $timemodified, int $warnafter, int $blockafter, int $blockperiod, int $completiondiscussions, int $completionreplies, int $completionposts, bool $displaywordcount, int $lockdiscussionafter, int $duedate, int $cutoffdate ) { $this->context = $context; $this->coursemodule = $coursemodule; $this->course = $course; $this->effectivegroupmode = $effectivegroupmode; $this->id = $id; $this->courseid = $courseid; $this->type = $type; $this->name = $name; $this->intro = $intro; $this->introformat = $introformat; $this->assessed = $assessed; $this->assesstimestart = $assesstimestart; $this->assesstimefinish = $assesstimefinish; $this->scale = $scale; $this->gradeforum = $gradeforum; $this->gradeforumnotify = $gradeforumnotify; $this->maxbytes = $maxbytes; $this->maxattachments = $maxattachments; $this->forcesubscribe = $forcesubscribe; $this->trackingtype = $trackingtype; $this->rsstype = $rsstype; $this->rssarticles = $rssarticles; $this->timemodified = $timemodified; $this->warnafter = $warnafter; $this->blockafter = $blockafter; $this->blockperiod = $blockperiod; $this->completiondiscussions = $completiondiscussions; $this->completionreplies = $completionreplies; $this->completionposts = $completionposts; $this->displaywordcount = $displaywordcount; $this->lockdiscussionafter = $lockdiscussionafter; $this->duedate = $duedate; $this->cutoffdate = $cutoffdate; } /** * Get the forum module context. * * @return context */ public function get_context(): context { return $this->context; } /** * Get the forum course module record * * @return stdClass */ public function get_course_module_record(): stdClass { return $this->coursemodule; } /** * Get the effective group mode. * * @return int */ public function get_effective_group_mode(): int { return $this->effectivegroupmode; } /** * Check if the forum is set to group mode. * * @return bool */ public function is_in_group_mode(): bool { return $this->get_effective_group_mode() !== NOGROUPS; } /** * Get the course record. * * @return stdClass */ public function get_course_record(): stdClass { return $this->course; } /** * Get the forum id. * * @return int */ public function get_id(): int { return $this->id; } /** * Get the id of the course that the forum belongs to. * * @return int */ public function get_course_id(): int { return $this->courseid; } /** * Get the forum type. * * @return string */ public function get_type(): string { return $this->type; } /** * Get the forum name. * * @return string */ public function get_name(): string { return $this->name; } /** * Get the forum intro text. * * @return string */ public function get_intro(): string { return $this->intro; } /** * Get the forum intro text format. * * @return int */ public function get_intro_format(): int { return $this->introformat; } /** * Get the rating aggregate. * * @return int */ public function get_rating_aggregate(): int { return $this->assessed; } /** * Does the forum have a rating aggregate? * * @return bool */ public function has_rating_aggregate(): bool { return $this->get_rating_aggregate() != RATING_AGGREGATE_NONE; } /** * Get the timestamp for when the assessment period begins. * * @return int */ public function get_assess_time_start(): int { return $this->assesstimestart; } /** * Get the timestamp for when the assessment period ends. * * @return int */ public function get_assess_time_finish(): int { return $this->assesstimefinish; } /** * Get the rating scale. * * @return int */ public function get_scale(): int { return $this->scale; } /** * Get the grade for the forum when grading holistically. * * @return int */ public function get_grade_for_forum(): int { return $this->gradeforum; } /** * Whether grading is enabled for this item. * * @return bool */ public function is_grading_enabled(): bool { return $this->get_grade_for_forum() !== 0; } /** * Get the default for whether the students should be notified when grading holistically. * * @return bool */ public function should_notify_students_default_when_grade_for_forum(): bool { return $this->gradeforumnotify; } /** * Get the maximum bytes. * * @return int */ public function get_max_bytes(): int { return $this->maxbytes; } /** * Get the maximum number of attachments. * * @return int */ public function get_max_attachments(): int { return $this->maxattachments; } /** * Get the subscription mode. * * @return int */ public function get_subscription_mode(): int { return $this->forcesubscribe; } /** * Is the subscription mode set to optional. * * @return bool */ public function is_subscription_optional(): bool { return $this->get_subscription_mode() === FORUM_CHOOSESUBSCRIBE; } /** * Is the subscription mode set to forced. * * @return bool */ public function is_subscription_forced(): bool { return $this->get_subscription_mode() === FORUM_FORCESUBSCRIBE; } /** * Is the subscription mode set to automatic. * * @return bool */ public function is_subscription_automatic(): bool { return $this->get_subscription_mode() === FORUM_INITIALSUBSCRIBE; } /** * Is the subscription mode set to disabled. * * @return bool */ public function is_subscription_disabled(): bool { return $this->get_subscription_mode() === FORUM_DISALLOWSUBSCRIBE; } /** * Get the tracking type. * * @return int */ public function get_tracking_type(): int { return $this->trackingtype; } /** * Get the RSS type. * * @return int */ public function get_rss_type(): int { return $this->rsstype; } /** * Get the RSS articles. * * @return int */ public function get_rss_articles(): int { return $this->rssarticles; } /** * Get the timestamp for when the forum was last modified. * * @return int */ public function get_time_modified(): int { return $this->timemodified; } /** * Get warn after. * * @return int */ public function get_warn_after(): int { return $this->warnafter; } /** * Get block after. * * @return int */ public function get_block_after(): int { return $this->blockafter; } /** * Get the block period. * * @return int */ public function get_block_period(): int { return $this->blockperiod; } /** * Does the forum have blocking enabled? * * @return bool */ public function has_blocking_enabled(): bool { return !empty($this->get_block_after()) && !empty($this->get_block_period()); } /** * Get the completion discussions. * * @return int */ public function get_completion_discussions(): int { return $this->completiondiscussions; } /** * Get the completion replies. * * @return int */ public function get_completion_replies(): int { return $this->completionreplies; } /** * Get the completion posts. * * @return int */ public function get_completion_posts(): int { return $this->completionposts; } /** * Should the word counts be shown in the posts? * * @return bool */ public function should_display_word_count(): bool { return $this->displaywordcount; } /** * Get the timestamp after which the discussion should be locked. * * @return int */ public function get_lock_discussions_after(): int { return $this->lockdiscussionafter; } /** * Does the forum have a discussion locking timestamp? * * @return bool */ public function has_lock_discussions_after(): bool { return !empty($this->get_lock_discussions_after()); } /** * Check whether the discussion is locked based on forum's time based locking criteria * * @param discussion_entity $discussion * @return bool */ public function is_discussion_time_locked(discussion_entity $discussion): bool { if (!$this->has_lock_discussions_after()) { return false; } if ($this->get_type() === 'single') { // It does not make sense to lock a single discussion forum. return false; } return (($discussion->get_time_modified() + $this->get_lock_discussions_after()) < time()); } /** * Get the cutoff date. * * @return int */ public function get_cutoff_date(): int { return $this->cutoffdate; } /** * Does the forum have a cutoff date? * * @return bool */ public function has_cutoff_date(): bool { return !empty($this->get_cutoff_date()); } /** * Is the cutoff date for the forum reached? * * @return bool */ public function is_cutoff_date_reached(): bool { if ($this->has_cutoff_date() && ($this->get_cutoff_date() < time())) { return true; } return false; } /** * Get the due date. * * @return int */ public function get_due_date(): int { return $this->duedate; } /** * Does the forum have a due date? * * @return bool */ public function has_due_date(): bool { return !empty($this->get_due_date()); } /** * Is the due date for the forum reached? * * @return bool */ public function is_due_date_reached(): bool { if ($this->has_due_date() && ($this->get_due_date() < time())) { return true; } return false; } /** * Is the discussion locked? - Takes into account both discussion settings AND forum's criteria * * @param discussion_entity $discussion The discussion to check * @return bool */ public function is_discussion_locked(discussion_entity $discussion): bool { if ($discussion->is_locked()) { return true; } return $this->is_discussion_time_locked($discussion); } } PK-B\D D 7classes/local/entities/post_read_receipt_collection.phpnu[. /** * Post read receipt collection class. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\entities; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\post as post_entity; use stdClass; /** * Post read receipt collection class. * * Contains the list of read receipts for posts. * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class post_read_receipt_collection { /** @var stdClass[] $receiptsbypostid Receipt records indexed by post id */ private $receiptsbypostid = []; /** * Constructor. * * @param array $records The list of post read receipt records. */ public function __construct(array $records) { foreach ($records as $record) { $postid = $record->postid; if (isset($this->receiptsbypostid[$postid])) { $this->receiptsbypostid[$postid][] = $record; } else { $this->receiptsbypostid[$postid] = [$record]; } } } /** * Check whether a user has read a post. * * @param stdClass $user The user to check * @param post_entity $post The post to check * @return bool */ public function has_user_read_post(stdClass $user, post_entity $post): bool { global $CFG; $isoldpost = ($post->get_time_modified() < (time() - ($CFG->forum_oldpostdays * 24 * 3600))); if ($isoldpost) { return true; } $receipts = isset($this->receiptsbypostid[$post->get_id()]) ? $this->receiptsbypostid[$post->get_id()] : []; foreach ($receipts as $receipt) { if ($receipt->userid == $user->id) { return true; } } return false; } } PK-B\_ -classes/local/entities/discussion_summary.phpnu[. /** * Discussion summary class. * * @package mod_forum * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\entities; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\discussion as discussion_entity; use mod_forum\local\entities\post as post_entity; use mod_forum\local\entities\author as author_entity; /** * Discussion summary class. * * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class discussion_summary { /** @var discussion_entity $discussion The discussion being summarised */ private $discussion; /** @var author_entity $firstpostauthor Author of the first post in the discussion */ private $firstpostauthor; /** @var post_entity $firstpost First post in the discussion */ private $firstpost; /** @var author_entity $latestpostauthor Author of the last post in the discussion */ private $latestpostauthor; /** * Constructor. * * @param discussion_entity $discussion The discussion being summarised * @param post_entity $firstpost First post in the discussion * @param author_entity $firstpostauthor Author of the first post in the discussion * @param author_entity $latestpostauthor Author of the last post in the discussion */ public function __construct( discussion_entity $discussion, post_entity $firstpost, author_entity $firstpostauthor, author_entity $latestpostauthor ) { $this->discussion = $discussion; $this->firstpostauthor = $firstpostauthor; $this->firstpost = $firstpost; $this->latestpostauthor = $latestpostauthor; } /** * Get the discussion entity. * * @return discussion_entity */ public function get_discussion(): discussion_entity { return $this->discussion; } /** * Get the author entity for the first post. * * @return author_entity */ public function get_first_post_author(): author_entity { return $this->firstpostauthor; } /** * Get the author entity for the last post. * * @return author_entity */ public function get_latest_post_author(): author_entity { return $this->latestpostauthor; } /** * Get the post entity for the first post. * * @return post_entity */ public function get_first_post(): post_entity { return $this->firstpost; } } PK-B\SS!classes/local/entities/sorter.phpnu[. /** * Class to sort items. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\entities; defined('MOODLE_INTERNAL') || die(); /** * Class to sort lists of items. * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sorter { /** @var callable $getid Function used to get the id from an item */ private $getid; /** @var callable $getparentid Function used to get the parent id from an item */ private $getparentid; /** * Constructor. * * Allows the calling code to provide 2 functions to get the id and parent id from * the list of items it is intended to process. * * This allows this class to be composed in numerous different ways to support various * types of items while keeping the underlying sorting algorithm consistent. * * @param callable $getid Function used to get the id from an item * @param callable $getparentid Function used to get the parent id from an item */ public function __construct(callable $getid, callable $getparentid) { $this->getid = $getid; $this->getparentid = $getparentid; } /** * Sort a list of items into a parent/child data structure. The resulting data structure * is a recursive array of arrays where the first element is the parent and the second is * an array of it's children. * * For example * If we have an array of items A, B, C, and D where D is a child of C, B and C are children * of A. * * This function would sort them into the following: * [ * [ * A, * [ * [ * B, * [] * ], * [ * C, * [ * [ * D, * [] * ] * ] * ] * ] * ] * ] * * @param array $items The list of items to sort. * @return array */ public function sort_into_children(array $items): array { $ids = array_reduce($items, function($carry, $item) { $carry[($this->getid)($item)] = true; return $carry; }, []); // Split out the items into "parents" and "replies" (children). These are unsorted // at this point. [$parents, $replies] = array_reduce($items, function($carry, $item) use ($ids) { $parentid = ($this->getparentid)($item); if (!empty($ids[$parentid])) { // This is a child to another item in the list so add it to the children list. $carry[1][] = $item; } else { // This isn't a child to anything in our list so it's a parent. $carry[0][] = $item; } return $carry; }, [[], []]); if (empty($replies)) { return array_map(function($parent) { return [$parent, []]; }, $parents); } // Recurse to sort the replies into the correct nesting. $sortedreplies = $this->sort_into_children($replies); // Sort the parents and sorted replies into their matching pairs. return array_map(function($parent) use ($sortedreplies) { $parentid = ($this->getid)($parent); return [ $parent, array_values(array_filter($sortedreplies, function($replydata) use ($parentid) { return ($this->getparentid)($replydata[0]) == $parentid; })) ]; }, $parents); } /** * Take the data structure returned from "sort_into_children" and flatten it back * into an array. It does a depth first flatten which maintains the reply ordering. * * @param array $items Items in the data structure returned by "sort_into_children" * @return array A flat array. */ public function flatten_children(array $items): array { $result = []; foreach ($items as [$item, $children]) { $result[] = $item; $result = array_merge($result, $this->flatten_children($children)); } return $result; } } PK-B\##%classes/local/entities/discussion.phpnu[. /** * Discussion class. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\entities; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\post as post_entity; /** * Discussion class. * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class discussion { /** @var int $id ID */ private $id; /** @var int $courseid Course id */ private $courseid; /** @var int $forumid Forum id */ private $forumid; /** @var string $name Discussion name */ private $name; /** @var int $firstpostid Id of the first post in the discussion */ private $firstpostid; /** @var int $userid Id of the user that created the discussion */ private $userid; /** @var int $groupid Group id if it's a group dicussion */ private $groupid; /** @var bool $assessed Is the discussion assessed? */ private $assessed; /** @var int $timemodified Timestamp for last modification to the discussion */ private $timemodified; /** @var int $usermodified Id of user that last modified the discussion */ private $usermodified; /** @var int $timestart Start time for the discussion */ private $timestart; /** @var int $timeend End time for the discussion */ private $timeend; /** @var bool $pinned Is the discussion pinned? */ private $pinned; /** @var int $locked The timestamp of when the discussion was locked */ private $timelocked; /** * Constructor. * * @param int $id ID * @param int $courseid Course id * @param int $forumid Forum id * @param string $name Discussion name * @param int $firstpostid Id of the first post in the discussion * @param int $userid Id of the user that created the discussion * @param int $groupid Group id if it's a group dicussion * @param bool $assessed Is the discussion assessed? * @param int $timemodified Timestamp for last modification to the discussion * @param int $usermodified Id of user that last modified the discussion * @param int $timestart Start time for the discussion * @param int $timeend End time for the discussion * @param bool $pinned Is the discussion pinned? * @param int $locked Time this discussion was locked */ public function __construct( int $id, int $courseid, int $forumid, string $name, int $firstpostid, int $userid, int $groupid, bool $assessed, int $timemodified, int $usermodified, int $timestart, int $timeend, bool $pinned, int $locked ) { $this->id = $id; $this->courseid = $courseid; $this->forumid = $forumid; $this->name = $name; $this->firstpostid = $firstpostid; $this->userid = $userid; $this->groupid = $groupid; $this->assessed = $assessed; $this->timemodified = $timemodified; $this->usermodified = $usermodified; $this->timestart = $timestart; $this->timeend = $timeend; $this->pinned = $pinned; $this->timelocked = $locked; } /** * Get the discussion id. * * @return int */ public function get_id(): int { return $this->id; } /** * Get the course id. * * @return int */ public function get_course_id(): int { return $this->courseid; } /** * Get the forum id. * * @return int */ public function get_forum_id(): int { return $this->forumid; } /** * Get the name of the discussion. * * @return string */ public function get_name(): string { return $this->name; } /** * Get the id of the fist post in the discussion. * * @return int */ public function get_first_post_id(): int { return $this->firstpostid; } /** * Get the id of the user that created the discussion. * * @return int */ public function get_user_id(): int { return $this->userid; } /** * Get the id of the group that this discussion belongs to. * * @return int */ public function get_group_id(): int { return $this->groupid; } /** * Check if this discussion is assessed. * * @return bool */ public function is_assessed(): bool { return $this->assessed; } /** * Get the timestamp for when this discussion was last modified. * * @return int */ public function get_time_modified(): int { return $this->timemodified; } /** * Get the id of the user that last modified this discussion. * * @return int */ public function get_user_modified(): int { return $this->usermodified; } /** * Get the start time of this discussion. Returns zero if the discussion * has no designated start time. * * @return int */ public function get_time_start(): int { return $this->timestart; } /** * Get the end time of this discussion. Returns zero if the discussion * has no designated end time. * * @return int */ public function get_time_end(): int { return $this->timeend; } /** * Check if this discussion is pinned. * * @return bool */ public function is_pinned(): bool { return $this->pinned; } /** * Get the locked time of this discussion. * * @return bool */ public function get_locked(): int { return $this->timelocked; } /** * Is this discussion locked based on it's locked attribute * * @return bool */ public function is_locked(): bool { return ($this->timelocked ? true : false); } /** * Set the locked timestamp * * @param int $timestamp The value we want to store into 'locked' */ public function toggle_locked_state(int $timestamp) { // Check the current value against what we want the value to be i.e. '$timestamp'. $this->timelocked = ($this->timelocked && $timestamp ? $this->timelocked : $timestamp); } /** * Check if the given post is the first post in this discussion. * * @param post_entity $post The post to check * @return bool */ public function is_first_post(post_entity $post): bool { return $this->get_first_post_id() === $post->get_id(); } /** * Check if the discussion has started yet. DEFAULTS: true if not set * * @return bool */ public function has_started(): bool { $startime = $this->get_time_start(); return empty($startime) || $startime < time(); } /** * Check if the discussion has ended. DEFAULTS: false if not set * * @return bool */ public function has_ended(): bool { $endtime = $this->get_time_end(); return !empty($endtime) && $endtime < time(); } /** * Check if the discussion belongs to a group. * * @return bool */ public function has_group(): bool { return $this->get_group_id() > 0; } /** * Set the pinned value for this entity * * @param int $targetstate The state to change the pin to * @return bool */ public function set_pinned(int $targetstate): void { if ($targetstate != $this->pinned) { $this->pinned = $targetstate; } } /** * Check if the discussion is timed. * * @return bool */ public function is_timed_discussion(): bool { global $CFG; return !empty($CFG->forum_enabletimedposts) && ($this->get_time_start() || $this->get_time_end()); } /** * Check if the timed discussion is visible. * * @return bool */ public function is_timed_discussion_visible(): bool { return !$this->is_timed_discussion() || ($this->has_started() && !$this->has_ended()); } } PK-B\#(CC!classes/local/entities/author.phpnu[. /** * Author class. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\entities; defined('MOODLE_INTERNAL') || die(); /** * Author class. * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class author { /** @var int $id ID */ private $id; /** @var int $pictureitemid Picture item id */ private $pictureitemid; /** @var string $firstname First name */ private $firstname; /** @var string $lastname Last name */ private $lastname; /** @var string $fullname Full name */ private $fullname; /** @var string $email Email */ private $email; /** @var bool $deleted Deleted */ private $deleted; /** @var string $middlename Middle name */ private $middlename; /** @var string $firstnamephonetic Phonetic spelling of first name */ private $firstnamephonetic; /** @var string $lastnamephonetic Phonetic spelling of last name */ private $lastnamephonetic; /** @var string $alternatename Altername name */ private $alternatename; /** @var string $imagealt Image alt */ private $imagealt; /** * Constructor. * * @param int $id ID * @param int $pictureitemid Picture item id * @param string $firstname First name * @param string $lastname Last name * @param string $fullname Full name * @param string $email Email * @param string|null $middlename Middle name * @param string|null $firstnamephonetic Phonetic spelling of first name * @param string|null $lastnamephonetic Phonetic spelling of last name * @param string|null $alternatename Altername name * @param string|null $imagealt Image alt */ public function __construct( int $id, int $pictureitemid, string $firstname, string $lastname, string $fullname, string $email, bool $deleted, ?string $middlename = null, ?string $firstnamephonetic = null, ?string $lastnamephonetic = null, ?string $alternatename = null, ?string $imagealt = null ) { $this->id = $id; $this->pictureitemid = $pictureitemid; $this->firstname = $firstname; $this->lastname = $lastname; $this->fullname = $fullname; $this->email = $email; $this->deleted = $deleted; $this->middlename = $middlename; $this->firstnamephonetic = $firstnamephonetic; $this->lastnamephonetic = $lastnamephonetic; $this->alternatename = $alternatename; $this->imagealt = $imagealt; } /** * Return the id. * * @return int */ public function get_id(): int { return $this->id; } /** * Return the picture item id. * * @return int */ public function get_picture_item_id(): int { return $this->pictureitemid; } /** * Return the first name. * * @return string */ public function get_first_name(): string { return $this->firstname; } /** * Return the last name. * * @return string */ public function get_last_name(): string { return $this->lastname; } /** * Return the full name. * * @return string */ public function get_full_name(): string { return $this->fullname; } /** * Return the email. * * @return string */ public function get_email(): string { return $this->email; } /** * Is the author deleted? * * @return bool */ public function is_deleted(): bool { return !empty($this->deleted); } /** * Return the middle name. * * @return string|null */ public function get_middle_name(): ?string { return $this->middlename; } /** * Return the first name phonetic. * * @return string|null */ public function get_first_name_phonetic(): ?string { return $this->firstnamephonetic; } /** * Return the last name phonetic. * * @return string|null */ public function get_last_name_phonetic(): ?string { return $this->lastnamephonetic; } /** * Return the alternate name. * * @return string|null */ public function get_alternate_name(): ?string { return $this->alternatename; } /** * Return the image alt. * * @return string|null */ public function get_image_alt(): ?string { return $this->imagealt; } } PK-B\#classes/local/factories/builder.phpnu[. /** * Builder factory. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\factories; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\builders\exported_posts as exported_posts_builder; use mod_forum\local\builders\exported_discussion_summaries as exported_discussion_summaries_builder; use mod_forum\local\builders\exported_discussion as exported_discussion_builder; use mod_forum\local\factories\vault as vault_factory; use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory; use mod_forum\local\factories\exporter as exporter_factory; use mod_forum\local\factories\manager as manager_factory; use renderer_base; /** * Builder factory to construct any builders for forum. * * See: * https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class builder { /** @var legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory */ private $legacydatamapperfactory; /** @var exporter_factory $exporterfactory Exporter factory */ private $exporterfactory; /** @var vault_factory $vaultfactory Vault factory */ private $vaultfactory; /** @var manager_factory $managerfactory Manager factory */ private $managerfactory; /** @var renderer_base $rendererbase Renderer base */ private $rendererbase; /** * Constructor. * * @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory * @param exporter_factory $exporterfactory Exporter factory * @param vault_factory $vaultfactory Vault factory * @param manager_factory $managerfactory Manager factory * @param renderer_base $rendererbase Renderer base */ public function __construct( legacy_data_mapper_factory $legacydatamapperfactory, exporter_factory $exporterfactory, vault_factory $vaultfactory, manager_factory $managerfactory, renderer_base $rendererbase ) { $this->legacydatamapperfactory = $legacydatamapperfactory; $this->exporterfactory = $exporterfactory; $this->vaultfactory = $vaultfactory; $this->managerfactory = $managerfactory; $this->rendererbase = $rendererbase; } /** * Get an instance of the exported posts builder. * * @return exported_posts_builder */ public function get_exported_posts_builder(): exported_posts_builder { return new exported_posts_builder( $this->rendererbase, $this->legacydatamapperfactory, $this->exporterfactory, $this->vaultfactory, $this->managerfactory ); } /** * Get an instance of the exported discussion summaries builder. * * @return exported_discussion_summaries_builder */ public function get_exported_discussion_summaries_builder(): exported_discussion_summaries_builder { return new exported_discussion_summaries_builder( $this->rendererbase, $this->legacydatamapperfactory, $this->exporterfactory, $this->vaultfactory, $this->managerfactory ); } /** * Get an instance of the exported discussion builder. * * @return exported_discussion_summaries_builder */ public function get_exported_discussion_builder(): exported_discussion_builder { return new exported_discussion_builder( $this->rendererbase, $this->legacydatamapperfactory, $this->exporterfactory, $this->vaultfactory, $this->managerfactory->get_rating_manager() ); } } PK-B\CJ">n>n$classes/local/factories/renderer.phpnu[. /** * Renderer factory. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\factories; defined('MOODLE_INTERNAL') || die(); use mod_forum\grades\forum_gradeitem; use mod_forum\local\entities\discussion as discussion_entity; use mod_forum\local\entities\forum as forum_entity; use mod_forum\local\factories\vault as vault_factory; use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory; use mod_forum\local\factories\entity as entity_factory; use mod_forum\local\factories\exporter as exporter_factory; use mod_forum\local\factories\manager as manager_factory; use mod_forum\local\factories\builder as builder_factory; use mod_forum\local\factories\url as url_factory; use mod_forum\local\renderers\discussion as discussion_renderer; use mod_forum\local\renderers\discussion_list as discussion_list_renderer; use mod_forum\local\renderers\posts as posts_renderer; use moodle_page; use core\output\notification; /** * Renderer factory. * * See: * https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class renderer { /** @var legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory */ private $legacydatamapperfactory; /** @var exporter_factory $exporterfactory Exporter factory */ private $exporterfactory; /** @var vault_factory $vaultfactory Vault factory */ private $vaultfactory; /** @var manager_factory $managerfactory Manager factory */ private $managerfactory; /** @var entity_factory $entityfactory Entity factory */ private $entityfactory; /** @var builder_factory $builderfactory Builder factory */ private $builderfactory; /** @var url_factory $urlfactory URL factory */ private $urlfactory; /** @var renderer_base $rendererbase Renderer base */ private $rendererbase; /** @var moodle_page $page Moodle page */ private $page; /** * Constructor. * * @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory * @param exporter_factory $exporterfactory Exporter factory * @param vault_factory $vaultfactory Vault factory * @param manager_factory $managerfactory Manager factory * @param entity_factory $entityfactory Entity factory * @param builder_factory $builderfactory Builder factory * @param url_factory $urlfactory URL factory * @param moodle_page $page Moodle page */ public function __construct( legacy_data_mapper_factory $legacydatamapperfactory, exporter_factory $exporterfactory, vault_factory $vaultfactory, manager_factory $managerfactory, entity_factory $entityfactory, builder_factory $builderfactory, url_factory $urlfactory, moodle_page $page ) { $this->legacydatamapperfactory = $legacydatamapperfactory; $this->exporterfactory = $exporterfactory; $this->vaultfactory = $vaultfactory; $this->managerfactory = $managerfactory; $this->entityfactory = $entityfactory; $this->builderfactory = $builderfactory; $this->urlfactory = $urlfactory; $this->page = $page; $this->rendererbase = $page->get_renderer('mod_forum'); } /** * Create a discussion renderer for the given forum and discussion. * * @param forum_entity $forum Forum the discussion belongs to * @param discussion_entity $discussion Discussion to render * @param int $displaymode How should the posts be formatted? * @return discussion_renderer */ public function get_discussion_renderer( forum_entity $forum, discussion_entity $discussion, int $displaymode ): discussion_renderer { $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $ratingmanager = $this->managerfactory->get_rating_manager(); $rendererbase = $this->rendererbase; $baseurl = $this->urlfactory->get_discussion_view_url_from_discussion($discussion); $notifications = []; return new discussion_renderer( $forum, $discussion, $displaymode, $rendererbase, $this->get_single_discussion_posts_renderer($displaymode, false), $this->page, $this->legacydatamapperfactory, $this->exporterfactory, $this->vaultfactory, $this->urlfactory, $this->entityfactory, $capabilitymanager, $ratingmanager, $this->entityfactory->get_exported_posts_sorter(), $baseurl, $notifications, function($discussion, $user, $forum) { $exportbuilder = $this->builderfactory->get_exported_discussion_builder(); return $exportbuilder->build( $user, $forum, $discussion ); } ); } /** * Create a posts renderer to render posts without defined parent/reply relationships. * * @return posts_renderer */ public function get_posts_renderer(): posts_renderer { return new posts_renderer( $this->rendererbase, $this->builderfactory->get_exported_posts_builder(), 'mod_forum/forum_discussion_posts' ); } /** * Create a posts renderer to render a list of posts in a single discussion. * * @param int|null $displaymode How should the posts be formatted? * @param bool $readonly Should the posts include the actions to reply, delete, etc? * @return posts_renderer */ public function get_single_discussion_posts_renderer(?int $displaymode = null, bool $readonly = false): posts_renderer { $exportedpostssorter = $this->entityfactory->get_exported_posts_sorter(); switch ($displaymode) { case FORUM_MODE_THREADED: $template = 'mod_forum/forum_discussion_threaded_posts'; break; case FORUM_MODE_NESTED: $template = 'mod_forum/forum_discussion_nested_posts'; break; case FORUM_MODE_NESTED_V2: $template = 'mod_forum/forum_discussion_nested_v2_posts'; break; default; $template = 'mod_forum/forum_discussion_posts'; break; } return new posts_renderer( $this->rendererbase, $this->builderfactory->get_exported_posts_builder(), $template, // Post process the exported posts for our template. This function will add the "replies" // and "hasreplies" properties to the exported posts. It will also sort them into the // reply tree structure if the display mode requires it. function($exportedposts, $forums, $discussions) use ($displaymode, $readonly, $exportedpostssorter) { $forum = array_shift($forums); $seenfirstunread = false; $postcount = count($exportedposts); $discussionsbyid = array_reduce($discussions, function($carry, $discussion) { $carry[$discussion->get_id()] = $discussion; return $carry; }, []); $exportedposts = array_map( function($exportedpost) use ($forum, $discussionsbyid, $readonly, $seenfirstunread, $displaymode) { $discussion = $discussionsbyid[$exportedpost->discussionid] ?? null; if ($forum->get_type() == 'single' && !$exportedpost->hasparent) { // Remove the author from any posts that don't have a parent. unset($exportedpost->author); unset($exportedpost->html['authorsubheading']); } $exportedpost->firstpost = false; $exportedpost->readonly = $readonly; $exportedpost->hasreplycount = false; $exportedpost->hasreplies = false; $exportedpost->replies = []; $exportedpost->discussionlocked = $discussion ? $discussion->is_locked() : null; $exportedpost->isfirstunread = false; if (!$seenfirstunread && $exportedpost->unread) { $exportedpost->isfirstunread = true; $seenfirstunread = true; } if ($displaymode === FORUM_MODE_NESTED_V2) { $exportedpost->showactionmenu = $exportedpost->capabilities['view'] || $exportedpost->capabilities['controlreadstatus'] || $exportedpost->capabilities['edit'] || $exportedpost->capabilities['split'] || $exportedpost->capabilities['delete'] || $exportedpost->capabilities['export'] || !empty($exportedpost->urls['viewparent']); } return $exportedpost; }, $exportedposts ); if ( $displaymode === FORUM_MODE_NESTED || $displaymode === FORUM_MODE_THREADED || $displaymode === FORUM_MODE_NESTED_V2 ) { $sortedposts = $exportedpostssorter->sort_into_children($exportedposts); $sortintoreplies = function($nestedposts) use (&$sortintoreplies) { return array_map(function($postdata) use (&$sortintoreplies) { [$post, $replies] = $postdata; $totalreplycount = 0; if (empty($replies)) { $post->replies = []; $post->hasreplies = false; } else { $sortedreplies = $sortintoreplies($replies); // Set the parent author name on the replies. This is used for screen // readers to help them identify the structure of the discussion. $sortedreplies = array_map(function($reply) use ($post) { if (isset($post->author)) { $reply->parentauthorname = $post->author->fullname; } else { // The only time the author won't be set is for a single discussion // forum. See above for where it gets unset. $reply->parentauthorname = get_string('firstpost', 'mod_forum'); } return $reply; }, $sortedreplies); $totalreplycount = array_reduce($sortedreplies, function($carry, $reply) { return $carry + 1 + $reply->totalreplycount; }, $totalreplycount); $post->replies = $sortedreplies; $post->hasreplies = true; } $post->totalreplycount = $totalreplycount; return $post; }, $nestedposts); }; // Set the "replies" property on the exported posts. $exportedposts = $sortintoreplies($sortedposts); } else if ($displaymode === FORUM_MODE_FLATNEWEST || $displaymode === FORUM_MODE_FLATOLDEST) { $exportedfirstpost = array_shift($exportedposts); $exportedfirstpost->replies = $exportedposts; $exportedfirstpost->hasreplies = true; $exportedposts = [$exportedfirstpost]; } if (!empty($exportedposts)) { // Need to identify the first post so that we can use it in behat tests. $exportedposts[0]->firstpost = true; $exportedposts[0]->hasreplycount = true; $exportedposts[0]->replycount = $postcount - 1; } return $exportedposts; } ); } /** * Create a posts renderer to render posts in the forum search results. * * @param string[] $searchterms The search terms to be highlighted in the posts * @return posts_renderer */ public function get_posts_search_results_renderer(array $searchterms): posts_renderer { $urlfactory = $this->urlfactory; return new posts_renderer( $this->rendererbase, $this->builderfactory->get_exported_posts_builder(), 'mod_forum/forum_search_results', // Post process the exported posts to add the highlighting of the search terms to the post // and also the additional context links in the subject. function($exportedposts, $forumsbyid, $discussionsbyid) use ($searchterms, $urlfactory) { $highlightwords = implode(' ', $searchterms); return array_map( function($exportedpost) use ( $forumsbyid, $discussionsbyid, $searchterms, $highlightwords, $urlfactory ) { $discussion = $discussionsbyid[$exportedpost->discussionid]; $forum = $forumsbyid[$discussion->get_forum_id()]; $viewdiscussionurl = $urlfactory->get_discussion_view_url_from_discussion($discussion); $exportedpost->urls['viewforum'] = $urlfactory->get_forum_view_url_from_forum($forum)->out(false); $exportedpost->urls['viewdiscussion'] = $viewdiscussionurl->out(false); $exportedpost->subject = highlight($highlightwords, $exportedpost->subject); $exportedpost->forumname = format_string($forum->get_name(), true); $exportedpost->discussionname = highlight($highlightwords, format_string($discussion->get_name(), true)); $exportedpost->showdiscussionname = $forum->get_type() != 'single'; // Identify search terms only found in HTML markup, and add a warning about them to // the start of the message text. This logic was copied exactly as is from the previous // implementation. $missingterms = ''; $exportedpost->message = highlight( $highlightwords, $exportedpost->message, 0, '', '' ); foreach ($searchterms as $searchterm) { if ( preg_match("/$searchterm/i", $exportedpost->message) && !preg_match('/' . $searchterm . '<\/fgw9sdpq4>/i', $exportedpost->message) ) { $missingterms .= " $searchterm"; } } $exportedpost->message = str_replace('', '', $exportedpost->message); $exportedpost->message = str_replace('', '', $exportedpost->message); if ($missingterms) { $strmissingsearchterms = get_string('missingsearchterms', 'forum'); $exportedpost->message = '

' . $strmissingsearchterms . ' ' . $missingterms . '

' . $exportedpost->message; } return $exportedpost; }, $exportedposts ); } ); } /** * Create a posts renderer to render posts in mod/forum/user.php. * * @param bool $addlinkstocontext Should links to the course, forum, and discussion be included? * @return posts_renderer */ public function get_user_forum_posts_report_renderer(bool $addlinkstocontext): posts_renderer { $urlfactory = $this->urlfactory; return new posts_renderer( $this->rendererbase, $this->builderfactory->get_exported_posts_builder(), 'mod_forum/forum_posts_with_context_links', function($exportedposts, $forumsbyid, $discussionsbyid) use ($urlfactory, $addlinkstocontext) { return array_map(function($exportedpost) use ($forumsbyid, $discussionsbyid, $addlinkstocontext, $urlfactory) { $discussion = $discussionsbyid[$exportedpost->discussionid]; $forum = $forumsbyid[$discussion->get_forum_id()]; $courserecord = $forum->get_course_record(); if ($addlinkstocontext) { $viewdiscussionurl = $urlfactory->get_discussion_view_url_from_discussion($discussion); $exportedpost->urls['viewforum'] = $urlfactory->get_forum_view_url_from_forum($forum)->out(false); $exportedpost->urls['viewdiscussion'] = $viewdiscussionurl->out(false); $exportedpost->urls['viewcourse'] = $urlfactory->get_course_url_from_forum($forum)->out(false); } $exportedpost->forumname = format_string($forum->get_name(), true); $exportedpost->discussionname = format_string($discussion->get_name(), true); $exportedpost->coursename = format_string($courserecord->shortname, true); $exportedpost->showdiscussionname = $forum->get_type() != 'single'; return $exportedpost; }, $exportedposts); } ); } /** * Create a standard type discussion list renderer. * * @param forum_entity $forum The forum that the discussions belong to * @return discussion_list_renderer */ public function get_discussion_list_renderer( forum_entity $forum ): discussion_list_renderer { $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $rendererbase = $this->rendererbase; $notifications = []; switch ($forum->get_type()) { case 'news': if (SITEID == $forum->get_course_id()) { $template = 'mod_forum/frontpage_news_discussion_list'; } else { $template = 'mod_forum/news_discussion_list'; } break; case 'qanda': $template = 'mod_forum/qanda_discussion_list'; break; default: $template = 'mod_forum/discussion_list'; } return new discussion_list_renderer( $forum, $rendererbase, $this->legacydatamapperfactory, $this->exporterfactory, $this->vaultfactory, $this->builderfactory, $capabilitymanager, $this->urlfactory, forum_gradeitem::load_from_forum_entity($forum), $template, $notifications, function($discussions, $user, $forum) { $exporteddiscussionsummarybuilder = $this->builderfactory->get_exported_discussion_summaries_builder(); return $exporteddiscussionsummarybuilder->build( $user, $forum, $discussions ); } ); } /** * Create a discussion list renderer which shows more information about the first post. * * @param forum_entity $forum The forum that the discussions belong to * @param string $template The template to use * @return discussion_list_renderer */ private function get_detailed_discussion_list_renderer( forum_entity $forum, string $template ): discussion_list_renderer { $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $rendererbase = $this->rendererbase; $notifications = []; return new discussion_list_renderer( $forum, $rendererbase, $this->legacydatamapperfactory, $this->exporterfactory, $this->vaultfactory, $this->builderfactory, $capabilitymanager, $this->urlfactory, forum_gradeitem::load_from_forum_entity($forum), $template, $notifications, function($discussions, $user, $forum) use ($capabilitymanager) { $exportedpostsbuilder = $this->builderfactory->get_exported_posts_builder(); $discussionentries = []; $postentries = []; foreach ($discussions as $discussion) { $discussionentries[] = $discussion->get_discussion(); $discussionentriesids[] = $discussion->get_discussion()->get_id(); $postentries[] = $discussion->get_first_post(); } $exportedposts['posts'] = $exportedpostsbuilder->build( $user, [$forum], $discussionentries, $postentries ); $postvault = $this->vaultfactory->get_post_vault(); $canseeanyprivatereply = $capabilitymanager->can_view_any_private_reply($user); $discussionrepliescount = $postvault->get_reply_count_for_discussion_ids( $user, $discussionentriesids, $canseeanyprivatereply ); $forumdatamapper = $this->legacydatamapperfactory->get_forum_data_mapper(); $forumrecord = $forumdatamapper->to_legacy_object($forum); if (forum_tp_is_tracked($forumrecord, $user)) { $discussionunreadscount = $postvault->get_unread_count_for_discussion_ids( $user, $discussionentriesids, $canseeanyprivatereply ); } else { $discussionunreadscount = []; } array_walk($exportedposts['posts'], function($post) use ($discussionrepliescount, $discussionunreadscount) { $post->discussionrepliescount = $discussionrepliescount[$post->discussionid] ?? 0; $post->discussionunreadscount = $discussionunreadscount[$post->discussionid] ?? 0; // TODO: Find a better solution due to language differences when defining the singular and plural form. $post->isreplyplural = $post->discussionrepliescount != 1 ? true : false; $post->isunreadplural = $post->discussionunreadscount != 1 ? true : false; }); $exportedposts['state']['hasdiscussions'] = $exportedposts['posts'] ? true : false; return $exportedposts; } ); } /** * Create a blog type discussion list renderer. * * @param forum_entity $forum The forum that the discussions belong to * @return discussion_list_renderer */ public function get_blog_discussion_list_renderer( forum_entity $forum ): discussion_list_renderer { return $this->get_detailed_discussion_list_renderer($forum, 'mod_forum/blog_discussion_list'); } /** * Create a discussion list renderer for the social course format. * * @param forum_entity $forum The forum that the discussions belong to * @return discussion_list_renderer */ public function get_social_discussion_list_renderer( forum_entity $forum ): discussion_list_renderer { return $this->get_detailed_discussion_list_renderer($forum, 'mod_forum/social_discussion_list'); } /** * Create a discussion list renderer for the social course format. * * @param forum_entity $forum The forum that the discussions belong to * @return discussion_list_renderer */ public function get_frontpage_news_discussion_list_renderer( forum_entity $forum ): discussion_list_renderer { return $this->get_detailed_discussion_list_renderer($forum, 'mod_forum/frontpage_social_discussion_list'); } /** * Create a single type discussion list renderer. * * @param forum_entity $forum Forum the discussion belongs to * @param discussion_entity $discussion The discussion entity * @param bool $hasmultiplediscussions Whether the forum has multiple discussions (more than one) * @param int $displaymode How should the posts be formatted? * @return discussion_renderer */ public function get_single_discussion_list_renderer( forum_entity $forum, discussion_entity $discussion, bool $hasmultiplediscussions, int $displaymode ): discussion_renderer { $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $ratingmanager = $this->managerfactory->get_rating_manager(); $rendererbase = $this->rendererbase; $cmid = $forum->get_course_module_record()->id; $baseurl = $this->urlfactory->get_forum_view_url_from_course_module_id($cmid); $notifications = array(); if ($hasmultiplediscussions) { $notifications[] = (new notification(get_string('warnformorepost', 'forum'))) ->set_show_closebutton(true); } return new discussion_renderer( $forum, $discussion, $displaymode, $rendererbase, $this->get_single_discussion_posts_renderer($displaymode, false), $this->page, $this->legacydatamapperfactory, $this->exporterfactory, $this->vaultfactory, $this->urlfactory, $this->entityfactory, $capabilitymanager, $ratingmanager, $this->entityfactory->get_exported_posts_sorter(), $baseurl, $notifications ); } } PK-B\iCv"""classes/local/factories/entity.phpnu[. /** * Entity factory. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\factories; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\author as author_entity; use mod_forum\local\entities\discussion as discussion_entity; use mod_forum\local\entities\discussion_summary as discussion_summary_entity; use mod_forum\local\entities\forum as forum_entity; use mod_forum\local\entities\post as post_entity; use mod_forum\local\entities\post_read_receipt_collection as post_read_receipt_collection_entity; use mod_forum\local\entities\sorter as sorter_entity; use stdClass; use context; use cm_info; use user_picture; use moodle_url; /** * Entity factory to create the forum entities. * * See: * https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class entity { /** * Create a forum entity from a stdClass (legacy forum object). * * @param stdClass $record The forum record * @param context $context The forum module context * @param stdClass $coursemodule Course module record for the forum * @param stdClass $course Course the forum belongs to * @return forum_entity */ public function get_forum_from_stdclass( stdClass $record, context $context, stdClass $coursemodule, stdClass $course ): forum_entity { // Note: cm_info::create loads a cm_info in the context of the current user which // creates hidden dependency on the logged in user (very bad) however it's the best // option to load some data we need which doesn't require the logged in user. // Only use properties which do not require the logged in user. $cm = \cm_info::create($coursemodule); return new forum_entity( $context, $coursemodule, $course, // This property is a general module property that isn't affected by the logged in user. $cm->effectivegroupmode, $record->id, $record->course, $record->type, $record->name, $record->intro, $record->introformat, $record->assessed, $record->assesstimestart, $record->assesstimefinish, $record->scale, $record->grade_forum, $record->grade_forum_notify, $record->maxbytes, $record->maxattachments, $record->forcesubscribe, $record->trackingtype, $record->rsstype, $record->rssarticles, $record->timemodified, $record->warnafter, $record->blockafter, $record->blockperiod, $record->completiondiscussions, $record->completionreplies, $record->completionposts, $record->displaywordcount, $record->lockdiscussionafter, $record->duedate, $record->cutoffdate ); } /** * Create a discussion entity from an stdClass (legacy dicussion object). * * @param stdClass $record Discussion record * @return discussion_entity */ public function get_discussion_from_stdclass(stdClass $record): discussion_entity { return new discussion_entity( $record->id, $record->course, $record->forum, $record->name, $record->firstpost, $record->userid, $record->groupid, $record->assessed, $record->timemodified, $record->usermodified, $record->timestart, $record->timeend, $record->pinned, $record->timelocked ); } /** * Create a post entity from an stdClass (legacy post object). * * @param stdClass $record The post record * @return post_entity */ public function get_post_from_stdclass(stdClass $record): post_entity { return new post_entity( $record->id, $record->discussion, $record->parent, $record->userid, $record->created, $record->modified, $record->mailed, $record->subject, $record->message, $record->messageformat, $record->messagetrust, $record->attachment, $record->totalscore, $record->mailnow, $record->deleted, $record->privatereplyto, $record->wordcount, $record->charcount ); } /** * Create an author entity from a user record. * * @param stdClass $record The user record * @return author_entity */ public function get_author_from_stdclass(stdClass $record): author_entity { return new author_entity( $record->id, $record->picture, $record->firstname, $record->lastname, fullname($record), $record->email, $record->deleted, $record->middlename, $record->firstnamephonetic, $record->lastnamephonetic, $record->alternatename, $record->imagealt ); } /** * Create a discussion summary enttiy from stdClasses. * * @param stdClass $discussion The discussion record * @param stdClass $firstpost A post record for the first post in the discussion * @param stdClass $firstpostauthor A user record for the author of the first post * @param stdClass $latestpostauthor A user record for the author of the latest post in the discussion * @return discussion_summary_entity */ public function get_discussion_summary_from_stdclass( stdClass $discussion, stdClass $firstpost, stdClass $firstpostauthor, stdClass $latestpostauthor ): discussion_summary_entity { $firstpostauthorentity = $this->get_author_from_stdclass($firstpostauthor); return new discussion_summary_entity( $this->get_discussion_from_stdclass($discussion), $this->get_post_from_stdclass($firstpost, $firstpostauthorentity), $firstpostauthorentity, $this->get_author_from_stdclass($latestpostauthor) ); } /** * Create a post read receipt collection entity from a list of read receipt records. * * @param array $records A list of read receipt records. * @return post_read_receipt_collection_entity */ public function get_post_read_receipt_collection_from_stdclasses(array $records): post_read_receipt_collection_entity { return new post_read_receipt_collection_entity($records); } /** * Create a sorter entity to sort post entities. * * @return sorter_entity */ public function get_posts_sorter(): sorter_entity { return new sorter_entity( // Get id function for a post_entity. function(post_entity $post) { return $post->get_id(); }, // Get parent id function for a post_entity. function(post_entity $post) { return $post->get_parent_id(); } ); } /** * Create a sorter entity to sort exported posts. * * @return sorter_entity */ public function get_exported_posts_sorter(): sorter_entity { return new sorter_entity( // Get id function for an exported post. function(stdClass $post) { return $post->id; }, // Get parent id function for an exported post. function(stdClass $post) { return $post->parentid; } ); } } PK-B\]K!classes/local/factories/vault.phpnu[. /** * Vault factory. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\factories; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\factories\entity as entity_factory; use mod_forum\local\vaults\author as author_vault; use mod_forum\local\vaults\discussion as discussion_vault; use mod_forum\local\vaults\discussion_list as discussion_list_vault; use mod_forum\local\vaults\forum as forum_vault; use mod_forum\local\vaults\post as post_vault; use mod_forum\local\vaults\post_attachment as post_attachment_vault; use mod_forum\local\vaults\post_read_receipt_collection as post_read_receipt_collection_vault; use file_storage; use moodle_database; /** * Vault factory. * * See: * https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class vault { /** @var entity_factory $entityfactory Entity factory */ private $entityfactory; /** @var legacy_data_mapper $legacymapper Entity factory */ private $legacymapper; /** @var moodle_database $db A moodle database */ private $db; /** @var file_storage $filestorage A file storage instance */ private $filestorage; /** * Constructor. * * @param moodle_database $db A moodle database * @param entity_factory $entityfactory Entity factory * @param file_storage $filestorage A file storage instance * @param legacy_data_mapper $legacyfactory Datamapper */ public function __construct(moodle_database $db, entity_factory $entityfactory, file_storage $filestorage, legacy_data_mapper $legacyfactory) { $this->db = $db; $this->entityfactory = $entityfactory; $this->filestorage = $filestorage; $this->legacymapper = $legacyfactory; } /** * Create a forum vault. * * @return forum_vault */ public function get_forum_vault(): forum_vault { return new forum_vault( $this->db, $this->entityfactory, $this->legacymapper->get_legacy_data_mapper_for_vault('forum') ); } /** * Create a discussion vault. * * @return discussion_vault */ public function get_discussion_vault(): discussion_vault { return new discussion_vault( $this->db, $this->entityfactory, $this->legacymapper->get_legacy_data_mapper_for_vault('discussion') ); } /** * Create a discussion list vault. * * @return discussion_list_vault */ public function get_discussions_in_forum_vault(): discussion_list_vault { return new discussion_list_vault( $this->db, $this->entityfactory, $this->legacymapper->get_legacy_data_mapper_for_vault('discussion') ); } /** * Create a post vault. * * @return post_vault */ public function get_post_vault(): post_vault { return new post_vault( $this->db, $this->entityfactory, $this->legacymapper->get_legacy_data_mapper_for_vault('post') ); } /** * Create an author vault. * * @return author_vault */ public function get_author_vault(): author_vault { return new author_vault( $this->db, $this->entityfactory, $this->legacymapper->get_legacy_data_mapper_for_vault('author') ); } /** * Create a post read receipt collection vault. * * @return post_read_receipt_collection_vault */ public function get_post_read_receipt_collection_vault(): post_read_receipt_collection_vault { return new post_read_receipt_collection_vault( $this->db, $this->entityfactory, $this->legacymapper->get_legacy_data_mapper_for_vault('post') ); } /** * Create a post attachment vault. * * @return post_attachment_vault */ public function get_post_attachment_vault(): post_attachment_vault { return new post_attachment_vault( $this->filestorage ); } } PK-B\,,$classes/local/factories/exporter.phpnu[. /** * Forum Exporter factory. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\factories; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\discussion as discussion_entity; use mod_forum\local\entities\forum as forum_entity; use mod_forum\local\entities\post as post_entity; use mod_forum\local\entities\post_read_receipt_collection as post_read_receipt_collection_entity; use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory; use mod_forum\local\factories\manager as manager_factory; use mod_forum\local\factories\url as url_factory; use mod_forum\local\factories\vault as vault_factory; use mod_forum\local\exporters\forum as forum_exporter; use mod_forum\local\exporters\discussion as discussion_exporter; use mod_forum\local\exporters\discussion_summaries as discussion_summaries_exporter; use mod_forum\local\exporters\post as post_exporter; use mod_forum\local\exporters\posts as posts_exporter; use context; use rating; use stdClass; /** * The exporter factory class used to fetch an instance of the different exporter types. * * See: * https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class exporter { /** @var legacy_data_mapper_factory The factory to fetch a legacy data mapper */ private $legacydatamapperfactory; /** @var manager_factory The factory to fetch a new manager */ private $managerfactory; /** @var url_factory The factory to create urls */ private $urlfactory; /** @var vault_factory The vault factory */ private $vaultfactory; /** * Constructor for the exporter factory. * * @param legacy_data_mapper_factory $legacydatamapperfactory The factory to fetch a legacy data mapper instance * @param manager_factory $managerfactory The factory fo fetch a manager instance * @param url_factory $urlfactory The factory to create urls * @param vault_factory $vaultfactory The vault factory */ public function __construct(legacy_data_mapper_factory $legacydatamapperfactory, manager_factory $managerfactory, url_factory $urlfactory, vault_factory $vaultfactory) { $this->legacydatamapperfactory = $legacydatamapperfactory; $this->managerfactory = $managerfactory; $this->urlfactory = $urlfactory; $this->vaultfactory = $vaultfactory; } /** * Construct a new forum exporter for the specified user and forum. * * @param stdClass $user The user viewing the forum * @param forum_entity $forum The forum being viewed * @param int $currentgroup The group currently being viewed * @return forum_exporter */ public function get_forum_exporter( stdClass $user, forum_entity $forum, ?int $currentgroup ): forum_exporter { return new forum_exporter($forum, [ 'legacydatamapperfactory' => $this->legacydatamapperfactory, 'capabilitymanager' => $this->managerfactory->get_capability_manager($forum), 'urlfactory' => $this->urlfactory, 'user' => $user, 'currentgroup' => $currentgroup, 'vaultfactory' => $this->vaultfactory, ]); } /** * Fetch the structure of the forum exporter. * * @return array */ public static function get_forum_export_structure(): array { return forum_exporter::read_properties_definition(); } /** * Construct a new discussion exporter for the specified user and forum discussion. * * @param stdClass $user The user viewing the forum * @param forum_entity $forum The forum being viewed * @param discussion_entity $discussion The discussion being viewed * @param stdClass[] $groupsbyid The list of groups in the forum * @return discussion_exporter */ public function get_discussion_exporter( stdClass $user, forum_entity $forum, discussion_entity $discussion, array $groupsbyid = [], array $favouriteids = [] ): discussion_exporter { return new discussion_exporter($discussion, [ 'context' => $forum->get_context(), 'forum' => $forum, 'capabilitymanager' => $this->managerfactory->get_capability_manager($forum), 'urlfactory' => $this->urlfactory, 'user' => $user, 'legacydatamapperfactory' => $this->legacydatamapperfactory, 'latestpostid' => null, 'groupsbyid' => $groupsbyid, 'favouriteids' => $favouriteids ]); } /** * Fetch the structure of the discussion exporter. * * @return array */ public static function get_discussion_export_structure() { return discussion_exporter::read_properties_definition(); } /** * Construct a new discussion summaries exporter for the specified user and set of discussions. * * @param stdClass $user The user viewing the forum * @param forum_entity $forum The forum being viewed * @param discussion_summary_entity[] $discussions The set of discussion summaries to export * @param stdClass[] $groupsbyauthorid The set of groups in an associative array for each author * @param stdClass[] $groupsbyid The set of groups in the forum in an associative array for each group * @param int[] $discussionreplycount The number of replies for each discussion * @param int[] $discussionunreadcount The number of unread posts for each discussion * @param int[] $latestpostids The latest post id for each discussion * @param int[] $postauthorcontextids The context ids for the first and last post authors (indexed by author id) * @param int[] $favourites The list of discussion ids that have been favourited * @return discussion_summaries_exporter */ public function get_discussion_summaries_exporter( stdClass $user, forum_entity $forum, array $discussions, array $groupsbyid = [], array $groupsbyauthorid = [], array $discussionreplycount = [], array $discussionunreadcount = [], array $latestpostids = [], array $postauthorcontextids = [], array $favourites = [], array $latestauthors = [] ): discussion_summaries_exporter { return new discussion_summaries_exporter( $discussions, $groupsbyid, $groupsbyauthorid, $discussionreplycount, $discussionunreadcount, $latestpostids, $postauthorcontextids, [ 'legacydatamapperfactory' => $this->legacydatamapperfactory, 'context' => $forum->get_context(), 'forum' => $forum, 'capabilitymanager' => $this->managerfactory->get_capability_manager($forum), 'urlfactory' => $this->urlfactory, 'user' => $user, 'favouriteids' => $favourites, 'latestauthors' => $latestauthors ] ); } /** * Fetch the structure of the discussion summaries exporter. * * @return array */ public static function get_discussion_summaries_export_structure() { return discussion_summaries_exporter::read_properties_definition(); } /** * Construct a new post exporter for the specified user and set of post. * * @param stdClass $user The user viewing the forum * @param forum_entity $forum The forum being viewed * @param discussion_entity $discussion The discussion that the post is in * @param post_entity[] $posts The set of posts to be exported * @param author_entity[] $authorsbyid List of authors indexed by author id * @param int[] $authorcontextids List of authors context ids indexed by author id * @param array $attachmentsbypostid List of attachments for each post indexed by post id * @param array $groupsbyauthorid List of groups for the post authors indexed by author id * @param post_read_receipt_collection_entity|null $readreceiptcollection Details of read receipts for each post * @param array $tagsbypostid List of tags for each post indexed by post id * @param rating[] $ratingbypostid List of ratings for each post indexed by post id * @param bool $includehtml Include some pre-constructed HTML in the export * @param array $inlineattachmentsbypostid List of attachments for each post indexed by post id * @return posts_exporter */ public function get_posts_exporter( stdClass $user, forum_entity $forum, discussion_entity $discussion, array $posts, array $authorsbyid = [], array $authorcontextids = [], array $attachmentsbypostid = [], array $groupsbyauthorid = [], ?post_read_receipt_collection_entity $readreceiptcollection = null, array $tagsbypostid = [], array $ratingbypostid = [], bool $includehtml = false, array $inlineattachmentsbypostid = [] ): posts_exporter { return new posts_exporter( $posts, $authorsbyid, $authorcontextids, $attachmentsbypostid, $groupsbyauthorid, $tagsbypostid, $ratingbypostid, [ 'capabilitymanager' => $this->managerfactory->get_capability_manager($forum), 'urlfactory' => $this->urlfactory, 'forum' => $forum, 'discussion' => $discussion, 'user' => $user, 'context' => $forum->get_context(), 'readreceiptcollection' => $readreceiptcollection, 'includehtml' => $includehtml ], $inlineattachmentsbypostid ); } /** * Fetch the structure of the posts exporter. * * @return array */ public static function get_posts_export_structure() { return posts_exporter::read_properties_definition(); } } PK-B\;K .classes/local/factories/legacy_data_mapper.phpnu[. /** * Legacy data mapper factory. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\factories; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\data_mappers\legacy\author as author_data_mapper; use mod_forum\local\data_mappers\legacy\discussion as discussion_data_mapper; use mod_forum\local\data_mappers\legacy\forum as forum_data_mapper; use mod_forum\local\data_mappers\legacy\post as post_data_mapper; use mod_forum\local\entities\forum; /** * Legacy data mapper factory. * * See: * https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class legacy_data_mapper { /** * Create a legacy forum data mapper. * * @return forum_data_mapper */ public function get_forum_data_mapper(): forum_data_mapper { return new forum_data_mapper(); } /** * Create a legacy discussion data mapper. * * @return discussion_data_mapper */ public function get_discussion_data_mapper(): discussion_data_mapper { return new discussion_data_mapper(); } /** * Create a legacy post data mapper. * * @return post_data_mapper */ public function get_post_data_mapper(): post_data_mapper { return new post_data_mapper(); } /** * Create a legacy author data mapper. * * @return author_data_mapper */ public function get_author_data_mapper(): author_data_mapper { return new author_data_mapper(); } /** * Get the corresponding entity based on the supplied value * * @param string $entity * @return author_data_mapper|discussion_data_mapper|forum_data_mapper|post_data_mapper */ public function get_legacy_data_mapper_for_vault($entity) { switch($entity) { case 'forum': return $this->get_forum_data_mapper(); case 'discussion': return $this->get_discussion_data_mapper(); case 'post': return $this->get_post_data_mapper(); case 'author': return $this->get_author_data_mapper(); } } } PK-B\SY #classes/local/factories/manager.phpnu[. /** * Managers factory. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\factories; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/rating/lib.php'); use mod_forum\local\entities\forum as forum_entity; use mod_forum\local\managers\capability as capability_manager; use rating_manager; /** * Managers factory. * * See: * https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class manager { /** @var legacy_data_mapper $legacydatamapperfactory Legacy data mapper factory */ private $legacydatamapperfactory; /** * Constructor. * * @param legacy_data_mapper $legacydatamapperfactory Legacy data mapper factory */ public function __construct(legacy_data_mapper $legacydatamapperfactory) { $this->legacydatamapperfactory = $legacydatamapperfactory; } /** * Create a capability manager for the given forum. * * @param forum_entity $forum The forum to manage capabilities for * @return capability_manager */ public function get_capability_manager(forum_entity $forum) { return new capability_manager( $forum, $this->legacydatamapperfactory->get_forum_data_mapper(), $this->legacydatamapperfactory->get_discussion_data_mapper(), $this->legacydatamapperfactory->get_post_data_mapper() ); } /** * Create a rating manager. * * @return rating_manager */ public function get_rating_manager(): rating_manager { return new rating_manager(); } } PK-B\CAAclasses/local/factories/url.phpnu[. /** * A URL factory for the forum. * * @package mod_forum * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\factories; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\author as author_entity; use mod_forum\local\entities\forum as forum_entity; use mod_forum\local\entities\discussion as discussion_entity; use mod_forum\local\entities\post as post_entity; use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory; use moodle_url; use stored_file; use user_picture; require_once($CFG->dirroot . '/mod/forum/lib.php'); /** * A URL factory for the forum. * * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class url { /** @var legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory */ private $legacydatamapperfactory; /** * Constructor. * * @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory */ public function __construct(legacy_data_mapper_factory $legacydatamapperfactory) { $this->legacydatamapperfactory = $legacydatamapperfactory; } /** * Get the course url from the given course id. * * @param int $courseid The course id * @return moodle_url */ public function get_course_url_from_courseid(int $courseid): moodle_url { return new moodle_url('/course/view.php', [ 'id' => $courseid, ]); } /** * Get the course url from the given forum entity. * * @param forum_entity $forum The forum entity * @return moodle_url */ public function get_course_url_from_forum(forum_entity $forum): moodle_url { return $this->get_course_url_from_courseid($forum->get_course_id()); } /** * Get the create discussion url for the given forum. * * @param forum_entity $forum The forum entity * @return moodle_url */ public function get_discussion_create_url(forum_entity $forum): moodle_url { return new moodle_url('/mod/forum/post.php', [ 'forum' => $forum->get_id(), ]); } /** * Get the view forum url for the given forum and optionally a page number. * * @param forum_entity $forum The forum entity * @param int|null $pageno The page number * @param int|null $sortorder The sorting order * @return moodle_url */ public function get_forum_view_url_from_forum(forum_entity $forum, ?int $pageno = null, ?int $sortorder = null): moodle_url { return $this->get_forum_view_url_from_course_module_id($forum->get_course_module_record()->id, $pageno, $sortorder); } /** * Get the view forum url for the given course module id and optionally a page number. * * @param int $coursemoduleid The course module id * @param int|null $pageno The page number * @param int|null $sortorder The sorting order * @return moodle_url */ public function get_forum_view_url_from_course_module_id(int $coursemoduleid, ?int $pageno = null, ?int $sortorder = null): moodle_url { $url = new moodle_url('/mod/forum/view.php', [ 'id' => $coursemoduleid, ]); if (null !== $pageno) { $url->param('p', $pageno); } if (null !== $sortorder) { $url->param('o', $sortorder); } return $url; } /** * Get the view discussion url from the given discussion id. * * @param int $discussionid The discussion id * @return moodle_url */ public function get_discussion_view_url_from_discussion_id(int $discussionid): moodle_url { return new moodle_url('/mod/forum/discuss.php', [ 'd' => $discussionid ]); } /** * Get the view discussion url from the given discussion. * * @param discussion_entity $discussion The discussion * @return moodle_url */ public function get_discussion_view_url_from_discussion(discussion_entity $discussion): moodle_url { return $this->get_discussion_view_url_from_discussion_id($discussion->get_id()); } /** * Get the url to view the first unread post in a discussion. * * @param discussion_entity $discussion The discussion * @return moodle_url */ public function get_discussion_view_first_unread_post_url_from_discussion(discussion_entity $discussion) { $viewurl = $this->get_discussion_view_url_from_discussion_id($discussion->get_id()); $viewurl->set_anchor('unread'); return $viewurl; } /** * Get the url to view the latest post in a discussion. * * @param discussion_entity $discussion The discussion * @param int|null $latestpost The id of the latest post * @return moodle_url */ public function get_discussion_view_latest_post_url_from_discussion(discussion_entity $discussion, ?int $latestpost) { $viewurl = $this->get_discussion_view_url_from_discussion_id($discussion->get_id()); if (null === $latestpost) { return $viewurl; } else { return new moodle_url($viewurl, ['parent' => $latestpost]); } } /** * Get the url to view a discussion from a post. * * @param post_entity $post The post * @return moodle_url */ public function get_discussion_view_url_from_post(post_entity $post): moodle_url { return $this->get_discussion_view_url_from_discussion_id($post->get_discussion_id()); } /** * Get the url to view a discussion from a discussion id and post id. * * @param int $discussionid The discussion id * @param int $postid The post id * @return moodle_url */ public function get_view_post_url_from_post_id(int $discussionid, int $postid): moodle_url { $url = $this->get_discussion_view_url_from_discussion_id($discussionid); $url->set_anchor('p' . $postid); return $url; } /** * Get the url to view a post in the context of the rest of the discussion. * * @param post_entity $post The post * @return moodle_url */ public function get_view_post_url_from_post(post_entity $post): moodle_url { return $this->get_view_post_url_from_post_id($post->get_discussion_id(), $post->get_id()); } /** * Get the url to view a post and it's replies in isolation without the rest of the * discussion. * * @param int $discussionid The discussion id * @param int $postid The post id * @return moodle_url */ public function get_view_isolated_post_url_from_post_id(int $discussionid, int $postid): moodle_url { $url = $this->get_discussion_view_url_from_discussion_id($discussionid); $url->params(['parent' => $postid]); return $url; } /** * Get the url to view a post and it's replies in isolation without the rest of the * discussion. * * @param post_entity $post The post * @return moodle_url */ public function get_view_isolated_post_url_from_post(post_entity $post): moodle_url { return $this->get_view_isolated_post_url_from_post_id($post->get_discussion_id(), $post->get_id()); } /** * Get the url to edit a post. * * @param forum_entity $forum The forum the post belongs to * @param post_entity $post The post * @return moodle_url */ public function get_edit_post_url_from_post(forum_entity $forum, post_entity $post): moodle_url { if ($forum->get_type() == 'single' && !$post->has_parent()) { return new moodle_url('/course/modedit.php', [ 'update' => $forum->get_course_module_record()->id, 'sesskey' => sesskey(), 'return' => 1 ]); } else { return new moodle_url('/mod/forum/post.php', [ 'edit' => $post->get_id() ]); } } /** * Get the url to split a discussion at a post. * * @param post_entity $post The post * @return moodle_url */ public function get_split_discussion_at_post_url_from_post(post_entity $post): moodle_url { return new moodle_url('/mod/forum/post.php', [ 'prune' => $post->get_id() ]); } /** * Get the url to delete a post. * * @param post_entity $post The post * @return moodle_url */ public function get_delete_post_url_from_post(post_entity $post): moodle_url { return new moodle_url('/mod/forum/post.php', [ 'delete' => $post->get_id() ]); } /** * Get the url to reply to a post. * * @param post_entity $post The post * @return moodle_url */ public function get_reply_to_post_url_from_post(post_entity $post): moodle_url { return new moodle_url('/mod/forum/post.php#mformforum', [ 'reply' => $post->get_id() ]); } /** * Get the url to export (see portfolios) a post. * * @param post_entity $post The post * @return moodle_url */ public function get_export_post_url_from_post(post_entity $post): ?moodle_url { global $CFG; require_once($CFG->libdir . '/portfoliolib.php'); $button = new \portfolio_add_button(); $button->set_callback_options('forum_portfolio_caller', ['postid' => $post->get_id()], 'mod_forum'); if ($post->has_attachments()) { $button->set_formats(PORTFOLIO_FORMAT_RICHHTML); } else { $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML); } $url = $button->to_html(PORTFOLIO_ADD_MOODLE_URL); return $url ?: null; } /** * Get the url to mark a post as read. * * @param post_entity $post The post * @param int $displaymode The display mode to show the forum in after marking as read * @return moodle_url */ public function get_mark_post_as_read_url_from_post(post_entity $post, int $displaymode = FORUM_MODE_THREADED): moodle_url { $params = [ 'd' => $post->get_discussion_id(), 'postid' => $post->get_id(), 'mark' => 'read' ]; $url = new moodle_url('/mod/forum/discuss.php', $params); if ($displaymode == FORUM_MODE_THREADED) { $url->param('parent', $post->get_parent_id()); } else { $url->set_anchor('p' . $post->get_id()); } return $url; } /** * Get the url to mark a post as unread. * * @param post_entity $post The post * @param int $displaymode The display mode to show the forum in after marking as unread * @return moodle_url */ public function get_mark_post_as_unread_url_from_post(post_entity $post, int $displaymode = FORUM_MODE_THREADED): moodle_url { $params = [ 'd' => $post->get_discussion_id(), 'postid' => $post->get_id(), 'mark' => 'unread' ]; $url = new moodle_url('/mod/forum/discuss.php', $params); if ($displaymode == FORUM_MODE_THREADED) { $url->param('parent', $post->get_parent_id()); } else { $url->set_anchor('p' . $post->get_id()); } return $url; } /** * Get the url to export attachments for a post. * * @param post_entity $post The post * @param stored_file $attachment * @return moodle_url|null */ public function get_export_attachment_url_from_post_and_attachment(post_entity $post, stored_file $attachment): ?moodle_url { global $CFG; require_once($CFG->libdir . '/portfoliolib.php'); $button = new \portfolio_add_button(); $button->set_callback_options( 'forum_portfolio_caller', ['postid' => $post->get_id(), 'attachment' => $attachment->get_id()], 'mod_forum' ); $button->set_format_by_file($attachment); $url = $button->to_html(PORTFOLIO_ADD_MOODLE_URL); return $url ?: null; } /** * Get the url to view an author's profile. * * @param author_entity $author The author * @param int $courseid The course id * @return moodle_url */ public function get_author_profile_url(author_entity $author, int $courseid): moodle_url { return new moodle_url('/user/view.php', [ 'id' => $author->get_id(), 'course' => $courseid ]); } /** * Get the url to view the author's profile image. The author's context id should be * provided to prevent the code from needing to load it. * * @param author_entity $author The author * @param int|null $authorcontextid The author context id * @param int $size The size of the image to return * @return moodle_url */ public function get_author_profile_image_url( author_entity $author, ?int $authorcontextid = null, int $size = 100 ): moodle_url { global $PAGE; $datamapper = $this->legacydatamapperfactory->get_author_data_mapper(); $record = $datamapper->to_legacy_object($author); $record->contextid = $authorcontextid; $userpicture = new user_picture($record); $userpicture->size = $size; return $userpicture->get_url($PAGE); } /** * Get the url to view an author's group. * * @param \stdClass $group The group * @return moodle_url */ public function get_author_group_url(\stdClass $group): moodle_url { return new moodle_url('/user/index.php', [ 'id' => $group->courseid, 'group' => $group->id ]); } /** * Get the url to mark a discussion as read. * * @param forum_entity $forum The forum that the discussion belongs to * @param discussion_entity $discussion The discussion * @return moodle_url */ public function get_mark_discussion_as_read_url_from_discussion( forum_entity $forum, discussion_entity $discussion ): moodle_url { return new moodle_url('/mod/forum/markposts.php', [ 'f' => $discussion->get_forum_id(), 'd' => $discussion->get_id(), 'mark' => 'read', 'sesskey' => sesskey(), 'return' => $this->get_forum_view_url_from_forum($forum)->out(), ]); } /** * Get the url to mark all discussions as read. * * @param forum_entity $forum The forum that the discussions belong to * @return moodle_url */ public function get_mark_all_discussions_as_read_url(forum_entity $forum): moodle_url { return new moodle_url('/mod/forum/markposts.php', [ 'f' => $forum->get_id(), 'mark' => 'read', 'sesskey' => sesskey(), 'return' => $this->get_forum_view_url_from_forum($forum)->out(), ]); } /** * Get the url to subscribe to a discussion. * * @param discussion_entity $discussion The discussion * @return moodle_url */ public function get_discussion_subscribe_url(discussion_entity $discussion): moodle_url { return new moodle_url('/mod/forum/subscribe.php', [ 'sesskey' => sesskey(), 'id' => $discussion->get_forum_id(), 'd' => $discussion->get_id() ]); } /** * Generate the pinned discussion link * * @param discussion_entity $discussion * @return moodle_url * @throws \moodle_exception */ public function get_pin_discussion_url_from_discussion(discussion_entity $discussion): moodle_url { return new moodle_url('discuss.php', [ 'sesskey' => sesskey(), 'd' => $discussion->get_id(), 'pin' => $discussion->is_pinned() ? FORUM_DISCUSSION_UNPINNED : FORUM_DISCUSSION_PINNED ]); } } PK-B\\-SeSe%classes/local/managers/capability.phpnu[. /** * Capability manager for the forum. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\managers; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\data_mappers\legacy\forum as legacy_forum_data_mapper; use mod_forum\local\data_mappers\legacy\discussion as legacy_discussion_data_mapper; use mod_forum\local\data_mappers\legacy\post as legacy_post_data_mapper; use mod_forum\local\entities\discussion as discussion_entity; use mod_forum\local\entities\forum as forum_entity; use mod_forum\local\entities\post as post_entity; use mod_forum\subscriptions; use context; use context_system; use stdClass; use moodle_exception; require_once($CFG->dirroot . '/mod/forum/lib.php'); /** * Capability manager for the forum. * * Defines all the business rules for what a user can and can't do in the forum. * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class capability { /** @var legacy_forum_data_mapper $forumdatamapper Legacy forum data mapper */ private $forumdatamapper; /** @var legacy_discussion_data_mapper $discussiondatamapper Legacy discussion data mapper */ private $discussiondatamapper; /** @var legacy_post_data_mapper $postdatamapper Legacy post data mapper */ private $postdatamapper; /** @var forum_entity $forum Forum entity */ private $forum; /** @var stdClass $forumrecord Legacy forum record */ private $forumrecord; /** @var context $context Module context for the forum */ private $context; /** @var array $canviewpostcache Cache of discussion posts that can be viewed by a user. */ protected $canviewpostcache = []; /** * Constructor. * * @param forum_entity $forum The forum entity to manage capabilities for. * @param legacy_forum_data_mapper $forumdatamapper Legacy forum data mapper * @param legacy_discussion_data_mapper $discussiondatamapper Legacy discussion data mapper * @param legacy_post_data_mapper $postdatamapper Legacy post data mapper */ public function __construct( forum_entity $forum, legacy_forum_data_mapper $forumdatamapper, legacy_discussion_data_mapper $discussiondatamapper, legacy_post_data_mapper $postdatamapper ) { $this->forumdatamapper = $forumdatamapper; $this->discussiondatamapper = $discussiondatamapper; $this->postdatamapper = $postdatamapper; $this->forum = $forum; $this->forumrecord = $forumdatamapper->to_legacy_object($forum); $this->context = $forum->get_context(); } /** * Can the user subscribe to this forum? * * @param stdClass $user The user to check * @return bool */ public function can_subscribe_to_forum(stdClass $user): bool { if ($this->forum->get_type() == 'single') { return false; } return !is_guest($this->get_context(), $user) && subscriptions::is_subscribable($this->get_forum_record()); } /** * Can the user create discussions in this forum? * * @param stdClass $user The user to check * @param int|null $groupid The current activity group id * @return bool */ public function can_create_discussions(stdClass $user, ?int $groupid = null): bool { if (isguestuser($user) or !isloggedin()) { return false; } if ($this->forum->is_cutoff_date_reached()) { if (!has_capability('mod/forum:canoverridecutoff', $this->get_context())) { return false; } } // If the user reaches the number of posts equal to warning/blocking setting then return the value of canpost in $warningobj. $cmrecord = $this->forum->get_course_module_record(); if ($warningobj = forum_check_throttling($this->forumrecord, $cmrecord)) { return $warningobj->canpost; } switch ($this->forum->get_type()) { case 'news': $capability = 'mod/forum:addnews'; break; case 'qanda': $capability = 'mod/forum:addquestion'; break; default: $capability = 'mod/forum:startdiscussion'; } if (!has_capability($capability, $this->forum->get_context(), $user)) { return false; } if ($this->forum->get_type() == 'eachuser') { if (forum_user_has_posted_discussion($this->forum->get_id(), $user->id, $groupid)) { return false; } } if ($this->forum->is_in_group_mode()) { return $groupid ? $this->can_access_group($user, $groupid) : $this->can_access_all_groups($user); } else { return true; } } /** * Can the user access all groups? * * @param stdClass $user The user to check * @return bool */ public function can_access_all_groups(stdClass $user): bool { return has_capability('moodle/site:accessallgroups', $this->get_context(), $user); } /** * Can the user access the given group? * * @param stdClass $user The user to check * @param int $groupid The id of the group that the forum is set to * @return bool */ public function can_access_group(stdClass $user, int $groupid): bool { if ($this->can_access_all_groups($user)) { // This user has access to all groups. return true; } // This is a group discussion for a forum in separate groups mode. // Check if the user is a member. // This is the most expensive check. return groups_is_member($groupid, $user->id); } /** * Can the user post to their groups? * * @param stdClass $user The user to check * @return bool */ public function can_post_to_my_groups(stdClass $user): bool { return has_capability('mod/forum:canposttomygroups', $this->get_context(), $user); } /** * Can the user view discussions in this forum? * * @param stdClass $user The user to check * @return bool */ public function can_view_discussions(stdClass $user): bool { return has_capability('mod/forum:viewdiscussion', $this->get_context(), $user); } /** * Can the user move discussions in this forum? * * @param stdClass $user The user to check * @return bool */ public function can_move_discussions(stdClass $user): bool { $forum = $this->get_forum(); return $forum->get_type() !== 'single' && has_capability('mod/forum:movediscussions', $this->get_context(), $user); } /** * Can the user pin discussions in this forum? * * @param stdClass $user The user to check * @return bool */ public function can_pin_discussions(stdClass $user): bool { return $this->forum->get_type() !== 'single' && has_capability('mod/forum:pindiscussions', $this->get_context(), $user); } /** * Can the user split discussions in this forum? * * @param stdClass $user The user to check * @return bool */ public function can_split_discussions(stdClass $user): bool { $forum = $this->get_forum(); return $forum->get_type() !== 'single' && has_capability('mod/forum:splitdiscussions', $this->get_context(), $user); } /** * Can the user export (see portfolios) discussions in this forum? * * @param stdClass $user The user to check * @return bool */ public function can_export_discussions(stdClass $user): bool { global $CFG; return $CFG->enableportfolios && has_capability('mod/forum:exportdiscussion', $this->get_context(), $user); } /** * Can the user manually mark posts as read/unread in this forum? * * @param stdClass $user The user to check * @return bool */ public function can_manually_control_post_read_status(stdClass $user): bool { global $CFG; return $CFG->forum_usermarksread && isloggedin() && forum_tp_is_tracked($this->get_forum_record(), $user); } /** * Is the user required to post in the discussion before they can view it? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @return bool */ public function must_post_before_viewing_discussion(stdClass $user, discussion_entity $discussion): bool { $forum = $this->get_forum(); if ($forum->get_type() === 'qanda') { // If it's a Q and A forum then the user must either have the capability to view without // posting or the user must have posted before they can view the discussion. return !has_capability('mod/forum:viewqandawithoutposting', $this->get_context(), $user) && !forum_user_has_posted($forum->get_id(), $discussion->get_id(), $user->id); } else { // No other forum types require posting before viewing. return false; } } /** * Can the user subscribe to the give discussion? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @return bool */ public function can_subscribe_to_discussion(stdClass $user, discussion_entity $discussion): bool { return $this->can_subscribe_to_forum($user); } /** * Can the user move the discussion in this forum? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @return bool */ public function can_move_discussion(stdClass $user, discussion_entity $discussion): bool { return $this->can_move_discussions($user); } /** * Is the user pin the discussion? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @return bool */ public function can_pin_discussion(stdClass $user, discussion_entity $discussion): bool { return $this->can_pin_discussions($user); } /** * Can the user post in this discussion? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @return bool */ public function can_post_in_discussion(stdClass $user, discussion_entity $discussion): bool { $forum = $this->get_forum(); $forumrecord = $this->get_forum_record(); $discussionrecord = $this->get_discussion_record($discussion); $context = $this->get_context(); $coursemodule = $forum->get_course_module_record(); $course = $forum->get_course_record(); $status = forum_user_can_post($forumrecord, $discussionrecord, $user, $coursemodule, $course, $context); // If the user reaches the number of posts equal to warning/blocking setting then logically and canpost value with $status. if ($warningobj = forum_check_throttling($forumrecord, $coursemodule)) { return $status && $warningobj->canpost; } return $status; } /** * Can the user favourite the discussion * * @param stdClass $user The user to check * @return bool */ public function can_favourite_discussion(stdClass $user): bool { $context = $this->get_context(); return has_capability('mod/forum:cantogglefavourite', $context, $user); } /** * Can the user view the content of a discussion? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @return bool */ public function can_view_discussion(stdClass $user, discussion_entity $discussion): bool { $forumrecord = $this->get_forum_record(); $discussionrecord = $this->get_discussion_record($discussion); $context = $this->get_context(); return forum_user_can_see_discussion($forumrecord, $discussionrecord, $context, $user); } /** * Can the user view the content of the post in this discussion? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @param post_entity $post The post the user wants to view * @return bool */ public function can_view_post(stdClass $user, discussion_entity $discussion, post_entity $post): bool { if (!$this->can_view_post_shell($user, $post)) { return false; } // Return cached can view if possible. if (isset($this->canviewpostcache[$user->id][$post->get_id()])) { return $this->canviewpostcache[$user->id][$post->get_id()]; } // Otherwise, check if the user can see this post. $forum = $this->get_forum(); $forumrecord = $this->get_forum_record(); $discussionrecord = $this->get_discussion_record($discussion); $postrecord = $this->get_post_record($post); $coursemodule = $forum->get_course_module_record(); $canviewpost = forum_user_can_see_post($forumrecord, $discussionrecord, $postrecord, $user, $coursemodule, false); // Then cache the result before returning. $this->canviewpostcache[$user->id][$post->get_id()] = $canviewpost; return $canviewpost; } /** * Can the user view the post at all? * In some situations the user can view the shell of a post without being able to view its content. * * @param stdClass $user The user to check * @param post_entity $post The post the user wants to view * @return bool * */ public function can_view_post_shell(stdClass $user, post_entity $post): bool { if ($post->is_owned_by_user($user)) { return true; } if (!$post->is_private_reply()) { return true; } if ($post->is_private_reply_intended_for_user($user)) { return true; } return $this->can_view_any_private_reply($user); } /** * Whether the user can view any private reply in the forum. * * @param stdClass $user The user to check * @return bool */ public function can_view_any_private_reply(stdClass $user): bool { return has_capability('mod/forum:readprivatereplies', $this->get_context(), $user); } /** * Can the user edit the post in this discussion? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @param post_entity $post The post the user wants to edit * @return bool */ public function can_edit_post(stdClass $user, discussion_entity $discussion, post_entity $post): bool { global $CFG; $context = $this->get_context(); $ownpost = $post->is_owned_by_user($user); $ineditingtime = $post->get_age() < $CFG->maxeditingtime; $mailnow = $post->should_mail_now(); switch ($this->forum->get_type()) { case 'news': // Allow editing of news posts once the discussion has started. $ineditingtime = !$post->has_parent() && $discussion->has_started(); break; case 'single': if ($discussion->is_first_post($post)) { return has_capability('moodle/course:manageactivities', $context, $user); } break; } return ($ownpost && $ineditingtime && !$mailnow) || has_capability('mod/forum:editanypost', $context, $user); } /** * Verifies is the given user can delete a post. * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @param post_entity $post The post the user wants to delete * @param bool $hasreplies Whether the post has replies * @return bool * @throws moodle_exception */ public function validate_delete_post(stdClass $user, discussion_entity $discussion, post_entity $post, bool $hasreplies = false): void { global $CFG; $forum = $this->get_forum(); if ($forum->get_type() == 'single' && $discussion->is_first_post($post)) { // Do not allow deleting of first post in single simple type. throw new moodle_exception('cannotdeletepost', 'forum'); } $context = $this->get_context(); $ownpost = $post->is_owned_by_user($user); $ineditingtime = $post->get_age() < $CFG->maxeditingtime; $mailnow = $post->should_mail_now(); if (!($ownpost && $ineditingtime && has_capability('mod/forum:deleteownpost', $context, $user) && !$mailnow || has_capability('mod/forum:deleteanypost', $context, $user))) { throw new moodle_exception('cannotdeletepost', 'forum'); } if ($post->get_total_score()) { throw new moodle_exception('couldnotdeleteratings', 'rating'); } if ($hasreplies && !has_capability('mod/forum:deleteanypost', $context, $user)) { throw new moodle_exception('couldnotdeletereplies', 'forum'); } } /** * Can the user delete the post in this discussion? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @param post_entity $post The post the user wants to delete * @param bool $hasreplies Whether the post has replies * @return bool */ public function can_delete_post(stdClass $user, discussion_entity $discussion, post_entity $post, bool $hasreplies = false): bool { try { $this->validate_delete_post($user, $discussion, $post, $hasreplies); return true; } catch (moodle_exception $e) { return false; } } /** * Can the user split the post in this discussion? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @param post_entity $post The post the user wants to split * @return bool */ public function can_split_post(stdClass $user, discussion_entity $discussion, post_entity $post): bool { if ($post->is_private_reply()) { // It is not possible to create a private discussion. return false; } return $this->can_split_discussions($user) && $post->has_parent(); } /** * Can the user reply to the post in this discussion? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @param post_entity $post The post the user wants to reply to * @return bool */ public function can_reply_to_post(stdClass $user, discussion_entity $discussion, post_entity $post): bool { if ($post->is_private_reply()) { // It is not possible to reply to a private reply. return false; } else if (!$this->can_view_post($user, $discussion, $post)) { // If the user cannot view the post in the first place, the user should not be able to reply to the post. return false; } return $this->can_post_in_discussion($user, $discussion); } /** * Can the user reply privately to the specified post? * * @param stdClass $user The user to check * @param post_entity $post The post the user wants to reply to * @return bool */ public function can_reply_privately_to_post(stdClass $user, post_entity $post): bool { if ($post->is_private_reply()) { // You cannot reply privately to a post which is, itself, a private reply. return false; } return has_capability('mod/forum:postprivatereply', $this->get_context(), $user); } /** * Can the user export (see portfolios) the post in this discussion? * * @param stdClass $user The user to check * @param post_entity $post The post the user wants to export * @return bool */ public function can_export_post(stdClass $user, post_entity $post): bool { global $CFG; $context = $this->get_context(); return $CFG->enableportfolios && (has_capability('mod/forum:exportpost', $context, $user) || ($post->is_owned_by_user($user) && has_capability('mod/forum:exportownpost', $context, $user))); } /** * Get the forum entity for this capability manager. * * @return forum_entity */ protected function get_forum(): forum_entity { return $this->forum; } /** * Get the legacy forum record for this forum. * * @return stdClass */ protected function get_forum_record(): stdClass { return $this->forumrecord; } /** * Get the context for this capability manager. * * @return context */ protected function get_context(): context { return $this->context; } /** * Get the legacy discussion record for the given discussion entity. * * @param discussion_entity $discussion The discussion to convert * @return stdClass */ protected function get_discussion_record(discussion_entity $discussion): stdClass { return $this->discussiondatamapper->to_legacy_object($discussion); } /** * Get the legacy post record for the given post entity. * * @param post_entity $post The post to convert * @return stdClass */ protected function get_post_record(post_entity $post): stdClass { return $this->postdatamapper->to_legacy_object($post); } /** * Can the user view the participants of this discussion? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @return bool */ public function can_view_participants(stdClass $user, discussion_entity $discussion): bool { return course_can_view_participants($this->get_context()) && !$this->must_post_before_viewing_discussion($user, $discussion); } /** * Can the user view hidden posts in this forum? * * @param stdClass $user The user to check * @return bool */ public function can_view_hidden_posts(stdClass $user): bool { return has_capability('mod/forum:viewhiddentimedposts', $this->get_context(), $user); } /** * Can the user manage this forum? * * @param stdClass $user The user to check * @return bool */ public function can_manage_forum(stdClass $user) { return has_capability('moodle/course:manageactivities', $this->get_context(), $user); } /** * Can the user manage tags on the site? * * @param stdClass $user The user to check * @return bool */ public function can_manage_tags(stdClass $user): bool { return has_capability('moodle/tag:manage', context_system::instance(), $user); } /** * Checks whether the user can self enrol into the course. * Mimics the checks on the add button in deprecatedlib/forum_print_latest_discussions * * @param stdClass $user * @return bool */ public function can_self_enrol(stdClass $user): bool { $canstart = false; if ($this->forum->get_type() != 'news') { if (isguestuser($user) or !isloggedin()) { $canstart = true; } if (!is_enrolled($this->context) and !is_viewing($this->context)) { // Allow guests and not-logged-in to see the button - they are prompted to log in after clicking the link, // Normal users with temporary guest access see this button too, they are asked to enrol instead, // Do not show the button to users with suspended enrolments here. $canstart = enrol_selfenrol_available($this->forum->get_course_id()); } } return $canstart; } /** * Checks whether the user can export the whole forum (discussions and posts). * * @param stdClass $user The user object. * @return bool True if the user can export the forum or false otherwise. */ public function can_export_forum(stdClass $user): bool { return has_capability('mod/forum:exportforum', $this->get_context(), $user); } /** * Check whether the supplied grader can grade the gradee. * * @param stdClass $grader The user grading * @param stdClass $gradee The user being graded * @return bool */ public function can_grade(stdClass $grader, ?stdClass $gradee = null): bool { if (!has_capability('mod/forum:grade', $this->get_context(), $grader)) { return false; } return true; } } PK-B\z)classes/potential_subscriber_selector.phpnu[. /** * A type of forum. * * @package mod_forum * @copyright 2014 Andrew Robert Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/user/selector/lib.php'); /** * A user selector control for potential subscribers to the selected forum * @package mod_forum * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_forum_potential_subscriber_selector extends mod_forum_subscriber_selector_base { /** * If set to true EVERYONE in this course is force subscribed to this forum * @var bool */ protected $forcesubscribed = false; /** * Can be used to store existing subscribers so that they can be removed from * the potential subscribers list */ protected $existingsubscribers = array(); /** * Constructor method * @param string $name * @param array $options */ public function __construct($name, $options) { parent::__construct($name, $options); if (isset($options['forcesubscribed'])) { $this->forcesubscribed=true; } } /** * Returns an arary of options for this control * @return array */ protected function get_options() { $options = parent::get_options(); if ($this->forcesubscribed===true) { $options['forcesubscribed']=1; } return $options; } /** * Finds all potential users * * Potential subscribers are all enroled users who are not already subscribed. * * @param string $search * @return array */ public function find_users($search) { global $DB; $whereconditions = array(); list($wherecondition, $params) = $this->search_sql($search, 'u'); if ($wherecondition) { $whereconditions[] = $wherecondition; } if (!$this->forcesubscribed) { $existingids = array(); foreach ($this->existingsubscribers as $group) { foreach ($group as $user) { $existingids[$user->id] = 1; } } if ($existingids) { list($usertest, $userparams) = $DB->get_in_or_equal( array_keys($existingids), SQL_PARAMS_NAMED, 'existing', false); $whereconditions[] = 'u.id ' . $usertest; $params = array_merge($params, $userparams); } } if ($whereconditions) { $wherecondition = 'WHERE ' . implode(' AND ', $whereconditions); } list($esql, $eparams) = get_enrolled_sql($this->context, '', $this->currentgroup, true); $params = array_merge($params, $eparams); $fields = 'SELECT ' . $this->required_fields_sql('u'); $sql = " FROM {user} u JOIN ($esql) je ON je.id = u.id $wherecondition"; list($sort, $sortparams) = users_order_by_sql('u', $search, $this->accesscontext); $order = ' ORDER BY ' . $sort; $availableusers = $DB->get_records_sql($fields . $sql . $order, array_merge($params, $sortparams)); $cm = get_coursemodule_from_instance('forum', $this->forumid); $modinfo = get_fast_modinfo($cm->course); $info = new \core_availability\info_module($modinfo->get_cm($cm->id)); $availableusers = $info->filter_user_list($availableusers); if (empty($availableusers)) { return array(); } // Check to see if there are too many to show sensibly. if (!$this->is_validating()) { $potentialmemberscount = count($availableusers); if ($potentialmemberscount > $this->maxusersperpage) { return $this->too_many_results($search, $potentialmemberscount); } } if ($this->forcesubscribed) { return array(get_string("existingsubscribers", 'forum') => $availableusers); } else { return array(get_string("potentialsubscribers", 'forum') => $availableusers); } } /** * Sets the existing subscribers * @param array $users */ public function set_existing_subscribers(array $users) { $this->existingsubscribers = $users; } /** * Sets this forum as force subscribed or not */ public function set_force_subscribed($setting=true) { $this->forcesubscribed = true; } } PK-B\*classes/prune_form.phpnu[. /** * This file provides form for splitting discussions * * @package mod_forum * @copyright 2015 Martin Mastny * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); } require_once("$CFG->libdir/formslib.php"); /** * Form which displays fields for splitting forum post to a separate threads. * * @package mod_forum * @copyright 2015 Martin Mastny * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_forum_prune_form extends moodleform { /** * Form constructor. * */ public function definition() { $mform = $this->_form; $mform->addElement('text', 'name', get_string('discussionname', 'forum'), array('size' => '60', 'maxlength' => '255')); $mform->setType('name', PARAM_TEXT); $mform->addRule('name', null, 'required', null, 'client'); $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); $this->add_action_buttons(true, get_string('prune', 'forum')); $mform->addElement('hidden', 'prune'); $mform->setType('prune', PARAM_INT); $mform->setConstant('prune', $this->_customdata['prune']); $mform->addElement('hidden', 'confirm'); $mform->setType('confirm', PARAM_INT); $mform->setConstant('confirm', $this->_customdata['confirm']); } } PK-B\rP (classes/existing_subscriber_selector.phpnu[. /** * A type of forum. * * @package mod_forum * @copyright 2014 Andrew Robert Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/user/selector/lib.php'); /** * User selector control for removing subscribed users * @package mod_forum * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_forum_existing_subscriber_selector extends mod_forum_subscriber_selector_base { /** * Finds all subscribed users * * @param string $search * @return array */ public function find_users($search) { global $DB; list($wherecondition, $params) = $this->search_sql($search, 'u'); $params['forumid'] = $this->forumid; // only active enrolled or everybody on the frontpage list($esql, $eparams) = get_enrolled_sql($this->context, '', $this->currentgroup, true); $fields = $this->required_fields_sql('u'); list($sort, $sortparams) = users_order_by_sql('u', $search, $this->accesscontext); $params = array_merge($params, $eparams, $sortparams); $subscribers = $DB->get_records_sql("SELECT $fields FROM {user} u JOIN ($esql) je ON je.id = u.id JOIN {forum_subscriptions} s ON s.userid = u.id WHERE $wherecondition AND s.forum = :forumid ORDER BY $sort", $params); $cm = get_coursemodule_from_instance('forum', $this->forumid); $modinfo = get_fast_modinfo($cm->course); $info = new \core_availability\info_module($modinfo->get_cm($cm->id)); $subscribers = $info->filter_user_list($subscribers); return array(get_string("existingsubscribers", 'forum') => $subscribers); } } PK-B\ 55)classes/message/inbound/reply_handler.phpnu[. /** * A Handler to process replies to forum posts. * * @package mod_forum * @subpackage core_message * @copyright 2014 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\message\inbound; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/forum/lib.php'); require_once($CFG->dirroot . '/repository/lib.php'); require_once($CFG->libdir . '/completionlib.php'); /** * A Handler to process replies to forum posts. * * @copyright 2014 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class reply_handler extends \core\message\inbound\handler { /** * Return a description for the current handler. * * @return string */ public function get_description() { return get_string('reply_handler', 'mod_forum'); } /** * Return a short name for the current handler. * This appears in the admin pages as a human-readable name. * * @return string */ public function get_name() { return get_string('reply_handler_name', 'mod_forum'); } /** * Process a message received and validated by the Inbound Message processor. * * @throws \core\message\inbound\processing_failed_exception * @param \stdClass $messagedata The Inbound Message record * @param \stdClass $messagedata The message data packet * @return bool Whether the message was successfully processed. */ public function process_message(\stdClass $record, \stdClass $messagedata) { global $CFG, $DB, $USER; // Load the post being replied to. $post = $DB->get_record('forum_posts', array('id' => $record->datavalue)); if (!$post) { mtrace("--> Unable to find a post matching with id {$record->datavalue}"); return false; } // Load the discussion that this post is in. $discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion)); if (!$post) { mtrace("--> Unable to find the discussion for post {$record->datavalue}"); return false; } // Load the other required data. $forum = $DB->get_record('forum', array('id' => $discussion->forum)); $course = $DB->get_record('course', array('id' => $forum->course)); $cm = get_fast_modinfo($course->id)->instances['forum'][$forum->id]; $modcontext = \context_module::instance($cm->id); $usercontext = \context_user::instance($USER->id); // Make sure user can post in this discussion. $canpost = true; if (!forum_user_can_post($forum, $discussion, $USER, $cm, $course, $modcontext)) { $canpost = false; } if (isset($cm->groupmode) && empty($course->groupmodeforce)) { $groupmode = $cm->groupmode; } else { $groupmode = $course->groupmode; } if ($groupmode == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $modcontext)) { if ($discussion->groupid == -1) { $canpost = false; } else { if (!groups_is_member($discussion->groupid)) { $canpost = false; } } } if (!$canpost) { $data = new \stdClass(); $data->forum = $forum; throw new \core\message\inbound\processing_failed_exception('messageinboundnopostforum', 'mod_forum', $data); } // And check the availability. if (!\core_availability\info_module::is_user_visible($cm)) { $data = new \stdClass(); $data->forum = $forum; throw new \core\message\inbound\processing_failed_exception('messageinboundforumhidden', 'mod_forum', $data); } // Before we add this we must check that the user will not exceed the blocking threshold. // This should result in an appropriate reply. $thresholdwarning = forum_check_throttling($forum, $cm); if (!empty($thresholdwarning) && !$thresholdwarning->canpost) { $data = new \stdClass(); $data->forum = $forum; $data->message = get_string($thresholdwarning->errorcode, $thresholdwarning->module, $thresholdwarning->additional); throw new \core\message\inbound\processing_failed_exception('messageinboundthresholdhit', 'mod_forum', $data); } $subject = clean_param($messagedata->envelope->subject, PARAM_TEXT); if (strpos($subject, $discussion->name)) { // The discussion name is mentioned in the e-mail subject. This is probably just the standard reply. Use the // standard reply subject instead. mtrace("--> Note: Post subject matched discussion name. Optimising from {$subject} to {$discussion->name}"); $subject = $discussion->name; } else if (strpos($subject, $post->subject)) { // The replied-to post's subject is mentioned in the e-mail subject. // Use the previous post's subject instead of the e-mail subject. mtrace("--> Note: Post subject matched original post subject. Optimising from {$subject} to {$post->subject}"); $subject = $post->subject; } $addpost = new \stdClass(); $addpost->course = $course->id; $addpost->forum = $forum->id; $addpost->discussion = $discussion->id; $addpost->modified = $messagedata->timestamp; $addpost->subject = $subject; $addpost->parent = $post->id; $addpost->itemid = file_get_unused_draft_itemid(); $addpost->deleted = 0; list ($message, $format) = self::remove_quoted_text($messagedata); $addpost->message = $message; $addpost->messageformat = $format; // We don't trust text coming from e-mail. $addpost->messagetrust = false; // Find all attachments. If format is plain text, treat inline attachments as regular ones. $attachments = !empty($messagedata->attachments['attachment']) ? $messagedata->attachments['attachment'] : []; $inlineattachments = []; if (!empty($messagedata->attachments['inline'])) { if ($addpost->messageformat == FORMAT_HTML) { $inlineattachments = $messagedata->attachments['inline']; } else { $attachments = array_merge($attachments, $messagedata->attachments['inline']); } } // Add attachments to the post. if ($attachments) { $attachmentcount = count($attachments); if (!forum_can_create_attachment($forum, $modcontext)) { // Attachments are not allowed. mtrace("--> User does not have permission to attach files in this forum. Rejecting e-mail."); $data = new \stdClass(); $data->forum = $forum; $data->attachmentcount = $attachmentcount; throw new \core\message\inbound\processing_failed_exception('messageinboundattachmentdisallowed', 'mod_forum', $data); } if ($forum->maxattachments < $attachmentcount) { // Too many attachments. mtrace("--> User attached {$attachmentcount} files when only {$forum->maxattachments} where allowed. " . " Rejecting e-mail."); $data = new \stdClass(); $data->forum = $forum; $data->attachmentcount = $attachmentcount; throw new \core\message\inbound\processing_failed_exception('messageinboundfilecountexceeded', 'mod_forum', $data); } $filesize = 0; $addpost->attachments = file_get_unused_draft_itemid(); foreach ($attachments as $attachment) { mtrace("--> Processing {$attachment->filename} as an attachment."); $this->process_attachment('*', $usercontext, $addpost->attachments, $attachment); $filesize += $attachment->filesize; } $maxbytes = get_user_max_upload_file_size($modcontext, $CFG->maxbytes, $course->maxbytes, $forum->maxbytes); if ($maxbytes < $filesize) { // Too many attachments. mtrace("--> User attached {$filesize} bytes of files when only {$maxbytes} were allowed. Rejecting e-mail."); $data = new \stdClass(); $data->forum = $forum; $data->maxbytes = display_size($maxbytes); $data->filesize = display_size($filesize); throw new \core\message\inbound\processing_failed_exception('messageinboundfilesizeexceeded', 'mod_forum', $data); } } // Process any files in the message itself. if ($inlineattachments) { foreach ($inlineattachments as $attachment) { mtrace("--> Processing {$attachment->filename} as an inline attachment."); $this->process_attachment('*', $usercontext, $addpost->itemid, $attachment); // Convert the contentid link in the message. $draftfile = \moodle_url::make_draftfile_url($addpost->itemid, '/', $attachment->filename); $addpost->message = preg_replace('/cid:' . $attachment->contentid . '/', $draftfile, $addpost->message); } } // Insert the message content now. $addpost->id = forum_add_new_post($addpost, true); // Log the new post creation. $params = array( 'context' => $modcontext, 'objectid' => $addpost->id, 'other' => array( 'discussionid' => $discussion->id, 'forumid' => $forum->id, 'forumtype' => $forum->type, ) ); $event = \mod_forum\event\post_created::create($params); $event->add_record_snapshot('forum_posts', $addpost); $event->add_record_snapshot('forum_discussions', $discussion); $event->trigger(); // Update completion state. $completion = new \completion_info($course); if ($completion->is_enabled($cm) && ($forum->completionreplies || $forum->completionposts)) { $completion->update_state($cm, COMPLETION_COMPLETE); mtrace("--> Updating completion status for user {$USER->id} in forum {$forum->id} for post {$addpost->id}."); } mtrace("--> Created a post {$addpost->id} in {$discussion->id}."); return $addpost; } /** * Process attachments included in a message. * * @param string[] $acceptedtypes String The mimetypes of the acceptable attachment types. * @param \context_user $context context_user The context of the user creating this attachment. * @param int $itemid int The itemid to store this attachment under. * @param \stdClass $attachment stdClass The Attachment data to store. * @return \stored_file */ protected function process_attachment($acceptedtypes, \context_user $context, $itemid, \stdClass $attachment) { global $USER, $CFG; // Create the file record. $record = new \stdClass(); $record->filearea = 'draft'; $record->component = 'user'; $record->itemid = $itemid; $record->license = $CFG->sitedefaultlicense; $record->author = fullname($USER); $record->contextid = $context->id; $record->userid = $USER->id; // All files sent by e-mail should have a flat structure. $record->filepath = '/'; $record->filename = $attachment->filename; mtrace("--> Attaching {$record->filename} to " . "/{$record->contextid}/{$record->component}/{$record->filearea}/" . "{$record->itemid}{$record->filepath}{$record->filename}"); $fs = get_file_storage(); return $fs->create_file_from_string($record, $attachment->content); } /** * Return the content of any success notification to be sent. * Both an HTML and Plain Text variant must be provided. * * @param \stdClass $messagedata The message data. * @param \stdClass $handlerresult The record for the newly created post. * @return \stdClass with keys `html` and `plain`. */ public function get_success_message(\stdClass $messagedata, $handlerresult) { $a = new \stdClass(); $a->subject = $handlerresult->subject; $discussionurl = new \moodle_url('/mod/forum/discuss.php', array('d' => $handlerresult->discussion)); $discussionurl->set_anchor('p' . $handlerresult->id); $a->discussionurl = $discussionurl->out(); $message = new \stdClass(); $message->plain = get_string('postbymailsuccess', 'mod_forum', $a); $message->html = get_string('postbymailsuccess_html', 'mod_forum', $a); return $message; } } PK-B\GBGBclasses/post_form.phpnu[. /** * File containing the form definition to post in the forum. * * @package mod_forum * @copyright Jamie Pratt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/formslib.php'); require_once($CFG->dirroot . '/repository/lib.php'); /** * Class to post in a forum. * * @package mod_forum * @copyright Jamie Pratt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_forum_post_form extends moodleform { /** * Returns the options array to use in filemanager for forum attachments * * @param stdClass $forum * @return array */ public static function attachment_options($forum) { global $COURSE, $PAGE, $CFG; $maxbytes = get_user_max_upload_file_size($PAGE->context, $CFG->maxbytes, $COURSE->maxbytes, $forum->maxbytes); return array( 'subdirs' => 0, 'maxbytes' => $maxbytes, 'maxfiles' => $forum->maxattachments, 'accepted_types' => '*', 'return_types' => FILE_INTERNAL | FILE_CONTROLLED_LINK ); } /** * Returns the options array to use in forum text editor * * @param context_module $context * @param int $postid post id, use null when adding new post * @return array */ public static function editor_options(context_module $context, $postid) { global $COURSE, $PAGE, $CFG; // TODO: add max files and max size support $maxbytes = get_user_max_upload_file_size($PAGE->context, $CFG->maxbytes, $COURSE->maxbytes); return array( 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $maxbytes, 'trusttext'=> true, 'return_types'=> FILE_INTERNAL | FILE_EXTERNAL, 'subdirs' => file_area_contains_subdirs($context, 'mod_forum', 'post', $postid) ); } /** * Form definition * * @return void */ function definition() { global $CFG, $OUTPUT; $mform =& $this->_form; $coursecontext = $this->_customdata['coursecontext']; $modcontext = $this->_customdata['modcontext']; $forum = $this->_customdata['forum']; $post = $this->_customdata['post']; $subscribe = $this->_customdata['subscribe']; $edit = $this->_customdata['edit']; $thresholdwarning = $this->_customdata['thresholdwarning']; $canreplyprivately = array_key_exists('canreplyprivately', $this->_customdata) ? $this->_customdata['canreplyprivately'] : false; $inpagereply = $this->_customdata['inpagereply'] ?? false; if (!$inpagereply) { // Fill in the data depending on page params later using set_data. $mform->addElement('header', 'general', ''); } // If there is a warning message and we are not editing a post we need to handle the warning. if (!empty($thresholdwarning) && !$edit) { // Here we want to display a warning if they can still post but have reached the warning threshold. if ($thresholdwarning->canpost) { $message = get_string($thresholdwarning->errorcode, $thresholdwarning->module, $thresholdwarning->additional); $mform->addElement('html', $OUTPUT->notification($message, \core\output\notification::NOTIFY_INFO)); } } $mform->addElement('text', 'subject', get_string('subject', 'forum'), 'size="48"'); $mform->setType('subject', PARAM_TEXT); $mform->addRule('subject', get_string('required'), 'required', null, 'client'); $mform->addRule('subject', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); $mform->addElement('editor', 'message', get_string('message', 'forum'), null, self::editor_options($modcontext, (empty($post->id) ? null : $post->id))); $mform->setType('message', PARAM_RAW); $mform->addRule('message', get_string('required'), 'required', null, 'client'); if ($inpagereply) { $this->add_group_elements_if_applicable($mform); } else { $manageactivities = has_capability('moodle/course:manageactivities', $coursecontext); if (\mod_forum\subscriptions::is_forcesubscribed($forum)) { $mform->addElement('checkbox', 'discussionsubscribe', get_string('discussionsubscription', 'forum')); $mform->freeze('discussionsubscribe'); $mform->setDefaults('discussionsubscribe', 0); $mform->addHelpButton('discussionsubscribe', 'forcesubscribed', 'forum'); } else if (\mod_forum\subscriptions::subscription_disabled($forum) && !$manageactivities) { $mform->addElement('checkbox', 'discussionsubscribe', get_string('discussionsubscription', 'forum')); $mform->freeze('discussionsubscribe'); $mform->setDefaults('discussionsubscribe', 0); $mform->addHelpButton('discussionsubscribe', 'disallowsubscription', 'forum'); } else { $mform->addElement('checkbox', 'discussionsubscribe', get_string('discussionsubscription', 'forum')); $mform->addHelpButton('discussionsubscribe', 'discussionsubscription', 'forum'); } if (forum_can_create_attachment($forum, $modcontext)) { $mform->addElement('filemanager', 'attachments', get_string('attachment', 'forum'), null, self::attachment_options($forum)); $mform->addHelpButton('attachments', 'attachment', 'forum'); } if (!$post->parent && has_capability('mod/forum:pindiscussions', $modcontext)) { $mform->addElement('checkbox', 'pinned', get_string('discussionpinned', 'forum')); $mform->addHelpButton('pinned', 'discussionpinned', 'forum'); } if (empty($post->id) && ($manageactivities || ($forum->type == 'qanda' && has_capability('mod/forum:canmailnow', $modcontext))) ) { $mform->addElement('checkbox', 'mailnow', get_string('mailnow', 'forum')); } if ((empty($post->id) && $canreplyprivately) || (!empty($post) && !empty($post->privatereplyto))) { // Only show the option to change private reply settings if this is a new post and the user can reply // privately, or if this is already private reply, in which case the state is shown but is not editable. $mform->addElement('checkbox', 'isprivatereply', get_string('privatereply', 'forum')); $mform->addHelpButton('isprivatereply', 'privatereply', 'forum'); if (!empty($post->privatereplyto)) { $mform->setDefault('isprivatereply', 1); $mform->freeze('isprivatereply'); } } $this->add_group_elements_if_applicable($mform); if (!empty($CFG->forum_enabletimedposts) && !$post->parent && has_capability('mod/forum:viewhiddentimedposts', $coursecontext)) { $mform->addElement('header', 'displayperiod', get_string('displayperiod', 'forum')); $mform->addElement('date_time_selector', 'timestart', get_string('displaystart', 'forum'), array('optional' => true)); $mform->addHelpButton('timestart', 'displaystart', 'forum'); $mform->addElement('date_time_selector', 'timeend', get_string('displayend', 'forum'), array('optional' => true)); $mform->addHelpButton('timeend', 'displayend', 'forum'); } else { $mform->addElement('hidden', 'timestart'); $mform->setType('timestart', PARAM_INT); $mform->addElement('hidden', 'timeend'); $mform->setType('timeend', PARAM_INT); $mform->setConstants(array('timestart' => 0, 'timeend' => 0)); } if (core_tag_tag::is_enabled('mod_forum', 'forum_posts')) { $mform->addElement('header', 'tagshdr', get_string('tags', 'tag')); $mform->addElement('tags', 'tags', get_string('tags'), array('itemtype' => 'forum_posts', 'component' => 'mod_forum')); } } //------------------------------------------------------------------------------- // buttons if (isset($post->edit)) { // hack alert $submitstring = get_string('savechanges'); } else { $submitstring = get_string('posttoforum', 'forum'); } // Always register a no submit button so it can be picked up if redirecting to the original post form. $mform->registerNoSubmitButton('advancedadddiscussion'); // This is an inpage add discussion which requires custom buttons. if ($inpagereply) { $mform->addElement('hidden', 'discussionsubscribe'); $mform->setType('discussionsubscribe', PARAM_INT); $buttonarray = array(); $buttonarray[] = &$mform->createElement('submit', 'submitbutton', $submitstring); $buttonarray[] = &$mform->createElement('button', 'cancelbtn', get_string('cancel', 'core'), // Additional attribs to handle collapsible div. ['data-bs-toggle' => 'collapse', 'data-bs-target' => "#collapseAddForm"]); $buttonarray[] = &$mform->createElement('submit', 'advancedadddiscussion', get_string('showadvancededitor'), null, null, ['customclassoverride' => 'btn-link']); $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false); $mform->closeHeaderBefore('buttonar'); } else { $this->add_action_buttons(true, $submitstring); } $mform->addElement('hidden', 'course'); $mform->setType('course', PARAM_INT); $mform->addElement('hidden', 'forum'); $mform->setType('forum', PARAM_INT); $mform->addElement('hidden', 'discussion'); $mform->setType('discussion', PARAM_INT); $mform->addElement('hidden', 'parent'); $mform->setType('parent', PARAM_INT); $mform->addElement('hidden', 'groupid'); $mform->setType('groupid', PARAM_INT); $mform->addElement('hidden', 'edit'); $mform->setType('edit', PARAM_INT); $mform->addElement('hidden', 'reply'); $mform->setType('reply', PARAM_INT); } /** * Form validation * * @param array $data data from the form. * @param array $files files uploaded. * @return array of errors. */ function validation($data, $files) { $errors = parent::validation($data, $files); if (($data['timeend']!=0) && ($data['timestart']!=0) && $data['timeend'] <= $data['timestart']) { $errors['timeend'] = get_string('timestartenderror', 'forum'); } if (empty($data['message']['text'])) { $errors['message'] = get_string('erroremptymessage', 'forum'); } if (empty($data['subject'])) { $errors['subject'] = get_string('erroremptysubject', 'forum'); } return $errors; } /** * Adds elements to control what groups will have access to the post or exits when no relevant group mode is set. * * @param MoodleQuickForm $mform */ private function add_group_elements_if_applicable(MoodleQuickForm $mform): void { global $CFG; $course = $this->_customdata['course']; $cm = $this->_customdata['cm']; $forum = $this->_customdata['forum']; $modcontext = $this->_customdata['modcontext']; $post = $this->_customdata['post']; if (!groups_get_activity_groupmode($cm, $course)) { return; } $groupdata = groups_get_activity_allowed_groups($cm); $groupinfo = []; foreach ($groupdata as $groupid => $group) { // Check whether this user can post in this group. // We must make this check because all groups are returned for a visible grouped activity. if (forum_user_can_post_discussion($forum, $groupid, null, $cm, $modcontext)) { // Build the data for the groupinfo select. $groupinfo[$groupid] = format_string($group->name, true, ['context' => $modcontext]); } else { unset($groupdata[$groupid]); } } $groupcount = count($groupinfo); // Check whether a user can post to all of their own groups. // Posts to all of my groups are copied to each group that the user is a member of. Certain conditions must be met. // 1) It only makes sense to allow this when a user is in more than one group. // Note: This check must come before we consider adding accessallgroups, because that is not a real group. $canposttoowngroups = empty($post->edit) && $groupcount > 1; // 2) Important: You can *only* post to multiple groups for a top level post. Never any reply. $canposttoowngroups = $canposttoowngroups && empty($post->parent); // 3) You also need the canposttoowngroups capability. $canposttoowngroups = $canposttoowngroups && has_capability('mod/forum:canposttomygroups', $modcontext); if ($canposttoowngroups) { // This user is in multiple groups, and can post to all of their own groups. // Note: This is not the same as accessallgroups. This option will copy a post to all groups that a // user is a member of. $mform->addElement('checkbox', 'posttomygroups', get_string('posttomygroups', 'forum')); $mform->addHelpButton('posttomygroups', 'posttomygroups', 'forum'); $mform->disabledIf('groupinfo', 'posttomygroups', 'checked'); } // Check whether this user can post to all groups. // Posts to the 'All participants' group go to all groups, not to each group in a list. // It makes sense to allow this, even if there currently aren't any groups because there may be in the future. if (forum_user_can_post_discussion($forum, -1, null, $cm, $modcontext)) { // Note: We must reverse in this manner because array_unshift renumbers the array. $groupinfo = array_reverse($groupinfo, true); $groupinfo[-1] = get_string('allparticipants'); $groupinfo = array_reverse($groupinfo, true); $groupcount++; } // Determine whether the user can select a group from the dropdown. The dropdown is available for several reasons. // 1) This is a new post (not an edit), and there are at least two groups to choose from. $canselectgroupfornew = empty($post->edit) && $groupcount > 1; // 2) This is editing of an existing post and the user is allowed to movediscussions. // We allow this because the post may have been moved from another forum where groups are not available. // We show this even if no groups are available as groups *may* have been available but now are not. $canselectgroupformove = $groupcount && !empty($post->edit) && has_capability('mod/forum:movediscussions', $modcontext); // Important: You can *only* change the group for a top level post. Never any reply. $canselectgroup = empty($post->parent) && ($canselectgroupfornew || $canselectgroupformove); if ($canselectgroup) { $mform->addElement('select', 'groupinfo', get_string('group'), $groupinfo); $mform->setDefault('groupinfo', $post->groupid); $mform->setType('groupinfo', PARAM_INT); } else { if (empty($post->groupid)) { $groupname = get_string('allparticipants'); } else { $groupname = format_string($groupdata[$post->groupid]->name); } $mform->addElement('static', 'groupinfo', get_string('group'), $groupname); } } } PK-B\OPq q classes/h5p/canedit.phpnu[. namespace mod_forum\h5p; /** * Class to check if the H5P content can be edited for this plugin. * * @package mod_forum * @copyright 2021 Sara Arjona (sara@moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class canedit { /** * Check if the user can edit an H5P file. In that case, this method will return true if the file belongs to mod_forum * filearea is post and the user can edit the post where the H5P is. * * @param \stored_file $file The H5P file to check. * * @return boolean Whether the user can edit or not the given file. * @since Moodle 4.0 */ public static function can_edit_content(\stored_file $file): bool { global $USER; list($type, $component) = \core_component::normalize_component($file->get_component()); if ($type === 'mod' && $component === 'forum') { // For mod_forum files in posts, check if the user can edit the post where the H5P is. if ($file->get_filearea() === 'post') { // Check if the user can edit the forum post. $vaultfactory = \mod_forum\local\container::get_vault_factory(); $forumvault = $vaultfactory->get_forum_vault(); $discussionvault = $vaultfactory->get_discussion_vault(); $postvault = $vaultfactory->get_post_vault(); $postid = $file->get_itemid(); $postentity = $postvault->get_from_id($postid); if (!empty($postentity)) { $discussionentity = $discussionvault->get_from_id($postentity->get_discussion_id()); $managerfactory = \mod_forum\local\container::get_manager_factory(); $forumentity = $forumvault->get_from_id($discussionentity->get_forum_id()); $capabilitymanager = $managerfactory->get_capability_manager($forumentity); if ($capabilitymanager->can_edit_post($USER, $discussionentity, $postentity)) { return true; } } } else { // For any other fileare, check whether the user can add/edit them. $context = \context::instance_by_id($file->get_contextid()); $plugins = \core_component::get_plugin_list($type); $isvalid = array_key_exists($component, $plugins); if ($isvalid && has_capability("$type/$component:addinstance", $context)) { // The user can edit the content because she has the capability for creating instances where the file belongs. return true; } } } return false; } } PK-B\ʊxclasses/observer.phpnu[. /** * Event observers used in forum. * * @package mod_forum * @copyright 2013 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Event observer for mod_forum. */ class mod_forum_observer { /** * Triggered via user_enrolment_deleted event. * * @param \core\event\user_enrolment_deleted $event */ public static function user_enrolment_deleted(\core\event\user_enrolment_deleted $event) { global $DB; // NOTE: this has to be as fast as possible. // Get user enrolment info from event. $cp = (object)$event->other['userenrolment']; if ($cp->lastenrol) { if (!$forums = $DB->get_records('forum', array('course' => $cp->courseid), '', 'id')) { return; } list($forumselect, $params) = $DB->get_in_or_equal(array_keys($forums), SQL_PARAMS_NAMED); $params['userid'] = $cp->userid; $DB->delete_records_select('forum_digests', 'userid = :userid AND forum '.$forumselect, $params); $DB->delete_records_select('forum_subscriptions', 'userid = :userid AND forum '.$forumselect, $params); $DB->delete_records_select('forum_track_prefs', 'userid = :userid AND forumid '.$forumselect, $params); $DB->delete_records_select('forum_read', 'userid = :userid AND forumid '.$forumselect, $params); } } /** * Observer for role_assigned event. * * @param \core\event\role_assigned $event * @return void */ public static function role_assigned(\core\event\role_assigned $event) { global $CFG, $DB; $context = context::instance_by_id($event->contextid, MUST_EXIST); // If contextlevel is course then only subscribe user. Role assignment // at course level means user is enroled in course and can subscribe to forum. if ($context->contextlevel != CONTEXT_COURSE) { return; } // Forum lib required for the constant used below. require_once($CFG->dirroot . '/mod/forum/lib.php'); $userid = $event->relateduserid; $sql = "SELECT f.id, f.course as course, cm.id AS cmid, f.forcesubscribe FROM {forum} f JOIN {course_modules} cm ON (cm.instance = f.id) JOIN {modules} m ON (m.id = cm.module) LEFT JOIN {forum_subscriptions} fs ON (fs.forum = f.id AND fs.userid = :userid) WHERE f.course = :courseid AND f.forcesubscribe = :initial AND m.name = 'forum' AND fs.id IS NULL"; $params = array('courseid' => $context->instanceid, 'userid' => $userid, 'initial' => FORUM_INITIALSUBSCRIBE); $forums = $DB->get_records_sql($sql, $params); foreach ($forums as $forum) { // If user doesn't have allowforcesubscribe capability then don't subscribe. $modcontext = context_module::instance($forum->cmid); if (has_capability('mod/forum:allowforcesubscribe', $modcontext, $userid)) { \mod_forum\subscriptions::subscribe_user($userid, $forum, $modcontext); } } } /** * Observer for \core\event\course_module_created event. * * @param \core\event\course_module_created $event * @return void */ public static function course_module_created(\core\event\course_module_created $event) { global $CFG; if ($event->other['modulename'] === 'forum') { // Include the forum library to make use of the forum_instance_created function. require_once($CFG->dirroot . '/mod/forum/lib.php'); $forum = $event->get_record_snapshot('forum', $event->other['instanceid']); forum_instance_created($event->get_context(), $forum); } } /** * Observer for \core\event\course_created event. * * @param \core\event\course_created $event * @return void */ public static function course_created(\core\event\course_created $event) { global $CFG; $course = $event->get_record_snapshot('course', $event->objectid); $format = course_get_format($course); $courseformat = $format->get_format(); $forumformat = ''; if ($format->supports_news() && !empty($course->newsitems)) { $forumformat = 'news'; } else if ($courseformat === 'social') { $forumformat = 'social'; } if ($forumformat) { require_once($CFG->dirroot . '/mod/forum/lib.php'); // Auto-create the course forum if necessary. forum_get_course_forum($event->objectid, $forumformat); } } /** * Observer for \core\event\course_updated event. * * @param \core\event\course_updated $event * @return void */ public static function course_updated(\core\event\course_updated $event) { global $CFG; $course = $event->get_record_snapshot('course', $event->objectid); $format = course_get_format($course); $courseformat = $format->get_format(); $forumformat = ''; if ($format->supports_news() && !empty($course->newsitems)) { $forumformat = 'news'; } else if ($courseformat === 'social') { $forumformat = 'social'; } if ($forumformat) { require_once($CFG->dirroot . '/mod/forum/lib.php'); // Auto-create the course forum if necessary. forum_get_course_forum($event->objectid, $forumformat); } } } PK-B\2 RR"classes/plugininfo/forumreport.phpnu[. /** * Subplugin info class. * * @package mod_forum * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\plugininfo; use core\plugininfo\base; defined('MOODLE_INTERNAL') || die(); /** * Forum report subplugin info class. * * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class forumreport extends base { /** * Allow the forum report subplugin be uninstalled. * * @return boolean */ public function is_uninstall_allowed() { return true; } } PK-B\~))classes/search/post.phpnu[. /** * Forum posts search area * * @package mod_forum * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\search; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/forum/lib.php'); /** * Forum posts search area. * * @package mod_forum * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class post extends \core_search\base_mod { /** * @var array Internal quick static cache. */ protected $forumsdata = array(); /** * @var array Internal quick static cache. */ protected $discussionsdata = array(); /** * @var array Internal quick static cache. */ protected $postsdata = array(); /** * Returns recordset containing required data for indexing forum posts. * * @param int $modifiedfrom timestamp * @param \context|null $context Optional context to restrict scope of returned results * @return moodle_recordset|null Recordset (or null if no results) */ public function get_document_recordset($modifiedfrom = 0, ?\context $context = null) { global $DB; list ($contextjoin, $contextparams) = $this->get_context_restriction_sql( $context, 'forum', 'f'); if ($contextjoin === null) { return null; } $sql = "SELECT fp.*, f.id AS forumid, f.course AS courseid, fd.groupid AS groupid FROM {forum_posts} fp JOIN {forum_discussions} fd ON fd.id = fp.discussion JOIN {forum} f ON f.id = fd.forum $contextjoin WHERE fp.modified >= ? ORDER BY fp.modified ASC"; return $DB->get_recordset_sql($sql, array_merge($contextparams, [$modifiedfrom])); } /** * Returns the document associated with this post id. * * @param stdClass $record Post info. * @param array $options * @return \core_search\document */ public function get_document($record, $options = array()) { try { $cm = $this->get_cm('forum', $record->forumid, $record->courseid); $context = \context_module::instance($cm->id); } catch (\dml_missing_record_exception $ex) { // Notify it as we run here as admin, we should see everything. debugging('Error retrieving ' . $this->areaid . ' ' . $record->id . ' document, not all required data is available: ' . $ex->getMessage(), DEBUG_DEVELOPER); return false; } catch (\dml_exception $ex) { // Notify it as we run here as admin, we should see everything. debugging('Error retrieving ' . $this->areaid . ' ' . $record->id . ' document: ' . $ex->getMessage(), DEBUG_DEVELOPER); return false; } // Prepare associative array with data from DB. $doc = \core_search\document_factory::instance($record->id, $this->componentname, $this->areaname); $doc->set('title', content_to_text($record->subject, false)); $doc->set('content', content_to_text($record->message, $record->messageformat)); $doc->set('contextid', $context->id); $doc->set('courseid', $record->courseid); $doc->set('userid', $record->userid); $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID); $doc->set('modified', $record->modified); // Store group id if there is one. (0 and -1 both mean not restricted to group.) if ($record->groupid > 0) { $doc->set('groupid', $record->groupid); } // Check if this document should be considered new. if (isset($options['lastindexedtime']) && ($options['lastindexedtime'] < $record->created)) { // If the document was created after the last index time, it must be new. $doc->set_is_new(true); } return $doc; } /** * Returns true if this area uses file indexing. * * @return bool */ public function uses_file_indexing() { return true; } /** * Return the context info required to index files for * this search area. * * @return array */ public function get_search_fileareas() { $fileareas = array( 'attachment', 'post' ); return $fileareas; } /** * Add the forum post attachments. * * @param document $document The current document * @return null */ public function attach_files($document) { global $DB; $postid = $document->get('itemid'); try { $post = $this->get_post($postid); } catch (\dml_missing_record_exception $e) { unset($this->postsdata[$postid]); debugging('Could not get record to attach files to '.$document->get('id'), DEBUG_DEVELOPER); return; } // Because this is used during indexing, we don't want to cache posts. Would result in memory leak. unset($this->postsdata[$postid]); $cm = $this->get_cm($this->get_module_name(), $post->forum, $document->get('courseid')); $context = \context_module::instance($cm->id); $contextid = $context->id; $fileareas = $this->get_search_fileareas(); $component = $this->get_component_name(); // Get the files and attach them. foreach ($fileareas as $filearea) { $fs = get_file_storage(); $files = $fs->get_area_files($contextid, $component, $filearea, $postid, '', false); foreach ($files as $file) { $document->add_stored_file($file); } } } /** * Whether the user can access the document or not. * * @throws \dml_missing_record_exception * @throws \dml_exception * @param int $id Forum post id * @return bool */ public function check_access($id) { global $USER; try { $post = $this->get_post($id); $forum = $this->get_forum($post->forum); $discussion = $this->get_discussion($post->discussion); $cminfo = $this->get_cm('forum', $forum->id, $forum->course); $cm = $cminfo->get_course_module_record(); } catch (\dml_missing_record_exception $ex) { return \core_search\manager::ACCESS_DELETED; } catch (\dml_exception $ex) { return \core_search\manager::ACCESS_DENIED; } // Recheck uservisible although it should have already been checked in core_search. if ($cminfo->uservisible === false) { return \core_search\manager::ACCESS_DENIED; } if (!forum_user_can_see_post($forum, $discussion, $post, $USER, $cm)) { return \core_search\manager::ACCESS_DENIED; } return \core_search\manager::ACCESS_GRANTED; } /** * Link to the forum post discussion * * @param \core_search\document $doc * @return \moodle_url */ public function get_doc_url(\core_search\document $doc) { // The post is already in static cache, we fetch it in self::search_access. $post = $this->get_post($doc->get('itemid')); return new \moodle_url('/mod/forum/discuss.php', array('d' => $post->discussion)); } /** * Link to the forum. * * @param \core_search\document $doc * @return \moodle_url */ public function get_context_url(\core_search\document $doc) { $contextmodule = \context::instance_by_id($doc->get('contextid')); return new \moodle_url('/mod/forum/view.php', array('id' => $contextmodule->instanceid)); } /** * Returns the specified forum post from its internal cache. * * @throws \dml_missing_record_exception * @param int $postid * @return stdClass */ protected function get_post($postid) { if (empty($this->postsdata[$postid])) { $this->postsdata[$postid] = forum_get_post_full($postid); if (!$this->postsdata[$postid]) { throw new \dml_missing_record_exception('forum_posts'); } } return $this->postsdata[$postid]; } /** * Returns the specified forum checking the internal cache. * * Store minimal information as this might grow. * * @throws \dml_exception * @param int $forumid * @return stdClass */ protected function get_forum($forumid) { global $DB; if (empty($this->forumsdata[$forumid])) { $this->forumsdata[$forumid] = $DB->get_record('forum', array('id' => $forumid), '*', MUST_EXIST); } return $this->forumsdata[$forumid]; } /** * Returns the discussion checking the internal cache. * * @throws \dml_missing_record_exception * @param int $discussionid * @return stdClass */ protected function get_discussion($discussionid) { global $DB; if (empty($this->discussionsdata[$discussionid])) { $this->discussionsdata[$discussionid] = $DB->get_record('forum_discussions', array('id' => $discussionid), '*', MUST_EXIST); } return $this->discussionsdata[$discussionid]; } /** * Changes the context ordering so that the forums with most recent discussions are indexed * first. * * @return string[] SQL join and ORDER BY */ protected function get_contexts_to_reindex_extra_sql() { return [ 'JOIN {forum_discussions} fd ON fd.course = cm.course AND fd.forum = cm.instance', 'MAX(fd.timemodified) DESC' ]; } /** * Confirms that data entries support group restrictions. * * @return bool True */ public function supports_group_restriction() { return true; } } PK-B\Ôxxclasses/search/activity.phpnu[. /** * Forum activities search area * * @package mod_forum * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\search; defined('MOODLE_INTERNAL') || die(); /** * Forum activities search area. * * @package mod_forum * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class activity extends \core_search\base_activity { /** * Returns true if this area uses file indexing. * * @return bool */ public function uses_file_indexing() { return true; } } PK-B\c c $classes/subscriber_selector_base.phpnu[. /** * A type of forum. * * @package mod_forum * @copyright 2014 Andrew Robert Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/user/selector/lib.php'); /** * Abstract class used by forum subscriber selection controls * @package mod_forum * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class mod_forum_subscriber_selector_base extends user_selector_base { /** * The id of the forum this selector is being used for * @var int */ protected $forumid = null; /** * The context of the forum this selector is being used for * @var object */ protected $context = null; /** * The id of the current group * @var int */ protected $currentgroup = null; /** * Constructor method * @param string $name * @param array $options */ public function __construct($name, $options) { $options['accesscontext'] = $options['context']; parent::__construct($name, $options); if (isset($options['context'])) { $this->context = $options['context']; } if (isset($options['currentgroup'])) { $this->currentgroup = $options['currentgroup']; } if (isset($options['forumid'])) { $this->forumid = $options['forumid']; } } /** * Returns an array of options to seralise and store for searches * * @return array */ protected function get_options() { global $CFG; $options = parent::get_options(); $options['file'] = substr(__FILE__, strlen($CFG->dirroot.'/')); $options['context'] = $this->context; $options['currentgroup'] = $this->currentgroup; $options['forumid'] = $this->forumid; return $options; } } PK-B\/G/Gclasses/output/forum_post.phpnu[. /** * Forum post renderable. * * @package mod_forum * @copyright 2015 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\output; defined('MOODLE_INTERNAL') || die(); /** * Forum post renderable. * * @copyright 2015 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * * @property boolean $viewfullnames Whether to override fullname() */ class forum_post implements \renderable, \templatable { /** * The course that the forum post is in. * * @var object $course */ protected $course = null; /** * The course module for the forum. * * @var object $cm */ protected $cm = null; /** * The forum that the post is in. * * @var object $forum */ protected $forum = null; /** * The discussion that the forum post is in. * * @var object $discussion */ protected $discussion = null; /** * The forum post being displayed. * * @var object $post */ protected $post = null; /** * Whether the user can reply to this post. * * @var boolean $canreply */ protected $canreply = false; /** * Whether to override forum display when displaying usernames. * @var boolean $viewfullnames */ protected $viewfullnames = false; /** * The user that is reading the post. * * @var object $userto */ protected $userto = null; /** * The user that wrote the post. * * @var object $author */ protected $author = null; /** * An associative array indicating which keys on this object should be writeable. * * @var array $writablekeys */ protected $writablekeys = array( 'viewfullnames' => true, ); /** @var \stdClass user record. */ protected $userfrom; /** * Builds a renderable forum post * * @param object $course Course of the forum * @param object $cm Course Module of the forum * @param object $forum The forum of the post * @param object $discussion Discussion thread in which the post appears * @param object $post The post * @param object $author Author of the post * @param object $recipient Recipient of the email * @param bool $canreply True if the user can reply to the post */ public function __construct($course, $cm, $forum, $discussion, $post, $author, $recipient, $canreply) { $this->course = $course; $this->cm = $cm; $this->forum = $forum; $this->discussion = $discussion; $this->post = $post; $this->author = $author; $this->userto = $recipient; $this->canreply = $canreply; } /** * Export this data so it can be used as the context for a mustache template. * * @param \mod_forum_renderer $renderer The render to be used for formatting the message and attachments * @param bool $plaintext Whethe the target is a plaintext target * @return array Data ready for use in a mustache template */ public function export_for_template(\renderer_base $renderer, $plaintext = false) { if ($plaintext) { return $this->export_for_template_text($renderer); } else { return $this->export_for_template_html($renderer); } } /** * Export this data so it can be used as the context for a mustache template. * * @param \mod_forum_renderer $renderer The render to be used for formatting the message and attachments * @return array Data ready for use in a mustache template */ protected function export_for_template_text(\mod_forum_renderer $renderer) { $data = $this->export_for_template_shared($renderer); return $data + array( 'id' => html_entity_decode($this->post->id, ENT_COMPAT), 'coursename' => html_entity_decode($this->get_coursename(), ENT_COMPAT), 'courselink' => html_entity_decode($this->get_courselink(), ENT_COMPAT), 'forumname' => html_entity_decode($this->get_forumname(), ENT_COMPAT), 'showdiscussionname' => html_entity_decode($this->get_showdiscussionname(), ENT_COMPAT), 'discussionname' => html_entity_decode($this->get_discussionname(), ENT_COMPAT), 'subject' => html_entity_decode($this->get_subject(), ENT_COMPAT), 'authorfullname' => html_entity_decode($this->get_author_fullname(), ENT_COMPAT), 'postdate' => html_entity_decode($this->get_postdate(), ENT_COMPAT), // Format some components according to the renderer. 'message' => html_entity_decode($renderer->format_message_text($this->cm, $this->post), ENT_COMPAT), 'attachments' => html_entity_decode($renderer->format_message_attachments($this->cm, $this->post), ENT_COMPAT), 'canreply' => $this->canreply, 'permalink' => $this->get_permalink(), 'firstpost' => $this->get_is_firstpost(), 'replylink' => $this->get_replylink(), 'unsubscribediscussionlink' => $this->get_unsubscribediscussionlink(), 'unsubscribeforumlink' => $this->get_unsubscribeforumlink(), 'parentpostlink' => $this->get_parentpostlink(), 'forumindexlink' => $this->get_forumindexlink(), 'forumviewlink' => $this->get_forumviewlink(), 'discussionlink' => $this->get_discussionlink(), 'authorlink' => $this->get_authorlink(), 'authorpicture' => $this->get_author_picture($renderer), 'grouppicture' => $this->get_group_picture($renderer), ); } /** * Export this data so it can be used as the context for a mustache template. * * @param \mod_forum_renderer $renderer The render to be used for formatting the message and attachments * @return array Data ready for use in a mustache template */ protected function export_for_template_html(\mod_forum_renderer $renderer) { $data = $this->export_for_template_shared($renderer); return $data + array( 'id' => $this->post->id, 'coursename' => $this->get_coursename(), 'courselink' => $this->get_courselink(), 'forumname' => $this->get_forumname(), 'showdiscussionname' => $this->get_showdiscussionname(), 'discussionname' => $this->get_discussionname(), 'subject' => $this->get_subject(), 'authorfullname' => $this->get_author_fullname(), 'postdate' => $this->get_postdate(), // Format some components according to the renderer. 'message' => $renderer->format_message_text($this->cm, $this->post), 'attachments' => $renderer->format_message_attachments($this->cm, $this->post), ); } /** * Export this data so it can be used as the context for a mustache template. * * @param \mod_forum_renderer $renderer The render to be used for formatting the message and attachments * @return stdClass Data ready for use in a mustache template */ protected function export_for_template_shared(\mod_forum_renderer $renderer) { return array( 'canreply' => $this->canreply, 'permalink' => $this->get_permalink(), 'firstpost' => $this->get_is_firstpost(), 'replylink' => $this->get_replylink(), 'unsubscribediscussionlink' => $this->get_unsubscribediscussionlink(), 'unsubscribeforumlink' => $this->get_unsubscribeforumlink(), 'parentpostlink' => $this->get_parentpostlink(), 'forumindexlink' => $this->get_forumindexlink(), 'forumviewlink' => $this->get_forumviewlink(), 'discussionlink' => $this->get_discussionlink(), 'authorlink' => $this->get_authorlink(), 'authorpicture' => $this->get_author_picture($renderer), 'grouppicture' => $this->get_group_picture($renderer), 'isprivatereply' => !empty($this->post->privatereplyto), ); } /** * Magically sets a property against this object. * * @param string $key * @param mixed $value */ public function __set($key, $value) { // First attempt to use the setter function. $methodname = 'set_' . $key; if (method_exists($this, $methodname)) { return $this->{$methodname}($value); } // Fall back to the writable keys list. if (isset($this->writablekeys[$key]) && $this->writablekeys[$key]) { return $this->{$key} = $value; } // Throw an error rather than fail silently. throw new \coding_exception('Tried to set unknown property "' . $key . '"'); } /** * Whether this is the first post. * * @return boolean */ public function get_is_firstpost() { return empty($this->post->parent); } /** * Get the link to the course. * * @return string */ public function get_courselink() { $link = new \moodle_url( // Posts are viewed on the topic. '/course/view.php', array( 'id' => $this->course->id, ) ); return $link->out(false); } /** * Get the link to the forum index for this course. * * @return string */ public function get_forumindexlink() { $link = new \moodle_url( // Posts are viewed on the topic. '/mod/forum/index.php', array( 'id' => $this->course->id, ) ); return $link->out(false); } /** * Get the link to the view page for this forum. * * @return string */ public function get_forumviewlink() { $link = new \moodle_url( // Posts are viewed on the topic. '/mod/forum/view.php', array( 'f' => $this->forum->id, ) ); return $link->out(false); } /** * Get the link to the current discussion. * * @return string */ protected function _get_discussionlink() { return new \moodle_url( // Posts are viewed on the topic. '/mod/forum/discuss.php', array( // Within a discussion. 'd' => $this->discussion->id, ) ); } /** * Get the link to the current discussion. * * @return string */ public function get_discussionlink() { $link = $this->_get_discussionlink(); return $link->out(false); } /** * Get the link to the current post, including post anchor. * * @return string */ public function get_permalink() { $link = $this->_get_discussionlink(); $link->set_anchor($this->get_postanchor()); return $link->out(false); } /** * Get the link to the parent post. * * @return string */ public function get_parentpostlink() { $link = $this->_get_discussionlink(); $link->param('parent', $this->post->parent); return $link->out(false); } /** * Get the link to the author's profile page. * * @return string */ public function get_authorlink() { $link = new \moodle_url( '/user/view.php', array( 'id' => $this->post->userid, 'course' => $this->course->id, ) ); return $link->out(false); } /** * Get the link to unsubscribe from the forum. * * @return string */ public function get_unsubscribeforumlink() { if (!\mod_forum\subscriptions::is_subscribable($this->forum)) { return null; } $link = new \moodle_url( '/mod/forum/subscribe.php', array( 'id' => $this->forum->id, ) ); return $link->out(false); } /** * Get the link to unsubscribe from the discussion. * * @return string */ public function get_unsubscribediscussionlink() { if (!\mod_forum\subscriptions::is_subscribable($this->forum)) { return null; } $link = new \moodle_url( '/mod/forum/subscribe.php', array( 'id' => $this->forum->id, 'd' => $this->discussion->id, ) ); return $link->out(false); } /** * Get the link to reply to the current post. * * @return string */ public function get_replylink() { return new \moodle_url( '/mod/forum/post.php', array( 'reply' => $this->post->id, ) ); } /** * The formatted subject for the current post. * * @return string */ public function get_subject() { return format_string($this->post->subject, true); } /** * The plaintext anchor id for the current post. * * @return string */ public function get_postanchor() { return 'p' . $this->post->id; } /** * ID number of the course that the forum is in. * * @return string */ public function get_courseidnumber() { return s($this->course->idnumber); } /** * The full name of the course that the forum is in. * * @return string */ public function get_coursefullname() { return format_string($this->course->fullname, true, array( 'context' => \context_course::instance($this->course->id), )); } /** * The name of the course that the forum is in. * * @return string */ public function get_coursename() { return format_string($this->course->shortname, true, array( 'context' => \context_course::instance($this->course->id), )); } /** * The name of the forum. * * @return string */ public function get_forumname() { return format_string($this->forum->name, true); } /** * The name of the current discussion. * * @return string */ public function get_discussionname() { return format_string($this->discussion->name, true); } /** * Whether to show the discussion name. * If the forum name matches the discussion name, the discussion name * is not typically displayed. * * @return boolean */ public function get_showdiscussionname() { return ($this->forum->name !== $this->discussion->name); } /** * The fullname of the post author. * * @return string */ public function get_author_fullname() { return fullname($this->author, $this->viewfullnames); } /** * The recipient of the post. * * @return string */ protected function get_postto() { global $USER; if (null === $this->userto) { return $USER; } return $this->userto; } /** * The date of the post, formatted according to the postto user's * preferences. * * @return string */ public function get_postdate() { global $CFG; $postmodified = $this->post->modified; if (!empty($CFG->forum_enabletimedposts) && ($this->discussion->timestart > $postmodified)) { $postmodified = $this->discussion->timestart; } return userdate($postmodified, "", \core_date::get_user_timezone($this->get_postto())); } /** * The HTML for the author's user picture. * * @param \renderer_base $renderer * @return string */ public function get_author_picture(\renderer_base $renderer) { return $renderer->user_picture($this->author, array('courseid' => $this->course->id)); } /** * The HTML for a group picture. * * @param \renderer_base $renderer * @return string */ public function get_group_picture(\renderer_base $renderer) { if (isset($this->userfrom->groups)) { $groups = $this->userfrom->groups[$this->forum->id]; } else { $groups = groups_get_all_groups($this->course->id, $this->author->id, $this->cm->groupingid); } if ($this->get_is_firstpost()) { return print_group_picture($groups, $this->course->id, false, true, true, true); } } } PK-B\HVV"classes/output/forum_actionbar.phpnu[. namespace mod_forum\output; use renderable; use renderer_base; use templatable; use moodle_url; use help_icon; use mod_forum\local\entities\forum as forum_entity; /** * Render activity page for tertiary nav * * Render elements search forum, add new discussion button and subscribe all * to the page action. * * @package mod_forum * @copyright 2021 Sujith Haridasan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class forum_actionbar implements renderable, templatable { /** * @var forum_entity $forum */ private $forum; /** * @var \stdClass $course */ private $course; /** * @var mixed $groupid */ private $groupid; /** * @var string $search */ private $search; /** * forum_actionbar constructor. * * @param forum_entity $forum The forum object. * @param \stdClass $course The course object. * @param int|null $groupid The group id. * @param string $search The search string. */ public function __construct(forum_entity $forum, \stdClass $course, ?int $groupid, string $search) { $this->forum = $forum; $this->course = $course; $this->groupid = $groupid; $this->search = $search; } /** * Render the new discussion button. * * @return string HTML button */ private function get_new_discussion_topic_button(): string { global $USER; $renderfactory = \mod_forum\local\container::get_renderer_factory(); $discussionrenderer = $renderfactory->get_discussion_list_renderer($this->forum); return $discussionrenderer->render_new_discussion($USER, $this->groupid); } /** * Data for the template. * * @param renderer_base $output The render_base object. * @return array data for the template */ public function export_for_template(renderer_base $output): array { global $USER; $actionurl = (new moodle_url('/mod/forum/search.php'))->out(false); $helpicon = new help_icon('search', 'core'); $hiddenfields = [ (object) ['name' => 'id', 'value' => $this->course->id], ]; $shownewdiscussionbtn = ''; if ($this->forum->get_type() !== 'single') { $shownewdiscussionbtn = $this->get_new_discussion_topic_button(); } $data = [ 'action' => $actionurl, 'hiddenfields' => $hiddenfields, 'query' => $this->search, 'helpicon' => $helpicon->export_for_template($output), 'inputname' => 'search', 'searchstring' => get_string('searchforums', 'mod_forum'), 'newdiscussionbtn' => $shownewdiscussionbtn, ]; $legacydatamapperfactory = \mod_forum\local\container::get_legacy_data_mapper_factory(); $forumobject = $legacydatamapperfactory->get_forum_data_mapper()->to_legacy_object($this->forum); $context = $this->forum->get_context(); $activeenrolled = is_enrolled($context, $USER, '', true); $canmanage = has_capability('mod/forum:managesubscriptions', $context); $cansubscribe = $activeenrolled && !($this->forum->get_subscription_mode() === FORUM_FORCESUBSCRIBE) && (!($this->forum->get_subscription_mode() === FORUM_DISALLOWSUBSCRIBE) || $canmanage); if ($cansubscribe) { $returnurl = (new moodle_url('/mod/forum/view.php', ['id' => $this->forum->get_course_module_record()->id]))->out(false); if (!\mod_forum\subscriptions::is_subscribed($USER->id, $forumobject, null, $this->forum->get_course_module_record())) { $data['subscribetoforum'] = (new moodle_url( '/mod/forum/subscribe.php', ['id' => $forumobject->id, 'sesskey' => sesskey(), 'returnurl' => $returnurl] ))->out(false); } else { $data['unsubscribefromforum'] = (new moodle_url( '/mod/forum/subscribe.php', ['id' => $forumobject->id, 'sesskey' => sesskey(), 'returnurl' => $returnurl] ))->out(false); } } return $data; } } PK-B\&!!)classes/output/subscription_actionbar.phpnu[. namespace mod_forum\output; use moodle_url; use renderer_base; use url_select; use renderable; use templatable; /** * Renders the subscribers page for this activity. * * @package mod_forum * @copyright 2021 Sujith Haridasan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class subscription_actionbar implements renderable, templatable { /** @var int course id */ private $id; /** @var moodle_url */ private $currenturl; /** @var \stdClass */ private $forum; /** @var int */ private $edit; /** * subscription_actionbar constructor. * * @param int $id The forum id. * @param moodle_url $currenturl Current URL. * @param \stdClass $forum The forum object. * @param int $edit This argument decides to show view/manage subscribers view. */ public function __construct(int $id, moodle_url $currenturl, \stdClass $forum, int $edit) { $this->id = $id; $this->currenturl = $currenturl; $this->forum = $forum; $this->edit = $edit; } /** * Create url select menu for subscription option * * @return url_select|null the url_select object */ private function create_subscription_menu(): ?url_select { // When user is on manage subscription, we don't have to show the subscription selector. if ($this->edit === 1 && !\mod_forum\subscriptions::is_forcesubscribed($this->forum)) { return null; } $sesskey = sesskey(); $modeset = \mod_forum\subscriptions::get_subscription_mode($this->forum); $optionallink = new moodle_url('/mod/forum/subscribe.php', ['id' => $this->id, 'mode' => FORUM_CHOOSESUBSCRIBE, 'sesskey' => $sesskey, 'edit' => $this->edit]); $forcedlink = new moodle_url('/mod/forum/subscribe.php', ['id' => $this->id, 'mode' => FORUM_FORCESUBSCRIBE, 'sesskey' => $sesskey, 'edit' => $this->edit]); $autolink = new moodle_url('/mod/forum/subscribe.php', ['id' => $this->id, 'mode' => FORUM_INITIALSUBSCRIBE, 'sesskey' => $sesskey, 'edit' => $this->edit]); $disabledlink = new moodle_url('/mod/forum/subscribe.php', ['id' => $this->id, 'mode' => FORUM_DISALLOWSUBSCRIBE, 'sesskey' => $sesskey, 'edit' => $this->edit]); $menu = [ $optionallink->out(false) => get_string('subscriptionoptional', 'forum'), $forcedlink->out(false) => get_string('subscriptionforced', 'forum'), $autolink->out(false) => get_string('subscriptionauto', 'forum'), $disabledlink->out(false) => get_string('subscriptiondisabled', 'forum'), ]; switch ($modeset) { case FORUM_CHOOSESUBSCRIBE: $set = get_string('subscriptionoptional', 'forum'); break; case FORUM_FORCESUBSCRIBE: $set = get_string('subscriptionforced', 'forum'); break; case FORUM_INITIALSUBSCRIBE: $set = get_string('subscriptionauto', 'forum'); break; case FORUM_DISALLOWSUBSCRIBE: $set = get_string('subscriptiondisabled', 'forum'); break; default: throw new \moodle_exception(get_string('invalidforcesubscribe', 'forum')); } $menu = array_filter($menu, function($key) use ($set) { if ($key !== $set) { return true; } }); $urlselect = new url_select($menu, $this->currenturl, ['' => $set], 'selectsubscriptionoptions'); $urlselect->set_label(get_string('subscriptionmode', 'mod_forum'), ['class' => 'me-1']); $urlselect->set_help_icon('subscriptionmode', 'mod_forum'); $urlselect->class .= ' float-end'; return $urlselect; } /** * Create view and manage subscribers select menu. * * @return url_select|null get url_select object. */ private function create_view_manage_menu(): ?url_select { // If forced subscription is used then no need to show the view. if (\mod_forum\subscriptions::is_forcesubscribed($this->forum)) { return null; } $viewlink = new moodle_url('/mod/forum/subscribers.php', ['id' => $this->id, 'edit' => 'off']); $managelink = new moodle_url('/mod/forum/subscribers.php', ['id' => $this->id, 'edit' => 'on']); $menu = [ $viewlink->out(false) => get_string('forum:viewsubscribers', 'forum'), $managelink->out(false) => get_string('managesubscriptionson', 'forum'), ]; if ($this->edit === 0) { $this->currenturl = $viewlink; } else { $this->currenturl = $managelink; } $urlselect = new url_select($menu, $this->currenturl->out(false), null, 'selectviewandmanagesubscribers'); $urlselect->set_label(get_string('subscribers', 'forum'), ['class' => 'accesshide']); return $urlselect; } /** * Data for the template. * * @param renderer_base $output The render_base object. * @return array data for template */ public function export_for_template(renderer_base $output): array { $data = []; $subscribeoptionselect = $this->create_subscription_menu(); $viewmanageselect = $this->create_view_manage_menu(); if ($subscribeoptionselect) { $data ['subscriptionoptions'] = $subscribeoptionselect->export_for_template($output); } if ($viewmanageselect) { $data['viewandmanageselect'] = $viewmanageselect->export_for_template($output); } return $data; } } PK-B\O !classes/output/email/renderer.phpnu[. /** * Forum post renderable. * * @package mod_forum * @copyright 2015 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\output\email; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/../../../renderer.php'); /** * Forum post renderable. * * @since Moodle 3.0 * @package mod_forum * @copyright 2015 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class renderer extends \mod_forum_renderer { /** * The template name for this renderer. * * @return string */ public function forum_post_template() { return 'forum_post_email_htmlemail'; } /** * The HTML version of the e-mail message. * * @param \stdClass $cm * @param \stdClass $post * @return string */ public function format_message_text($cm, $post) { $context = \context_module::instance($cm->id); $message = file_rewrite_pluginfile_urls( $post->message, 'pluginfile.php', $context->id, 'mod_forum', 'post', $post->id, [ 'includetoken' => true, ]); $options = new \stdClass(); $options->para = true; $options->context = $context; $options->filter = false; // Disable auto-filters to apply them manually below. // Apply filters, excluding those incompatible with email output. $skipfilters = ['mediaplugin']; $message = \core_filters\filter_manager::instance()->filter_text($message, $context, [], $skipfilters); return format_text($message, $post->messageformat, $options); } /** * The HTML version of the attachments list. * * @param \stdClass $cm * @param \stdClass $post * @return string */ public function format_message_attachments($cm, $post) { return forum_print_attachments($post, $cm, "html"); } } PK-B\qcc+classes/output/email/renderer_textemail.phpnu[. /** * Forum post renderable. * * @package mod_forum * @copyright 2015 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\output\email; defined('MOODLE_INTERNAL') || die(); /** * Forum post renderable. * * @since Moodle 3.0 * @package mod_forum * @copyright 2015 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class renderer_textemail extends renderer { /** * The template name for this renderer. * * @return string */ public function forum_post_template() { return 'forum_post_email_textemail'; } /** * The plaintext version of the e-mail message. * * @param \stdClass $cm * @param \stdClass $post * @return string */ public function format_message_text($cm, $post) { $message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', \context_module::instance($cm->id)->id, 'mod_forum', 'post', $post->id); return format_text_email($message, $post->messageformat); } /** * The plaintext version of the attachments list. * * @param \stdClass $cm * @param \stdClass $post * @return string */ public function format_message_attachments($cm, $post) { return forum_print_attachments($post, $cm, "text"); } } PK-B\hN[#classes/output/forum_post_email.phpnu[. /** * Forum post renderable for e-mail. * * @package mod_forum * @copyright 2015 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\output; defined('MOODLE_INTERNAL') || die(); /** * Forum post renderable for use in e-mail. * * @copyright 2015 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class forum_post_email extends forum_post { } PK-B\h틝,classes/output/emaildigestbasic/renderer.phpnu[. /** * Forum post renderable. * * @package mod_forum * @copyright 2015 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\output\emaildigestbasic; defined('MOODLE_INTERNAL') || die(); /** * Forum post renderable. * * @since Moodle 3.0 * @package mod_forum * @copyright 2015 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class renderer extends \mod_forum\output\email\renderer { /** * The template name for this renderer. * * @return string */ public function forum_post_template() { return 'forum_post_emaildigestbasic_htmlemail'; } } PK-B\66classes/output/emaildigestbasic/renderer_textemail.phpnu[. /** * Forum post renderable. * * @package mod_forum * @copyright 2015 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\output\emaildigestbasic; defined('MOODLE_INTERNAL') || die(); /** * Forum post renderable. * * @since Moodle 3.0 * @package mod_forum * @copyright 2015 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class renderer_textemail extends \mod_forum\output\email\renderer_textemail { /** * The template name for this renderer. * * @return string */ public function forum_post_template() { return 'forum_post_emaildigestbasic_textemail'; } /** * The plaintext version of the e-mail message. * * @param \stdClass $cm * @param \stdClass $post * @return string */ public function format_message_text($cm, $post) { $context = \context_module::instance($cm->id); $message = file_rewrite_pluginfile_urls( $post->message, 'pluginfile.php', $context->id, 'mod_forum', 'post', $post->id, [ 'includetoken' => true, ]); return format_text_email($message, $post->messageformat); } } PK-B\ &fg "classes/output/big_search_form.phpnu[. /** * Big search form. * * @package mod_forum * @copyright 2016 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\output; defined('MOODLE_INTERNAL') || die(); use html_writer; use moodle_url; use renderable; use renderer_base; use stdClass; use templatable; /** * Big search form class. * * @package mod_forum * @copyright 2016 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class big_search_form implements renderable, templatable { public $course; public $datefrom; public $dateto; public $forumoptions; public $fullwords; public $notwords; public $phrase; public $showfullwords; public $subject; public $user; public $words; public $tags; /** @var string The URL of the search form. */ public $actionurl; /** @var bool Is the user a guest user? */ public $guestuser; /** @var bool Whether the include starredonly checkbox is checked. */ public $starredonly; /** @var int forum ID. */ public $forumid; /** * Constructor. * * @param object $course The course. * @param object $user The user. */ public function __construct($course) { global $DB, $USER; $this->course = $course; $this->tags = []; $this->guestuser = !isloggedin() || isguestuser($USER); $this->showfullwords = $DB->get_dbfamily() == 'mysql' || $DB->get_dbfamily() == 'postgres'; $this->actionurl = new moodle_url('/mod/forum/search.php'); $forumoptions = ['' => get_string('allforums', 'forum')] + forum_menu_list($course); $this->forumoptions = array_map(function($option) use ($forumoptions) { return [ 'value' => $option, 'name' => $forumoptions[$option] ]; }, array_keys($forumoptions)); } /** * Set date from. * * @param mixed $value Date from. */ public function set_datefrom($value) { $this->datefrom = $value; } /** * Set date to. * * @param mixed $value Date to. */ public function set_dateto($value) { $this->dateto = $value; } /** * Set full words. * * @param mixed $value Full words. */ public function set_fullwords($value) { $this->fullwords = $value; } /** * Set not words. * * @param mixed $value Not words. */ public function set_notwords($value) { $this->notwords = $value; } /** * Set phrase. * * @param mixed $value Phrase. */ public function set_phrase($value) { $this->phrase = $value; } /** * Set subject. * * @param mixed $value Subject. */ public function set_subject($value) { $this->subject = $value; } /** * Set user. * * @param mixed $value User. */ public function set_user($value) { $this->user = $value; } /** * Set words. * * @param mixed $value Words. */ public function set_words($value) { $this->words = $value; } /** * Set tags. * * @param mixed $value Tags. */ public function set_tags($value) { $this->tags = $value; } /** * Set starred only value. * * @param mixed $value Bool. */ public function set_starredonly($value) { $this->starredonly = $value; } /** * Forum ID setter search criteria. * * @param int $forumid The forum ID. */ public function set_forumid($forumid) { $this->forumid = $forumid; } public function export_for_template(renderer_base $output) { global $DB, $CFG, $PAGE; $data = new stdClass(); $data->courseid = $this->course->id; $data->words = $this->words; $data->phrase = $this->phrase; $data->notwords = $this->notwords; $data->fullwords = $this->fullwords; $data->datefromchecked = !empty($this->datefrom); $data->datetochecked = !empty($this->dateto); $data->subject = $this->subject; $data->user = $this->user; $data->showfullwords = $this->showfullwords; $data->guestuser = $this->guestuser; $data->starredonly = $this->starredonly; $data->actionurl = $this->actionurl->out(false); $tagtypestoshow = \core_tag_area::get_showstandard('mod_forum', 'forum_posts'); $showstandard = ($tagtypestoshow != \core_tag_tag::HIDE_STANDARD); $typenewtags = ($tagtypestoshow != \core_tag_tag::STANDARD_ONLY); $PAGE->requires->js_call_amd('core/form-autocomplete', 'enhance', $params = array('#tags', $typenewtags, '', get_string('entertags', 'tag'), false, $showstandard, get_string('noselection', 'form'))); $data->tagsenabled = \core_tag_tag::is_enabled('mod_forum', 'forum_posts'); $namefield = empty($CFG->keeptagnamecase) ? 'name' : 'rawname'; $tags = $DB->get_records('tag', array('isstandard' => 1, 'tagcollid' => \core_tag_area::get_collection('mod_forum', 'forum_posts')), $namefield, 'rawname,' . $namefield . ' as fieldname'); $data->tags = []; foreach ($tags as $tag) { $data->tagoptions[] = ['value' => $tag->rawname, 'text' => $tag->fieldname, 'selected' => in_array($tag->rawname, $this->tags) ]; } $datefrom = $this->datefrom; if (empty($datefrom)) { $datefrom = make_timestamp(2000, 1, 1, 0, 0, 0); } $dateto = $this->dateto; if (empty($dateto)) { $dateto = time() + HOURSECS; } $data->datefromfields = html_writer::div(html_writer::select_time('days', 'fromday', $datefrom), 'mb-3 fitem ms-2') . html_writer::div(html_writer::select_time('months', 'frommonth', $datefrom), 'mb-3 fitem ms-2') . html_writer::div(html_writer::select_time('years', 'fromyear', $datefrom), 'mb-3 fitem ms-2') . html_writer::div(html_writer::select_time('hours', 'fromhour', $datefrom), 'mb-3 fitem ms-2') . html_writer::div(html_writer::select_time('minutes', 'fromminute', $datefrom), 'mb-3 fitem ms-2'); $data->datetofields = html_writer::div(html_writer::select_time('days', 'today', $dateto), 'mb-3 fitem ms-2') . html_writer::div(html_writer::select_time('months', 'tomonth', $dateto), 'mb-3 fitem ms-2') . html_writer::div(html_writer::select_time('years', 'toyear', $dateto), 'mb-3 fitem ms-2') . html_writer::div(html_writer::select_time('hours', 'tohour', $dateto), 'mb-3 fitem ms-2') . html_writer::div(html_writer::select_time('minutes', 'tominute', $dateto), 'mb-3 fitem ms-2'); if ($this->forumid && !empty($this->forumoptions)) { foreach ($this->forumoptions as $index => $option) { if ($option['value'] == $this->forumid) { $this->forumoptions[$index]['selected'] = true; } else { $this->forumoptions[$index]['selected'] = false; } } } $data->forumoptions = $this->forumoptions; return $data; } } PK-B\f $classes/output/quick_search_form.phpnu[. /** * Quick search form renderable. * * @package mod_forum * @copyright 2016 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\output; defined('MOODLE_INTERNAL') || die(); use help_icon; use moodle_url; use renderable; use renderer_base; use templatable; /** * Quick search form renderable class. * * @package mod_forum * @copyright 2016 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class quick_search_form implements renderable, templatable { /** @var int The course ID. */ protected $courseid; /** @var string Current query. */ protected $query; /** @var moodle_url The form action URL. */ protected $actionurl; /** @var help_icon The help icon. */ protected $helpicon; /** * Constructor. * * @param int $courseid The course ID. * @param string $query The current query. */ public function __construct($courseid, $query = '') { $this->courseid = $courseid; $this->query = $query; $this->actionurl = new moodle_url('/mod/forum/search.php'); $this->helpicon = new help_icon('search', 'core'); } public function export_for_template(renderer_base $output) { $hiddenfields = [ (object) ['name' => 'id', 'value' => $this->courseid], ]; $data = [ 'action' => $this->actionurl->out(false), 'hiddenfields' => $hiddenfields, 'query' => $this->query, 'helpicon' => $this->helpicon->export_for_template($output), 'inputname' => 'search', 'searchstring' => get_string('searchforums', 'mod_forum') ]; return $data; } } PK-B\kNK+classes/output/emaildigestfull/renderer.phpnu[. /** * Forum post renderable. * * @package mod_forum * @copyright 2015 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\output\emaildigestfull; defined('MOODLE_INTERNAL') || die(); /** * Forum post renderable. * * @since Moodle 3.0 * @package mod_forum * @copyright 2015 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class renderer extends \mod_forum\output\email\renderer { /** * The template name for this renderer. * * @return string */ public function forum_post_template() { return 'forum_post_emaildigestfull_htmlemail'; } } PK-B\q5classes/output/emaildigestfull/renderer_textemail.phpnu[. /** * Forum post renderable. * * @package mod_forum * @copyright 2015 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\output\emaildigestfull; defined('MOODLE_INTERNAL') || die(); /** * Forum post renderable. * * @since Moodle 3.0 * @package mod_forum * @copyright 2015 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class renderer_textemail extends \mod_forum\output\email\renderer_textemail { /** * The template name for this renderer. * * @return string */ public function forum_post_template() { return 'forum_post_emaildigestfull_textemail'; } /** * The plaintext version of the e-mail message. * * @param \stdClass $cm * @param \stdClass $post * @return string */ public function format_message_text($cm, $post) { $context = \context_module::instance($cm->id); $message = file_rewrite_pluginfile_urls( $post->message, 'pluginfile.php', $context->id, 'mod_forum', 'post', $post->id, [ 'includetoken' => true, ]); return format_text_email($message, $post->messageformat); } } PK-B\, -classes/output/courseformat/activitybadge.phpnu[. namespace mod_forum\output\courseformat; /** * Activity badge forum class, used for rendering unread messages. * * @package mod_forum * @copyright 2023 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class activitybadge extends \core_courseformat\output\activitybadge { /** * This method will be called before exporting the template. */ protected function update_content(): void { global $CFG; require_once($CFG->dirroot . '/mod/forum/lib.php'); if (forum_tp_can_track_forums()) { if ($unread = forum_tp_count_forum_unread_posts($this->cminfo, $this->cminfo->get_course())) { if ($unread == 1) { $this->content = get_string('unreadpostsone', 'forum'); } else { $this->content = get_string('unreadpostsnumber', 'forum', $unread); } $this->style = self::STYLES['dark']; } } } } PK-B\ 661classes/event/discussion_subscription_created.phpnu[. /** * The mod_forum discussion_subscription created event. * * @package mod_forum * @copyright 2014 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_forum discussion_subscription created event class. * * @property-read array $other { * Extra information about the event. * * - int forumid: The id of the forum which the discussion is in. * - int discussion: The id of the discussion which has been subscribed to. * } * * @package mod_forum * @since Moodle 2.8 * @copyright 2014 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class discussion_subscription_created extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'c'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'forum_discussion_subs'; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' subscribed the user with id '$this->relateduserid' to the discussion " . " with id '{$this->other['discussion']}' in the forum with the course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventdiscussionsubscriptioncreated', 'mod_forum'); } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/forum/subscribe.php', array( 'id' => $this->other['forumid'], 'd' => $this->other['discussion'], )); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } if (!isset($this->other['forumid'])) { throw new \coding_exception('The \'forumid\' value must be set in other.'); } if (!isset($this->other['discussion'])) { throw new \coding_exception('The \'discussion\' value must be set in other.'); } if ($this->contextlevel != CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_objectid_mapping() { return array('db' => 'forum_discussion_subs', 'restore' => 'forum_discussion_sub'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['forumid'] = array('db' => 'forum', 'restore' => 'forum'); $othermapped['discussion'] = array('db' => 'forum_discussions', 'restore' => 'forum_discussion'); return $othermapped; } } PK-B\LzM M )classes/event/discussion_lock_updated.phpnu[. namespace mod_forum\event; use coding_exception; use moodle_url; /** * The mod_forum discussion lock updated event. * * @package mod_forum * @copyright 2022 Université Rennes 2 {@link https://www.univ-rennes2.fr} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class discussion_lock_updated extends \core\event\base { /** * Init method. * * @return void */ protected function init(): void { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_OTHER; $this->data['objecttable'] = 'forum_discussions'; } /** * Returns description of what happened. * * @return string */ public function get_description(): string { return "The user with id '$this->userid' {$this->other['status']} the discussion: $this->objectid". " in the forum: {$this->other['forumid']}"; } /** * Return localised event name. * * @return string */ public static function get_name(): string { return get_string('eventdiscussionlockupdated', 'mod_forum'); } /** * Get URL related to the action * * @return moodle_url */ public function get_url(): moodle_url { return new moodle_url('/mod/forum/discuss.php', ['d' => $this->objectid]); } /** * Custom validation. * * @throws coding_exception * @return void */ protected function validate_data(): void { parent::validate_data(); if (!isset($this->other['forumid'])) { throw new coding_exception('forumid must be set in $other.'); } if (!isset($this->other['status'])) { throw new \coding_exception('The \'status\' value must be set in other.'); } if (!in_array($this->other['status'], ['locked', 'unlocked'], true)) { throw new \coding_exception('The \'status\' value must be \'locked\' or \'unlocked\'.'); } if ($this->contextlevel != CONTEXT_MODULE) { throw new coding_exception('Context passed must be module context.'); } if (!isset($this->objectid)) { throw new coding_exception('objectid must be set to the discussionid.'); } } /** * Forum discussion object id mappings. * * @return array */ public static function get_objectid_mapping(): array { return ['db' => 'forum_discussions', 'restore' => 'forum_discussion']; } /** * Forum id mappings. * * @return array */ public static function get_other_mapping(): array { return ['forumid' => ['db' => 'forum', 'restore' => 'forum']]; } } PK-B\Jk' ' &classes/event/subscription_created.phpnu[. /** * The mod_forum subscription created event. * * @package mod_forum * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_forum subscription created event class. * * @property-read array $other { * Extra information about the event. * * - int forumid: The id of the forum which has been subscribed to. * } * * @package mod_forum * @since Moodle 2.7 * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class subscription_created extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'c'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'forum_subscriptions'; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' subscribed the user with id '$this->relateduserid' to the forum with " . "course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventsubscriptioncreated', 'mod_forum'); } /** * Get URL related to the action * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/forum/subscribers.php', array('id' => $this->other['forumid'])); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } if (!isset($this->other['forumid'])) { throw new \coding_exception('The \'forumid\' value must be set in other.'); } if ($this->contextlevel != CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_objectid_mapping() { return array('db' => 'forum_subscriptions', 'restore' => 'forum_subscription'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['forumid'] = array('db' => 'forum', 'restore' => 'forum'); return $othermapped; } } PK-B\1R{ $classes/event/discussion_deleted.phpnu[. /** * The mod_forum discussion deleted event. * * @package mod_forum * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_forum discussion deleted event class. * * @property-read array $other { * Extra information about the event. * * - int forumid: The id of the forum the discussion is in. * } * * @package mod_forum * @since Moodle 2.7 * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class discussion_deleted extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'd'; $this->data['edulevel'] = self::LEVEL_OTHER; $this->data['objecttable'] = 'forum_discussions'; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has deleted the discussion with id '$this->objectid' in the forum " . "with course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventdiscussiondeleted', 'mod_forum'); } /** * Get URL related to the action * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/forum/view.php', array('id' => $this->contextinstanceid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['forumid'])) { throw new \coding_exception('The \'forumid\' value must be set in other.'); } if ($this->contextlevel != CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_objectid_mapping() { return array('db' => 'forum_discussions', 'restore' => 'forum_discussion'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['forumid'] = array('db' => 'forum', 'restore' => 'forum'); return $othermapped; } } PK-B\v %classes/event/discussion_unpinned.phpnu[. /** * The mod_forum discussion unpinned event. * * @package mod_forum * @copyright 2014 Charles Fulton * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_forum discussion unpinned event. * * @package mod_forum * @copyright 2014 Charles Fulton * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class discussion_unpinned extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_OTHER; $this->data['objecttable'] = 'forum_discussions'; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user {$this->userid} has unpinned the discussion {$this->objectid} in the forum {$this->other['forumid']}"; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventdiscussionunpinned', 'mod_forum'); } /** * Get URL related to the action * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/forum/discuss.php', array('d' => $this->objectid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['forumid'])) { throw new \coding_exception('forumid must be set in $other.'); } if ($this->contextlevel != CONTEXT_MODULE) { throw new \coding_exception('Context passed must be module context.'); } if (!isset($this->objectid)) { throw new \coding_exception('objectid must be set to the discussionid.'); } } /** * Forum discussion object id mappings. * * @return array */ public static function get_objectid_mapping() { return array('db' => 'forum_discussions', 'restore' => 'forum_discussion'); } /** * Forum id mappings. * * @return array */ public static function get_other_mapping() { $othermapped = array(); $othermapped['forumid'] = array('db' => 'forum', 'restore' => 'forum'); return $othermapped; } } PK-B\q(Q !classes/event/course_searched.phpnu[. /** * The mod_forum course searched event. * * @package mod_forum * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_forum course searched event class. * * @property-read array $other { * Extra information about the event. * * - string searchterm: The searchterm used on forum search. * } * * @package mod_forum * @since Moodle 2.7 * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_searched extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } /** * Returns description of what happened. * * @return string */ public function get_description() { $searchterm = s($this->other['searchterm']); return "The user with id '$this->userid' has searched the course with id '$this->courseid' for forum posts " . "containing \"{$searchterm}\"."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventcoursesearched', 'mod_forum'); } /** * Get URL related to the action * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/forum/search.php', array('id' => $this->courseid, 'search' => $this->other['searchterm'])); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['searchterm'])) { throw new \coding_exception('The \'searchterm\' value must be set in other.'); } if ($this->contextlevel != CONTEXT_COURSE) { throw new \coding_exception('Context level must be CONTEXT_COURSE.'); } } public static function get_other_mapping() { return false; } } PK-B\AHHclasses/event/post_updated.phpnu[. /** * The mod_forum post updated event. * * @package mod_forum * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_forum post updated event class. * * @property-read array $other { * Extra information about the event. * * - int discussionid: The discussion id the post is part of. * - int forumid: The forum id the post is part of. * - string forumtype: The type of forum the post is part of. * } * * @package mod_forum * @since Moodle 2.7 * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class post_updated extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'forum_posts'; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has updated the post with id '$this->objectid' in the discussion with " . "id '{$this->other['discussionid']}' in the forum with course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventpostupdated', 'mod_forum'); } /** * Get URL related to the action * * @return \moodle_url */ public function get_url() { if ($this->other['forumtype'] == 'single') { // Single discussion forums are an exception. We show // the forum itself since it only has one discussion // thread. $url = new \moodle_url('/mod/forum/view.php', array('f' => $this->other['forumid'])); } else { $url = new \moodle_url('/mod/forum/discuss.php', array('d' => $this->other['discussionid'])); } $url->set_anchor('p'.$this->objectid); return $url; } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['discussionid'])) { throw new \coding_exception('The \'discussionid\' value must be set in other.'); } if (!isset($this->other['forumid'])) { throw new \coding_exception('The \'forumid\' value must be set in other.'); } if (!isset($this->other['forumtype'])) { throw new \coding_exception('The \'forumtype\' value must be set in other.'); } if ($this->contextlevel != CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_objectid_mapping() { return array('db' => 'forum_posts', 'restore' => 'forum_post'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['forumid'] = array('db' => 'forum', 'restore' => 'forum'); $othermapped['discussionid'] = array('db' => 'forum_discussions', 'restore' => 'forum_discussion'); return $othermapped; } } PK-B\d #classes/event/discussion_viewed.phpnu[. /** * The mod_forum discussion viewed event. * * @package mod_forum * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_forum discussion viewed event class. * * @package mod_forum * @since Moodle 2.7 * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class discussion_viewed extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'forum_discussions'; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has viewed the discussion with id '$this->objectid' in the forum " . "with course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventdiscussionviewed', 'mod_forum'); } /** * Get URL related to the action * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/forum/discuss.php', array('d' => $this->objectid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if ($this->contextlevel != CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_objectid_mapping() { return array('db' => 'forum_discussions', 'restore' => 'forum_discussion'); } } PK-B\ classes/event/post_deleted.phpnu[. /** * The mod_forum post deleted event. * * @package mod_forum * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_forum post deleted event class. * * @property-read array $other { * Extra information about the event. * * - int discussionid: The discussion id the post is part of. * - int forumid: The forum id the post is part of. * - string forumtype: The type of forum the post is part of. * } * * @package mod_forum * @since Moodle 2.7 * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class post_deleted extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'd'; $this->data['edulevel'] = self::LEVEL_OTHER; $this->data['objecttable'] = 'forum_posts'; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has deleted the post with id '$this->objectid' in the discussion with " . "id '{$this->other['discussionid']}' in the forum with course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventpostdeleted', 'mod_forum'); } /** * Get URL related to the action * * @return \moodle_url */ public function get_url() { if ($this->other['forumtype'] == 'single') { // Single discussion forums are an exception. We show // the forum itself since it only has one discussion // thread. $url = new \moodle_url('/mod/forum/view.php', array('f' => $this->other['forumid'])); } else { $url = new \moodle_url('/mod/forum/discuss.php', array('d' => $this->other['discussionid'])); } return $url; } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['discussionid'])) { throw new \coding_exception('The \'discussionid\' value must be set in other.'); } if (!isset($this->other['forumid'])) { throw new \coding_exception('The \'forumid\' value must be set in other.'); } if (!isset($this->other['forumtype'])) { throw new \coding_exception('The \'forumtype\' value must be set in other.'); } if ($this->contextlevel != CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_objectid_mapping() { return array('db' => 'forum_posts', 'restore' => 'forum_post'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['forumid'] = array('db' => 'forum', 'restore' => 'forum'); $othermapped['discussionid'] = array('db' => 'forum_discussions', 'restore' => 'forum_discussion'); return $othermapped; } } PK-B\#g g &classes/event/readtracking_enabled.phpnu[. /** * The mod_forum read tracking enabled event. * * @package mod_forum * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_forum read tracking enabled event class. * * @property-read array $other { * Extra information about the event. * * - int forumid: The id of the forum which readtracking has been enabled on. * } * * @package mod_forum * @since Moodle 2.7 * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class readtracking_enabled extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'c'; $this->data['edulevel'] = self::LEVEL_OTHER; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has enabled read tracking for the user with id '$this->relateduserid' " . "in the forum with course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventreadtrackingenabled', 'mod_forum'); } /** * Get URL related to the action * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/forum/view.php', array('f' => $this->other['forumid'])); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } if (!isset($this->other['forumid'])) { throw new \coding_exception('The \'forumid\' value must be set in other.'); } if ($this->contextlevel != CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_other_mapping() { $othermapped = array(); $othermapped['forumid'] = array('db' => 'forum', 'restore' => 'forum'); return $othermapped; } } PK-B\[-w w $classes/event/discussion_updated.phpnu[. /** * The mod_forum discussion updated event. * * @package mod_forum * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_forum discussion updated event class. * * @property-read array $other { * Extra information about the event. * * - int forumid: The id of the forum the discussion is in * } * * @package mod_forum * @since Moodle 2.7 * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class discussion_updated extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_OTHER; $this->data['objecttable'] = 'forum_discussions'; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has updated the discussion with id '$this->objectid' in the forum " . "with course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventdiscussionupdated', 'mod_forum'); } /** * Get URL related to the action * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/forum/discuss.php', array('d' => $this->objectid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['forumid'])) { throw new \coding_exception('The \'forumid\' value must be set in other.'); } if ($this->contextlevel != CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_objectid_mapping() { return array('db' => 'forum_discussions', 'restore' => 'forum_discussion'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['forumid'] = array('db' => 'forum', 'restore' => 'forum'); return $othermapped; } } PK-B\]N'  $classes/event/discussion_created.phpnu[. /** * The mod_forum discussion created event. * * @package mod_forum * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_forum discussion created event class. * * @property-read array $other { * Extra information about the event. * * - int forumid: The id of the forum the discussion is in. * } * * @package mod_forum * @since Moodle 2.7 * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class discussion_created extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'c'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'forum_discussions'; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has created the discussion with id '$this->objectid' in the forum " . "with course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventdiscussioncreated', 'mod_forum'); } /** * Get URL related to the action * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/forum/discuss.php', array('d' => $this->objectid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['forumid'])) { throw new \coding_exception('The \'forumid\' value must be set in other.'); } if ($this->contextlevel != CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_objectid_mapping() { return array('db' => 'forum_discussions', 'restore' => 'forum_discussion'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['forumid'] = array('db' => 'forum', 'restore' => 'forum'); return $othermapped; } } PK-B\fd>>1classes/event/discussion_subscription_deleted.phpnu[. /** * The mod_forum discussion_subscription deleted event. * * @package mod_forum * @copyright 2014 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_forum discussion_subscription deleted event class. * * @property-read array $other { * Extra information about the event. * * - int forumid: The id of the forum which the discussion is in. * - int discussion: The id of the discussion which has been unsubscribed from. * } * * @package mod_forum * @since Moodle 2.8 * @copyright 2014 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class discussion_subscription_deleted extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'd'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'forum_discussion_subs'; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' unsubscribed the user with id '$this->relateduserid' from the discussion " . " with id '{$this->other['discussion']}' in the forum with the course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventdiscussionsubscriptiondeleted', 'mod_forum'); } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/forum/subscribe.php', array( 'id' => $this->other['forumid'], 'd' => $this->other['discussion'], )); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } if (!isset($this->other['forumid'])) { throw new \coding_exception('The \'forumid\' value must be set in other.'); } if (!isset($this->other['discussion'])) { throw new \coding_exception('The \'discussion\' value must be set in other.'); } if ($this->contextlevel != CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_objectid_mapping() { return array('db' => 'forum_discussion_subs', 'restore' => 'forum_discussion_sub'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['forumid'] = array('db' => 'forum', 'restore' => 'forum'); $othermapped['discussion'] = array('db' => 'forum_discussions', 'restore' => 'forum_discussion'); return $othermapped; } } PK-B\o 66&classes/event/course_module_viewed.phpnu[. /** * The mod_forum course module viewed event. * * @package mod_forum * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_forum course module viewed event class. * * @package mod_forum * @since Moodle 2.7 * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_module_viewed extends \core\event\course_module_viewed { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'forum'; } /** * Get URL related to the action * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/forum/view.php', array('f' => $this->objectid)); } public static function get_objectid_mapping() { return array('db' => 'forum', 'restore' => 'forum'); } } PK-B\ʌc #classes/event/discussion_pinned.phpnu[. /** * The mod_forum discussion pinned event. * * @package mod_forum * @copyright 2014 Charles Fulton * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_forum discussion pinned event. * * @package mod_forum * @copyright 2014 Charles Fulton * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class discussion_pinned extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_OTHER; $this->data['objecttable'] = 'forum_discussions'; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user {$this->userid} has pinned the discussion {$this->objectid} in the forum {$this->other['forumid']}"; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventdiscussionpinned', 'mod_forum'); } /** * Get URL related to the action * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/forum/discuss.php', array('d' => $this->objectid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['forumid'])) { throw new \coding_exception('forumid must be set in $other.'); } if ($this->contextlevel != CONTEXT_MODULE) { throw new \coding_exception('Context passed must be module context.'); } if (!isset($this->objectid)) { throw new \coding_exception('objectid must be set to the discussionid.'); } } /** * Forum discussion object id mappings. * * @return array */ public static function get_objectid_mapping() { return array('db' => 'forum_discussions', 'restore' => 'forum_discussion'); } /** * Forum id mappings. * * @return array */ public static function get_other_mapping() { $othermapped = array(); $othermapped['forumid'] = array('db' => 'forum', 'restore' => 'forum'); return $othermapped; } } PK-B\ j j 'classes/event/readtracking_disabled.phpnu[. /** * The mod_forum subscription created event. * * @package mod_forum * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_forum subscription created event class. * * @property-read array $other { * Extra information about the event. * * - int forumid: The id of the forum which read tracking has been disabled on. * } * * @package mod_forum * @since Moodle 2.7 * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class readtracking_disabled extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'd'; $this->data['edulevel'] = self::LEVEL_OTHER; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has disabled read tracking for the user with id '$this->relateduserid' " . "in the forum with course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventreadtrackingdisabled', 'mod_forum'); } /** * Get URL related to the action * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/forum/view.php', array('f' => $this->other['forumid'])); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } if (!isset($this->other['forumid'])) { throw new \coding_exception('The \'forumid\' value must be set in other.'); } if ($this->contextlevel != CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_other_mapping() { $othermapped = array(); $othermapped['forumid'] = array('db' => 'forum', 'restore' => 'forum'); return $othermapped; } } PK-B\~HHclasses/event/post_created.phpnu[. /** * The mod_forum post created event. * * @package mod_forum * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_forum post created event class. * * @property-read array $other { * Extra information about the event. * * - int discussionid: The discussion id the post is part of. * - int forumid: The forum id the post is part of. * - string forumtype: The type of forum the post is part of. * } * * @package mod_forum * @since Moodle 2.7 * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class post_created extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'c'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'forum_posts'; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has created the post with id '$this->objectid' in the discussion with " . "id '{$this->other['discussionid']}' in the forum with course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventpostcreated', 'mod_forum'); } /** * Get URL related to the action * * @return \moodle_url */ public function get_url() { if ($this->other['forumtype'] == 'single') { // Single discussion forums are an exception. We show // the forum itself since it only has one discussion // thread. $url = new \moodle_url('/mod/forum/view.php', array('f' => $this->other['forumid'])); } else { $url = new \moodle_url('/mod/forum/discuss.php', array('d' => $this->other['discussionid'])); } $url->set_anchor('p'.$this->objectid); return $url; } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['discussionid'])) { throw new \coding_exception('The \'discussionid\' value must be set in other.'); } if (!isset($this->other['forumid'])) { throw new \coding_exception('The \'forumid\' value must be set in other.'); } if (!isset($this->other['forumtype'])) { throw new \coding_exception('The \'forumtype\' value must be set in other.'); } if ($this->contextlevel != CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_objectid_mapping() { return array('db' => 'forum_posts', 'restore' => 'forum_post'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['forumid'] = array('db' => 'forum', 'restore' => 'forum'); $othermapped['discussionid'] = array('db' => 'forum_discussions', 'restore' => 'forum_discussion'); return $othermapped; } } PK-B\rU $classes/event/subscribers_viewed.phpnu[. /** * The mod_forum subscribers list viewed event. * * @package mod_forum * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_forum subscribers list viewed event class. * * @property-read array $other { * Extra information about the event. * * - int forumid: The id of the forum which the subscriberslist has been viewed. * } * * @package mod_forum * @since Moodle 2.7 * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class subscribers_viewed extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_OTHER; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has viewed the subscribers list for the forum with course " . "module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventsubscribersviewed', 'mod_forum'); } /** * Get URL related to the action * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/forum/subscribers.php', array('id' => $this->other['forumid'])); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['forumid'])) { throw new \coding_exception('The \'forumid\' value must be set in other.'); } if ($this->contextlevel != CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_other_mapping() { $othermapped = array(); $othermapped['forumid'] = array('db' => 'forum', 'restore' => 'forum'); return $othermapped; } } PK-B\}- - &classes/event/subscription_deleted.phpnu[. /** * The mod_forum subscription deleted event. * * @package mod_forum * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_forum subscription deleted event class. * * @property-read array $other { * Extra information about the event. * * - int forumid: The id of the forum which has been unsusbcribed from. * } * * @package mod_forum * @since Moodle 2.7 * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class subscription_deleted extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'd'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'forum_subscriptions'; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' unsubscribed the user with id '$this->relateduserid' to the forum with " . "course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventsubscriptiondeleted', 'mod_forum'); } /** * Get URL related to the action * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/forum/subscribers.php', array('id' => $this->other['forumid'])); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } if (!isset($this->other['forumid'])) { throw new \coding_exception('The \'forumid\' value must be set in other.'); } if ($this->contextlevel != CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_objectid_mapping() { return array('db' => 'forum_subscriptions', 'restore' => 'forum_subscription'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['forumid'] = array('db' => 'forum', 'restore' => 'forum'); return $othermapped; } } PK-B\% $classes/event/user_report_viewed.phpnu[. /** * The mod_forum user report viewed event. * * @package mod_forum * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_forum user report viewed event class. * * @property-read array $other { * Extra information about the event. * * - string reportmode: The mode the report has been viewed in (posts or discussions). * } * * @package mod_forum * @since Moodle 2.7 * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_report_viewed extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has viewed the user report for the user with id '$this->relateduserid' in " . "the course with id '$this->courseid' with viewing mode '{$this->other['reportmode']}'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventuserreportviewed', 'mod_forum'); } /** * Get URL related to the action * * @return \moodle_url */ public function get_url() { $url = new \moodle_url('/mod/forum/user.php', array('id' => $this->relateduserid, 'mode' => $this->other['reportmode'])); if ($this->courseid != SITEID) { $url->param('course', $this->courseid); } return $url; } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } if (!isset($this->other['reportmode'])) { throw new \coding_exception('The \'reportmode\' value must be set in other.'); } switch ($this->contextlevel) { case CONTEXT_COURSE: case CONTEXT_SYSTEM: case CONTEXT_USER: // OK, expected context level. break; default: // Unexpected contextlevel. throw new \coding_exception('Context level must be either CONTEXT_SYSTEM, CONTEXT_COURSE or CONTEXT_USER.'); } } public static function get_other_mapping() { return false; } } PK-B\!;  %classes/event/assessable_uploaded.phpnu[. /** * The mod_forum assessable uploaded event. * * @package mod_forum * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_forum assessable uploaded event class. * * @property-read array $other { * Extra information about event. * * - int discussionid: id of discussion. * - string triggeredfrom: name of the function from where event was triggered. * } * * @package mod_forum * @since Moodle 2.6 * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assessable_uploaded extends \core\event\assessable_uploaded { /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has posted content in the forum post with id '$this->objectid' " . "in the discussion '{$this->other['discussionid']}' located in the forum with course module id " . "'$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventassessableuploaded', 'mod_forum'); } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/forum/discuss.php', array('d' => $this->other['discussionid'], 'parent' => $this->objectid)); } /** * Init method. * * @return void */ protected function init() { parent::init(); $this->data['objecttable'] = 'forum_posts'; } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['discussionid'])) { throw new \coding_exception('The \'discussionid\' value must be set in other.'); } else if (!isset($this->other['triggeredfrom'])) { throw new \coding_exception('The \'triggeredfrom\' value must be set in other.'); } } public static function get_objectid_mapping() { return array('db' => 'forum_posts', 'restore' => 'forum_post'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['discussionid'] = array('db' => 'forum_discussions', 'restore' => 'forum_discussion'); return $othermapped; } } PK-B\M +classes/event/subscription_mode_updated.phpnu[. namespace mod_forum\event; use coding_exception; use moodle_url; /** * The mod_forum subscription mode updated event. * * @package mod_forum * @copyright 2022 Université Rennes 2 {@link https://www.univ-rennes2.fr} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class subscription_mode_updated extends \core\event\base { /** * Init method. * * @return void */ protected function init(): void { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_OTHER; $this->data['objecttable'] = 'forum'; } /** * Returns description of what happened. * * @return string */ public function get_description(): string { return "The user with id '$this->userid' has updated the forum subscription from mode '{$this->other['oldvalue']}' to mode '{$this->other['newvalue']}' in the forum id '$this->objectid'"; } /** * Return localised event name. * * @return string */ public static function get_name(): string { return get_string('eventforumsubscriptionupdated', 'mod_forum'); } /** * Get URL related to the action * * @return moodle_url */ public function get_url(): moodle_url { return new moodle_url('/mod/forum/subscribers.php', ['id' => $this->objectid]); } /** * Custom validation. * * @throws coding_exception * @return void */ protected function validate_data(): void { parent::validate_data(); if (!isset($this->other['oldvalue'])) { throw new coding_exception('oldvalue must be set in $other.'); } if (!isset($this->other['newvalue'])) { throw new coding_exception('newvalue must be set in $other.'); } if ($this->contextlevel != CONTEXT_MODULE) { throw new coding_exception('Context passed must be module context.'); } if (!isset($this->objectid)) { throw new coding_exception('objectid must be set to the forumid.'); } } /** * Forum object id mappings. * * @return array */ public static function get_objectid_mapping(): array { return ['db' => 'forum', 'restore' => 'forum']; } } PK-B\Un "classes/event/discussion_moved.phpnu[. /** * The mod_forum discussion moved event. * * @package mod_forum * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_forum discussion moved event class. * * @property-read array $other { * Extra information about the event. * * - int fromforumid: The id of the forum the discussion is being moved from. * - int toforumid: The id of the forum the discussion is being moved to. * } * * @package mod_forum * @since Moodle 2.7 * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class discussion_moved extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_OTHER; $this->data['objecttable'] = 'forum_discussions'; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has moved the discussion with id '$this->objectid' from the " . "forum with id '{$this->other['fromforumid']}' to the forum with id '{$this->other['toforumid']}'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventdiscussionmoved', 'mod_forum'); } /** * Get URL related to the action * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/forum/discuss.php', array('d' => $this->objectid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['fromforumid'])) { throw new \coding_exception('The \'fromforumid\' value must be set in other.'); } if (!isset($this->other['toforumid'])) { throw new \coding_exception('The \'toforumid\' value must be set in other.'); } if ($this->contextlevel != CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_objectid_mapping() { return array('db' => 'forum_discussions', 'restore' => 'forum_discussion'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['fromforumid'] = array('db' => 'forum', 'restore' => 'forum'); $othermapped['toforumid'] = array('db' => 'forum', 'restore' => 'forum'); return $othermapped; } } PK-B\BA;``4classes/event/course_module_instance_list_viewed.phpnu[. /** * The mod_forum instance list viewed event. * * @package mod_forum * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_forum instance list viewed event class. * * @package mod_forum * @since Moodle 2.7 * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed { // No need for any code here as everything is handled by the parent class. } PK-B\a`OOclasses/task/cron_task.phpnu[. /** * A scheduled task for forum cron. * * @package mod_forum * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\task; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/forum/lib.php'); /** * The main scheduled task for the forum. * * @package mod_forum * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cron_task extends \core\task\scheduled_task { // Use the logging trait to get some nice, juicy, logging. use \core\task\logging_trait; /** * @var The list of courses which contain posts to be sent. */ protected $courses = []; /** * @var The list of forums which contain posts to be sent. */ protected $forums = []; /** * @var The list of discussions which contain posts to be sent. */ protected $discussions = []; /** * @var The list of posts to be sent. */ protected $posts = []; /** * @var The list of post authors. */ protected $users = []; /** * @var The list of subscribed users. */ protected $subscribedusers = []; /** * @var The list of digest users. */ protected $digestusers = []; /** * @var The list of adhoc data for sending. */ protected $adhocdata = []; /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name() { return get_string('crontask', 'mod_forum'); } /** * Execute the scheduled task. */ public function execute() { global $CFG, $DB; $timenow = time(); // Delete any really old posts in the digest queue. $weekago = $timenow - WEEKSECS; $this->log_start("Removing old digest records from 7 days ago."); $selectsqlcondition = "timemodified < :weekago AND discussionid IN (SELECT id FROM {forum_discussions} WHERE timestart < :holdperiod)"; $params = [ 'weekago' => $weekago, 'holdperiod' => $weekago, ]; $DB->delete_records_select('forum_queue', $selectsqlcondition, $params); $this->log_finish("Removed all old digest records."); $endtime = $timenow - $CFG->maxeditingtime; $starttime = $endtime - (2 * DAYSECS); $this->log_start("Fetching unmailed posts."); if (!$posts = $this->get_unmailed_posts($starttime, $endtime, $timenow)) { $this->log_finish("No posts found.", 1); return false; } $this->log_finish("Done"); // Process post data and turn into adhoc tasks. $this->process_post_data($posts); // Mark posts as read. list($in, $params) = $DB->get_in_or_equal(array_keys($posts)); $DB->set_field_select('forum_posts', 'mailed', 1, "id {$in}", $params); } /** * Process all posts and convert to appropriated hoc tasks. * * @param \stdClass[] $posts */ protected function process_post_data($posts) { $discussionids = []; $forumids = []; $courseids = []; $this->log_start("Processing post information"); $start = microtime(true); foreach ($posts as $id => $post) { $discussionids[$post->discussion] = true; $forumids[$post->forum] = true; $courseids[$post->course] = true; $this->add_data_for_post($post); $this->posts[$id] = $post; } $this->log_finish(sprintf("Processed %s posts", count($this->posts))); if (empty($this->posts)) { $this->log("No posts found. Returning early."); return; } // Please note, this order is intentional. // The forum cache makes use of the course. $this->log_start("Filling caches"); $start = microtime(true); $this->log_start("Filling course cache", 1); $this->fill_course_cache(array_keys($courseids)); $this->log_finish("Done", 1); $this->log_start("Filling forum cache", 1); $this->fill_forum_cache(array_keys($forumids)); $this->log_finish("Done", 1); $this->log_start("Filling discussion cache", 1); $this->fill_discussion_cache(array_keys($discussionids)); $this->log_finish("Done", 1); $this->log_start("Filling user subscription cache", 1); $this->fill_user_subscription_cache(); $this->log_finish("Done", 1); $this->log_start("Filling digest cache", 1); $this->fill_digest_cache(); $this->log_finish("Done", 1); $this->log_finish("All caches filled"); $this->log_start("Queueing user tasks."); $this->queue_user_tasks(); $this->log_finish("All tasks queued."); } /** * Fill the course cache. * * @param int[] $courseids */ protected function fill_course_cache($courseids) { global $DB; list($in, $params) = $DB->get_in_or_equal($courseids); $this->courses = $DB->get_records_select('course', "id $in", $params); } /** * Fill the forum cache. * * @param int[] $forumids */ protected function fill_forum_cache($forumids) { global $DB; $requiredfields = [ 'id', 'course', 'forcesubscribe', 'type', ]; list($in, $params) = $DB->get_in_or_equal($forumids); $this->forums = $DB->get_records_select('forum', "id $in", $params, '', implode(', ', $requiredfields)); foreach ($this->forums as $id => $forum) { \mod_forum\subscriptions::fill_subscription_cache($id); \mod_forum\subscriptions::fill_discussion_subscription_cache($id); } } /** * Fill the discussion cache. * * @param int[] $discussionids */ protected function fill_discussion_cache($discussionids) { global $DB; if (empty($discussionids)) { $this->discussions = []; } else { $requiredfields = [ 'id', 'groupid', 'firstpost', 'timestart', 'timeend', ]; list($in, $params) = $DB->get_in_or_equal($discussionids); $this->discussions = $DB->get_records_select( 'forum_discussions', "id $in", $params, '', implode(', ', $requiredfields)); } } /** * Fill the cache of user digest preferences. */ protected function fill_digest_cache() { global $DB; if (empty($this->users)) { return; } // Get the list of forum subscriptions for per-user per-forum maildigest settings. list($in, $params) = $DB->get_in_or_equal(array_keys($this->users)); $digestspreferences = $DB->get_recordset_select( 'forum_digests', "userid $in", $params, '', 'id, userid, forum, maildigest'); foreach ($digestspreferences as $digestpreference) { if (!isset($this->digestusers[$digestpreference->forum])) { $this->digestusers[$digestpreference->forum] = []; } $this->digestusers[$digestpreference->forum][$digestpreference->userid] = $digestpreference->maildigest; } $digestspreferences->close(); } /** * Add dsta for the current forum post to the structure of adhoc data. * * @param \stdClass $post */ protected function add_data_for_post($post) { if (!isset($this->adhocdata[$post->course])) { $this->adhocdata[$post->course] = []; } if (!isset($this->adhocdata[$post->course][$post->forum])) { $this->adhocdata[$post->course][$post->forum] = []; } if (!isset($this->adhocdata[$post->course][$post->forum][$post->discussion])) { $this->adhocdata[$post->course][$post->forum][$post->discussion] = []; } $this->adhocdata[$post->course][$post->forum][$post->discussion][$post->id] = $post->id; } /** * Fill the cache of user subscriptions. */ protected function fill_user_subscription_cache() { foreach ($this->forums as $forum) { $cm = get_fast_modinfo($this->courses[$forum->course])->instances['forum'][$forum->id]; $modcontext = \context_module::instance($cm->id); $this->subscribedusers[$forum->id] = []; if ($users = \mod_forum\subscriptions::fetch_subscribed_users($forum, 0, $modcontext, 'u.id, u.maildigest', true)) { foreach ($users as $user) { // This user is subscribed to this forum. $this->subscribedusers[$forum->id][$user->id] = $user->id; if (!isset($this->users[$user->id])) { // Store minimal user info. $this->users[$user->id] = $user; } } // Release memory. unset($users); } } } /** * Queue the user tasks. */ protected function queue_user_tasks() { global $CFG, $DB; $timenow = time(); $sitetimezone = \core_date::get_server_timezone(); $counts = [ 'digests' => 0, 'individuals' => 0, 'users' => 0, 'ignored' => 0, 'messages' => 0, ]; $this->log("Processing " . count($this->users) . " users", 1); foreach ($this->users as $user) { $usercounts = [ 'digests' => 0, 'messages' => 0, ]; $send = false; // Setup this user so that the capabilities are cached, and environment matches receiving user. \core\cron::setup_user($user); list($individualpostdata, $digestpostdata) = $this->fetch_posts_for_user($user); if (!empty($digestpostdata)) { // Insert all of the records for the digest. $DB->insert_records('forum_queue', $digestpostdata); $servermidnight = usergetmidnight($timenow, $sitetimezone); $digesttime = $servermidnight + ($CFG->digestmailtime * 3600); if ($digesttime < $timenow) { // Digest time is in the past. Get a new time for tomorrow. $servermidnight = usergetmidnight($timenow + DAYSECS, $sitetimezone); $digesttime = $servermidnight + ($CFG->digestmailtime * 3600); } $task = new \mod_forum\task\send_user_digests(); $task->set_userid($user->id); $task->set_component('mod_forum'); $task->set_custom_data(['servermidnight' => $servermidnight]); $task->set_next_run_time($digesttime); \core\task\manager::reschedule_or_queue_adhoc_task($task); $usercounts['digests']++; $send = true; } if (!empty($individualpostdata)) { $usercounts['messages'] += count($individualpostdata); $task = new \mod_forum\task\send_user_notifications(); $task->set_userid($user->id); $task->set_custom_data($individualpostdata); $task->set_component('mod_forum'); \core\task\manager::queue_adhoc_task($task); $counts['individuals']++; $send = true; } if ($send) { $counts['users']++; $counts['messages'] += $usercounts['messages']; $counts['digests'] += $usercounts['digests']; } else { $counts['ignored']++; } $this->log(sprintf("Queued %d digests and %d messages for %s", $usercounts['digests'], $usercounts['messages'], $user->id ), 2); } $this->log( sprintf( "Queued %d digests, and %d individual tasks for %d post mails. " . "Unique users: %d (%d ignored)", $counts['digests'], $counts['individuals'], $counts['messages'], $counts['users'], $counts['ignored'] ), 1); } /** * Fetch posts for this user. * * @param \stdClass $user The user to fetch posts for. */ protected function fetch_posts_for_user($user) { // We maintain a mapping of user groups for each forum. $usergroups = []; $digeststructure = []; $poststructure = $this->adhocdata; $poststosend = []; foreach ($poststructure as $courseid => $forumids) { $course = $this->courses[$courseid]; foreach ($forumids as $forumid => $discussionids) { $forum = $this->forums[$forumid]; $maildigest = forum_get_user_maildigest_bulk($this->digestusers, $user, $forumid); if (!isset($this->subscribedusers[$forumid][$user->id])) { // This user has no subscription of any kind to this forum. // Do not send them any posts at all. unset($poststructure[$courseid][$forumid]); continue; } $subscriptiontime = \mod_forum\subscriptions::fetch_discussion_subscription($forum->id, $user->id); $cm = get_fast_modinfo($course)->instances['forum'][$forumid]; foreach ($discussionids as $discussionid => $postids) { $discussion = $this->discussions[$discussionid]; if (!\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussionid, $cm)) { // The user does not subscribe to this forum as a whole, or to this specific discussion. unset($poststructure[$courseid][$forumid][$discussionid]); continue; } if ($discussion->groupid > 0 and $groupmode = groups_get_activity_groupmode($cm, $course)) { // This discussion has a groupmode set (SEPARATEGROUPS or VISIBLEGROUPS). // Check whether the user can view it based on their groups. if (!isset($usergroups[$forum->id])) { $usergroups[$forum->id] = groups_get_all_groups($courseid, $user->id, $cm->groupingid); } if (!isset($usergroups[$forum->id][$discussion->groupid])) { // This user is not a member of this group, or the group no longer exists. $modcontext = \context_module::instance($cm->id); if (!has_capability('moodle/site:accessallgroups', $modcontext, $user)) { // This user does not have the accessallgroups and is not a member of the group. // Do not send posts from other groups when in SEPARATEGROUPS or VISIBLEGROUPS. unset($poststructure[$courseid][$forumid][$discussionid]); continue; } } } foreach ($postids as $postid) { $post = $this->posts[$postid]; if ($subscriptiontime) { // Skip posts if the user subscribed to the discussion after it was created. $subscribedafter = isset($subscriptiontime[$post->discussion]); $subscribedafter = $subscribedafter && ($subscriptiontime[$post->discussion] > $post->created); if ($subscribedafter) { // The user subscribed to the discussion/forum after this post was created. unset($poststructure[$courseid][$forumid][$discussionid][$postid]); continue; } } if ($maildigest > 0) { // This user wants the mails to be in digest form. $digeststructure[] = (object) [ 'userid' => $user->id, 'discussionid' => $discussion->id, 'postid' => $post->id, 'timemodified' => $post->created, ]; unset($poststructure[$courseid][$forumid][$discussionid][$postid]); continue; } else { // Add this post to the list of postids to be sent. $poststosend[] = $postid; } } } if (empty($poststructure[$courseid][$forumid])) { // This user is not subscribed to any discussions in this forum at all. unset($poststructure[$courseid][$forumid]); continue; } } if (empty($poststructure[$courseid])) { // This user is not subscribed to any forums in this course. unset($poststructure[$courseid]); } } return [$poststosend, $digeststructure]; } /** * Returns a list of all new posts that have not been mailed yet * * @param int $starttime posts created after this time * @param int $endtime posts created before this * @param int $now used for timed discussions only * @return array */ protected function get_unmailed_posts($starttime, $endtime, $now = null) { global $CFG, $DB; $params = array(); $params['mailed'] = FORUM_MAILED_PENDING; $params['ptimestart'] = $starttime; $params['ptimeend'] = $endtime; $params['mailnow'] = 1; if (!empty($CFG->forum_enabletimedposts)) { if (empty($now)) { $now = time(); } $selectsql = "AND (p.created >= :ptimestart OR d.timestart >= :pptimestart)"; $params['pptimestart'] = $starttime; $timedsql = "AND (d.timestart < :dtimestart AND (d.timeend = 0 OR d.timeend > :dtimeend))"; $params['dtimestart'] = $now; $params['dtimeend'] = $now; } else { $timedsql = ""; $selectsql = "AND p.created >= :ptimestart"; } return $DB->get_records_sql( "SELECT p.id, p.discussion, d.forum, d.course, p.created, p.parent, p.userid FROM {forum_posts} p JOIN {forum_discussions} d ON d.id = p.discussion WHERE p.mailed = :mailed $selectsql AND (p.created < :ptimeend OR p.mailnow = :mailnow) $timedsql ORDER BY p.modified ASC", $params); } } PK-B\8YVV"classes/task/send_user_digests.phpnu[. /** * This file defines an adhoc task to send notifications. * * @package mod_forum * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\task; defined('MOODLE_INTERNAL') || die(); use html_writer; require_once($CFG->dirroot . '/mod/forum/lib.php'); /** * Adhoc task to send moodle forum digests for the specified user. * * @package mod_forum * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class send_user_digests extends \core\task\adhoc_task { // Use the logging trait to get some nice, juicy, logging. use \core\task\logging_trait; /** * @var \stdClass A shortcut to $USER. */ protected $recipient; /** * @var bool[] Whether the user can view fullnames for each forum. */ protected $viewfullnames = []; /** * @var bool[] Whether the user can post in each forum. */ protected $canpostto = []; /** * @var \stdClass[] Courses with posts them. */ protected $courses = []; /** * @var \stdClass[] Forums with posts them. */ protected $forums = []; /** * @var \stdClass[] Discussions with posts them. */ protected $discussions = []; /** * @var \stdClass[] The posts to be sent. */ protected $posts = []; /** * @var \stdClass[] The various authors. */ protected $users = []; /** * @var \stdClass[] A list of any per-forum digest preference that this user holds. */ protected $forumdigesttypes = []; /** * @var bool Whether the user has requested HTML or not. */ protected $allowhtml = true; /** * @var string The subject of the message. */ protected $postsubject = ''; /** * @var string The plaintext content of the whole message. */ protected $notificationtext = ''; /** * @var string The HTML content of the whole message. */ protected $notificationhtml = ''; /** * @var string The plaintext content for the current discussion being processed. */ protected $discussiontext = ''; /** * @var string The HTML content for the current discussion being processed. */ protected $discussionhtml = ''; /** * @var int The number of messages sent in this digest. */ protected $sentcount = 0; /** * @var \renderer[][] A cache of the different types of renderer, stored both by target (HTML, or Text), and type. */ protected $renderers = [ 'html' => [], 'text' => [], ]; /** * @var int[] A list of post IDs to be marked as read for this user. */ protected $markpostsasread = []; /** * Send out messages. * @throws \moodle_exception */ public function execute() { $starttime = time(); $this->recipient = \core_user::get_user($this->get_userid()); $this->log_start("Sending forum digests for {$this->recipient->username} ({$this->recipient->id})"); if (empty($this->recipient->mailformat) || $this->recipient->mailformat != 1) { // This user does not want to receive HTML. $this->allowhtml = false; } // Fetch all of the data we need to mail these posts. $this->prepare_data($starttime); if (empty($this->posts) || empty($this->discussions) || empty($this->forums)) { $this->log_finish("No messages found to send."); return; } // Add the message headers. $this->add_message_header(); foreach ($this->discussions as $discussion) { // Raise the time limit for each discussion. \core_php_time_limit::raise(120); // Grab the data pertaining to this discussion. $forum = $this->forums[$discussion->forum]; $course = $this->courses[$forum->course]; $cm = get_fast_modinfo($course)->instances['forum'][$forum->id]; $modcontext = \context_module::instance($cm->id); $coursecontext = \context_course::instance($course->id); if (empty($this->posts[$discussion->id])) { // Somehow there are no posts. // This should not happen but better safe than sorry. continue; } if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) { // The course is hidden and the user does not have access to it. // Permissions may have changed since it was queued. continue; } if (!forum_user_can_see_discussion($forum, $discussion, $modcontext, $this->recipient)) { // User cannot see this discussion. // Permissions may have changed since it was queued. continue; } if (!\mod_forum\subscriptions::is_subscribed($this->recipient->id, $forum, $discussion->id, $cm)) { // The user does not subscribe to this forum as a whole, or to this specific discussion. continue; } // Fetch additional values relating to this forum. if (!isset($this->canpostto[$discussion->id])) { $this->canpostto[$discussion->id] = forum_user_can_post( $forum, $discussion, $this->recipient, $cm, $course, $modcontext); } if (!isset($this->viewfullnames[$forum->id])) { $this->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext, $this->recipient->id); } // Set the discussion storage values. $discussionpostcount = 0; $this->discussiontext = ''; $this->discussionhtml = ''; // Add the header for this discussion. $this->add_discussion_header($discussion, $forum, $course); $this->log_start("Adding messages in discussion {$discussion->id} (forum {$forum->id})", 1); // Add all posts in this forum. foreach ($this->posts[$discussion->id] as $post) { $author = $this->get_post_author($post->userid, $course, $forum, $cm, $modcontext); if (empty($author)) { // Unable to find the author. Skip to avoid errors. continue; } if (!forum_user_can_see_post($forum, $discussion, $post, $this->recipient, $cm)) { // User cannot see this post. // Permissions may have changed since it was queued. continue; } $this->add_post_body($author, $post, $discussion, $forum, $cm, $course); $discussionpostcount++; } // Add the forum footer. $this->add_discussion_footer($discussion, $forum, $course); // Add the data for this discussion to the notification body. if ($discussionpostcount) { $this->sentcount += $discussionpostcount; $this->notificationtext .= $this->discussiontext; $this->notificationhtml .= $this->discussionhtml; $this->log_finish("Added {$discussionpostcount} messages to discussion {$discussion->id}", 1); } else { $this->log_finish("No messages found in discussion {$discussion->id} - skipped.", 1); } } if ($this->sentcount) { // This digest has at least one post and should therefore be sent. if ($this->send_mail()) { $this->log_finish("Digest sent with {$this->sentcount} messages."); if (get_user_preferences('forum_markasreadonnotification', 1, $this->recipient->id) == 1) { forum_tp_mark_posts_read($this->recipient, $this->markpostsasread); } } else { $this->log_finish("Issue sending digest. Skipping."); throw new \moodle_exception("Issue sending digest. Skipping."); } } else { $this->log_finish("No messages found to send."); } // Empty the queue only if successful. $this->empty_queue($this->recipient->id, $starttime); // We have finishied all digest emails, update $CFG->digestmailtimelast. set_config('digestmailtimelast', $starttime); } /** * Prepare the data for this run. * * Note: This will also remove posts from the queue. * * @param int $timenow */ protected function prepare_data(int $timenow) { global $DB; $sql = "SELECT p.*, f.id AS forum, f.course FROM {forum_queue} q INNER JOIN {forum_posts} p ON p.id = q.postid INNER JOIN {forum_discussions} d ON d.id = p.discussion INNER JOIN {forum} f ON f.id = d.forum WHERE q.userid = :userid AND q.timemodified < :timemodified ORDER BY d.id, q.timemodified ASC"; $queueparams = [ 'userid' => $this->recipient->id, 'timemodified' => $timenow, ]; $posts = $DB->get_recordset_sql($sql, $queueparams); $discussionids = []; $forumids = []; $courseids = []; $userids = []; foreach ($posts as $post) { $discussionids[] = $post->discussion; $forumids[] = $post->forum; $courseids[] = $post->course; $userids[] = $post->userid; unset($post->forum); if (!isset($this->posts[$post->discussion])) { $this->posts[$post->discussion] = []; } $this->posts[$post->discussion][$post->id] = $post; } $posts->close(); if (empty($discussionids)) { // All posts have been removed since the task was queued. $this->empty_queue($this->recipient->id, $timenow); return; } list($in, $params) = $DB->get_in_or_equal($discussionids); $this->discussions = $DB->get_records_select('forum_discussions', "id {$in}", $params); list($in, $params) = $DB->get_in_or_equal($forumids); $this->forums = $DB->get_records_select('forum', "id {$in}", $params); list($in, $params) = $DB->get_in_or_equal($courseids); $this->courses = $DB->get_records_select('course', "id $in", $params); list($in, $params) = $DB->get_in_or_equal($userids); $this->users = $DB->get_records_select('user', "id $in", $params); $this->fill_digest_cache(); } /** * Empty the queue of posts for this user. * * @param int $userid user id which queue elements are going to be removed. * @param int $timemodified up time limit of the queue elements to be removed. */ protected function empty_queue(int $userid, int $timemodified): void { global $DB; $DB->delete_records_select('forum_queue', "userid = :userid AND timemodified < :timemodified", [ 'userid' => $userid, 'timemodified' => $timemodified, ]); } /** * Fill the cron digest cache. */ protected function fill_digest_cache() { global $DB; $this->forumdigesttypes = $DB->get_records_menu('forum_digests', [ 'userid' => $this->recipient->id, ], '', 'forum, maildigest'); } /** * Fetch and initialise the post author. * * @param int $userid The id of the user to fetch * @param \stdClass $course * @param \stdClass $forum * @param \stdClass $cm * @param \context $context * @return \stdClass */ protected function get_post_author($userid, $course, $forum, $cm, $context) { if (!isset($this->users[$userid])) { // This user no longer exists. return false; } $user = $this->users[$userid]; if (!isset($user->groups)) { // Initialise the groups list. $user->groups = []; } if (!isset($user->groups[$forum->id])) { $user->groups[$forum->id] = groups_get_all_groups($course->id, $user->id, $cm->groupingid); } // Clone the user object to prevent leaks between messages. return (object) (array) $user; } /** * Add the header to this message. */ protected function add_message_header() { $site = get_site(); // Set the subject of the message. $this->postsubject = get_string('digestmailsubject', 'forum', format_string($site->shortname, true)); // And the content of the header in body. $headerdata = (object) [ 'sitename' => format_string($site->fullname, true), 'userprefs' => (new \moodle_url('/user/forum.php', [ 'id' => $this->recipient->id, 'course' => $site->id, ]))->out(false), ]; $this->notificationtext .= get_string('digestmailheader', 'forum', $headerdata) . "\n"; if ($this->allowhtml) { $headerdata->userprefs = html_writer::link($headerdata->userprefs, get_string('digestmailprefs', 'forum'), [ 'target' => '_blank', ]); $this->notificationhtml .= html_writer::tag('p', get_string('digestmailheader', 'forum', $headerdata)); $this->notificationhtml .= html_writer::empty_tag('br'); $this->notificationhtml .= html_writer::empty_tag('hr', [ 'size' => 1, 'noshade' => 'noshade', ]); } } /** * Add the header for this discussion. * * @param \stdClass $discussion The discussion to add the footer for * @param \stdClass $forum The forum that the discussion belongs to * @param \stdClass $course The course that the forum belongs to */ protected function add_discussion_header($discussion, $forum, $course) { global $CFG; $shortname = format_string($course->shortname, true, [ 'context' => \context_course::instance($course->id), ]); $strforums = get_string('forums', 'forum'); $this->discussiontext .= "\n=====================================================================\n\n"; $this->discussiontext .= "$shortname -> $strforums -> " . format_string($forum->name, true); if ($discussion->name != $forum->name) { $this->discussiontext .= " -> " . format_string($discussion->name, true); } $this->discussiontext .= "\n"; $this->discussiontext .= new \moodle_url('/mod/forum/discuss.php', [ 'd' => $discussion->id, ]); $this->discussiontext .= "\n"; if ($this->allowhtml) { $this->discussionhtml .= "

". "wwwroot/course/view.php?id=$course->id\">$shortname -> ". "wwwroot/mod/forum/index.php?id=$course->id\">$strforums -> ". "wwwroot/mod/forum/view.php?f=$forum->id\">" . format_string($forum->name, true).""; if ($discussion->name == $forum->name) { $this->discussionhtml .= "

"; } else { $this->discussionhtml .= " -> wwwroot/mod/forum/discuss.php?d=$discussion->id\">" . format_string($discussion->name, true)."

"; } $this->discussionhtml .= '

'; } } /** * Add the body of this post. * * @param \stdClass $author The author of the post * @param \stdClass $post The post being sent * @param \stdClass $discussion The discussion that the post is in * @param \stdClass $forum The forum that the discussion belongs to * @param \cminfo $cm The cminfo object for the forum * @param \stdClass $course The course that the forum belongs to */ protected function add_post_body($author, $post, $discussion, $forum, $cm, $course) { global $CFG; $canreply = $this->canpostto[$discussion->id]; $data = new \mod_forum\output\forum_post_email( $course, $cm, $forum, $discussion, $post, $author, $this->recipient, $canreply ); // Override the viewfullnames value. $data->viewfullnames = $this->viewfullnames[$forum->id]; // Determine the type of digest being sent. $maildigest = $this->get_maildigest($forum->id); $textrenderer = $this->get_renderer($maildigest); $this->discussiontext .= $textrenderer->render($data); $this->discussiontext .= "\n"; if ($this->allowhtml) { $htmlrenderer = $this->get_renderer($maildigest, true); $this->discussionhtml .= $htmlrenderer->render($data); $this->log("Adding post {$post->id} in format {$maildigest} with HTML", 2); } else { $this->log("Adding post {$post->id} in format {$maildigest} without HTML", 2); } if ($maildigest == 1 && !$CFG->forum_usermarksread) { // Create an array of postid's for this user to mark as read. $this->markpostsasread[] = $post->id; } } /** * Add the footer for this discussion. * * @param \stdClass $discussion The discussion to add the footer for */ protected function add_discussion_footer($discussion) { global $CFG; if ($this->allowhtml) { $footerlinks = []; $forum = $this->forums[$discussion->forum]; if (\mod_forum\subscriptions::is_forcesubscribed($forum)) { // This forum is force subscribed. The user cannot unsubscribe. $footerlinks[] = get_string("everyoneissubscribed", "forum"); } else { $footerlinks[] = "wwwroot/mod/forum/subscribe.php?id=$forum->id\">" . get_string("unsubscribe", "forum") . ""; } $footerlinks[] = "" . get_string("digestmailpost", "forum") . ''; $this->discussionhtml .= "\n

" . implode(' ', $footerlinks) . '
'; $this->discussionhtml .= '

'; } } /** * Get the forum digest type for the specified forum, failing back to * the default setting for the current user if not specified. * * @param int $forumid * @return int */ protected function get_maildigest($forumid) { $maildigest = -1; if (isset($this->forumdigesttypes[$forumid])) { $maildigest = $this->forumdigesttypes[$forumid]; } if ($maildigest === -1 && !empty($this->recipient->maildigest)) { $maildigest = $this->recipient->maildigest; } if ($maildigest === -1) { // There is no maildigest type right now. $maildigest = 1; } return $maildigest; } /** * Send the composed message to the user. */ protected function send_mail() { // Headers to help prevent auto-responders. $userfrom = \core_user::get_noreply_user(); $userfrom->customheaders = array( "Precedence: Bulk", 'X-Auto-Response-Suppress: All', 'Auto-Submitted: auto-generated', ); $eventdata = new \core\message\message(); $eventdata->courseid = SITEID; $eventdata->component = 'mod_forum'; $eventdata->name = 'digests'; $eventdata->userfrom = $userfrom; $eventdata->userto = $this->recipient; $eventdata->subject = $this->postsubject; $eventdata->fullmessage = $this->notificationtext; $eventdata->fullmessageformat = FORMAT_PLAIN; $eventdata->fullmessagehtml = $this->notificationhtml; $eventdata->notification = 1; $eventdata->smallmessage = get_string('smallmessagedigest', 'forum', $this->sentcount); return message_send($eventdata); } /** * Helper to fetch the required renderer, instantiating as required. * * @param int $maildigest The type of mail digest being sent * @param bool $html Whether to fetch the HTML renderer * @return \core_renderer */ protected function get_renderer($maildigest, $html = false) { global $PAGE; $type = $maildigest == 2 ? 'emaildigestbasic' : 'emaildigestfull'; $target = $html ? 'htmlemail' : 'textemail'; if (!isset($this->renderers[$target][$type])) { $this->renderers[$target][$type] = $PAGE->get_renderer('mod_forum', $type, $target); } return $this->renderers[$target][$type]; } } PK-B\b.Y.Y(classes/task/send_user_notifications.phpnu[. /** * This file defines an adhoc task to send notifications. * * @package mod_forum * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\task; defined('MOODLE_INTERNAL') || die(); /** * Adhoc task to send user forum notifications. * * @package mod_forum * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class send_user_notifications extends \core\task\adhoc_task { // Use the logging trait to get some nice, juicy, logging. use \core\task\logging_trait; /** * @var \stdClass A shortcut to $USER. */ protected $recipient; /** * @var \stdClass[] List of courses the messages are in, indexed by courseid. */ protected $courses = []; /** * @var \stdClass[] List of forums the messages are in, indexed by courseid. */ protected $forums = []; /** * @var int[] List of IDs for forums in each course. */ protected $courseforums = []; /** * @var \stdClass[] List of discussions the messages are in, indexed by forumid. */ protected $discussions = []; /** * @var \stdClass[] List of IDs for discussions in each forum. */ protected $forumdiscussions = []; /** * @var \stdClass[] List of posts the messages are in, indexed by discussionid. */ protected $posts = []; /** * @var bool[] Whether the user can view fullnames for each forum. */ protected $viewfullnames = []; /** * @var bool[] Whether the user can post in each discussion. */ protected $canpostto = []; /** * @var \renderer[] The renderers. */ protected $renderers = []; /** * @var \core\message\inbound\address_manager The inbound message address manager. */ protected $inboundmanager; /** * @var array List of users. */ protected $users = []; /** * Send out messages. * @throws \moodle_exception */ public function execute() { global $CFG; // Raise the time limit for each discussion. \core_php_time_limit::raise(120); $this->recipient = \core_user::get_user($this->get_userid()); // Create the generic messageinboundgenerator. $this->inboundmanager = new \core\message\inbound\address_manager(); $this->inboundmanager->set_handler('\mod_forum\message\inbound\reply_handler'); $data = $this->get_custom_data(); $this->prepare_data((array) $data); $failedposts = []; $markposts = []; $errorcount = 0; $sentcount = 0; $this->log_start("Sending messages to {$this->recipient->username} ({$this->recipient->id})"); foreach ($this->courses as $course) { $coursecontext = \context_course::instance($course->id); if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) { // The course is hidden and the user does not have access to it. // Permissions may have changed since it was queued. continue; } foreach ($this->courseforums[$course->id] as $forumid) { $forum = $this->forums[$forumid]; $cm = get_fast_modinfo($course)->instances['forum'][$forumid]; $modcontext = \context_module::instance($cm->id); foreach (array_values($this->forumdiscussions[$forumid]) as $discussionid) { $discussion = $this->discussions[$discussionid]; if (!forum_user_can_see_discussion($forum, $discussion, $modcontext, $this->recipient)) { // User cannot see this discussion. // Permissions may have changed since it was queued. continue; } if (!\mod_forum\subscriptions::is_subscribed($this->recipient->id, $forum, $discussionid, $cm)) { // The user does not subscribe to this forum as a whole, or to this specific discussion. continue; } foreach ($this->posts[$discussionid] as $post) { if (!forum_user_can_see_post($forum, $discussion, $post, $this->recipient, $cm)) { // User cannot see this post. // Permissions may have changed since it was queued. continue; } if ($this->send_post($course, $forum, $discussion, $post, $cm, $modcontext)) { $this->log("Post {$post->id} sent", 1); // Mark post as read if forum_usermarksread is set off. if (!$CFG->forum_usermarksread) { $markposts[$post->id] = true; } $sentcount++; } else { $this->log("Failed to send post {$post->id}", 1); $failedposts[] = $post->id; $errorcount++; } } } } } $this->log_finish("Sent {$sentcount} messages with {$errorcount} failures"); if (!empty($markposts)) { if (get_user_preferences('forum_markasreadonnotification', 1, $this->recipient->id) == 1) { $this->log_start("Marking posts as read"); $count = count($markposts); forum_tp_mark_posts_read($this->recipient, array_keys($markposts)); $this->log_finish("Marked {$count} posts as read"); } } if ($errorcount > 0 && $sentcount === 0) { // All messages errored. So fail. // Checking if the task failed because of empty email address so that it doesn't get rescheduled. if (!empty($this->recipient->email)) { throw new \moodle_exception('Error sending posts.'); } else { mtrace("Failed to send emails for the user with ID ". $this->recipient->id ." due to an empty email address. Skipping re-queuing of the task."); } } else if ($errorcount > 0) { // Requeue failed messages as a new task. $task = new send_user_notifications(); $task->set_userid($this->recipient->id); $task->set_custom_data($failedposts); $task->set_component('mod_forum'); $task->set_next_run_time(time() + MINSECS); $task->set_fail_delay(MINSECS); \core\task\manager::reschedule_or_queue_adhoc_task($task); } } /** * Prepare all data for this run. * * Take all post ids, and fetch the relevant authors, discussions, forums, and courses for them. * * @param int[] $postids The list of post IDs */ protected function prepare_data(array $postids) { global $DB; if (empty($postids)) { return; } list($in, $params) = $DB->get_in_or_equal(array_values($postids)); $sql = "SELECT p.*, f.id AS forum, f.course FROM {forum_posts} p INNER JOIN {forum_discussions} d ON d.id = p.discussion INNER JOIN {forum} f ON f.id = d.forum WHERE p.id {$in}"; $posts = $DB->get_recordset_sql($sql, $params); $discussionids = []; $forumids = []; $courseids = []; $userids = []; foreach ($posts as $post) { $discussionids[] = $post->discussion; $forumids[] = $post->forum; $courseids[] = $post->course; $userids[] = $post->userid; unset($post->forum); if (!isset($this->posts[$post->discussion])) { $this->posts[$post->discussion] = []; } $this->posts[$post->discussion][$post->id] = $post; } $posts->close(); if (empty($discussionids)) { // All posts have been removed since the task was queued. return; } // Fetch all discussions. list($in, $params) = $DB->get_in_or_equal(array_values($discussionids)); $this->discussions = $DB->get_records_select('forum_discussions', "id {$in}", $params); foreach ($this->discussions as $discussion) { if (empty($this->forumdiscussions[$discussion->forum])) { $this->forumdiscussions[$discussion->forum] = []; } $this->forumdiscussions[$discussion->forum][] = $discussion->id; } // Fetch all forums. list($in, $params) = $DB->get_in_or_equal(array_values($forumids)); $this->forums = $DB->get_records_select('forum', "id {$in}", $params); foreach ($this->forums as $forum) { if (empty($this->courseforums[$forum->course])) { $this->courseforums[$forum->course] = []; } $this->courseforums[$forum->course][] = $forum->id; } // Fetch all courses. list($in, $params) = $DB->get_in_or_equal(array_values($courseids)); $this->courses = $DB->get_records_select('course', "id $in", $params); // Fetch all authors. list($in, $params) = $DB->get_in_or_equal(array_values($userids)); $users = $DB->get_recordset_select('user', "id $in", $params); foreach ($users as $user) { $this->minimise_user_record($user); $this->users[$user->id] = $user; } $users->close(); // Fill subscription caches for each forum. // These are per-user. foreach (array_values($forumids) as $id) { \mod_forum\subscriptions::fill_subscription_cache($id); \mod_forum\subscriptions::fill_discussion_subscription_cache($id); } } /** * Send the specified post for the current user. * * @param \stdClass $course * @param \stdClass $forum * @param \stdClass $discussion * @param \stdClass $post * @param \stdClass $cm * @param \context $context */ protected function send_post($course, $forum, $discussion, $post, $cm, $context) { global $CFG, $PAGE; $author = $this->get_post_author($post->userid, $course, $forum, $cm, $context); if (empty($author)) { return false; } // Prepare to actually send the post now, and build up the content. $cleanforumname = str_replace('"', "'", strip_tags(format_string($forum->name))); $shortname = format_string($course->shortname, true, [ 'context' => \context_course::instance($course->id), ]); // Generate a reply-to address from using the Inbound Message handler. $replyaddress = $this->get_reply_address($course, $forum, $discussion, $post, $cm, $context); $data = new \mod_forum\output\forum_post_email( $course, $cm, $forum, $discussion, $post, $author, $this->recipient, $this->can_post($course, $forum, $discussion, $post, $cm, $context) ); $data->viewfullnames = $this->can_view_fullnames($course, $forum, $discussion, $post, $cm, $context); // Not all of these variables are used in the default string but are made available to support custom subjects. $site = get_site(); $a = (object) [ 'subject' => $data->get_subject(), 'forumname' => $cleanforumname, 'sitefullname' => format_string($site->fullname), 'siteshortname' => format_string($site->shortname), 'courseidnumber' => $data->get_courseidnumber(), 'coursefullname' => $data->get_coursefullname(), 'courseshortname' => $data->get_coursename(), ]; $postsubject = html_to_text(get_string('postmailsubject', 'forum', $a), 0); // Message headers are stored against the message author. $author->customheaders = $this->get_message_headers($course, $forum, $discussion, $post, $a, $data); $eventdata = new \core\message\message(); $eventdata->courseid = $course->id; $eventdata->component = 'mod_forum'; $eventdata->name = 'posts'; $eventdata->userfrom = $author; $eventdata->userto = $this->recipient; $eventdata->subject = $postsubject; $eventdata->fullmessage = $this->get_renderer()->render($data); $eventdata->fullmessageformat = FORMAT_PLAIN; $eventdata->fullmessagehtml = $this->get_renderer(true)->render($data); $eventdata->notification = 1; $eventdata->replyto = $replyaddress; if (!empty($replyaddress)) { // Add extra text to email messages if they can reply back. $eventdata->set_additional_content('email', [ 'fullmessage' => [ 'footer' => "\n\n" . get_string('replytopostbyemail', 'mod_forum'), ], 'fullmessagehtml' => [ 'footer' => \html_writer::tag('p', get_string('replytopostbyemail', 'mod_forum')), ] ]); } $eventdata->smallmessage = get_string('smallmessage', 'forum', (object) [ 'user' => fullname($author), 'forumname' => "$shortname: " . format_string($forum->name, true) . ": " . $discussion->name, 'message' => $post->message, ]); $contexturl = new \moodle_url('/mod/forum/discuss.php', ['d' => $discussion->id], "p{$post->id}"); $eventdata->contexturl = $contexturl->out(); $eventdata->contexturlname = $discussion->name; // User image. $userpicture = new \user_picture($author); $userpicture->size = 1; // Use f1 size. $userpicture->includetoken = $this->recipient->id; // Generate an out-of-session token for the user receiving the message. $eventdata->customdata = [ 'cmid' => $cm->id, 'instance' => $forum->id, 'discussionid' => $discussion->id, 'postid' => $post->id, 'notificationiconurl' => $userpicture->get_url($PAGE)->out(false), 'actionbuttons' => [ 'reply' => get_string_manager()->get_string('reply', 'forum', null, $eventdata->userto->lang), ], ]; return message_send($eventdata); } /** * Fetch and initialise the post author. * * @param int $userid The id of the user to fetch * @param \stdClass $course * @param \stdClass $forum * @param \stdClass $cm * @param \context $context * @return \stdClass */ protected function get_post_author($userid, $course, $forum, $cm, $context) { if (!isset($this->users[$userid])) { // This user no longer exists. return false; } $user = $this->users[$userid]; if (!isset($user->groups)) { // Initialise the groups list. $user->groups = []; } if (!isset($user->groups[$forum->id])) { $user->groups[$forum->id] = groups_get_all_groups($course->id, $user->id, $cm->groupingid); } // Clone the user object to prevent leaks between messages. return (object) (array) $user; } /** * Helper to fetch the required renderer, instantiating as required. * * @param bool $html Whether to fetch the HTML renderer * @return \core_renderer */ protected function get_renderer($html = false) { global $PAGE; $target = $html ? 'htmlemail' : 'textemail'; if (!isset($this->renderers[$target])) { $this->renderers[$target] = $PAGE->get_renderer('mod_forum', 'email', $target); } return $this->renderers[$target]; } /** * Get the list of message headers. * * @param \stdClass $course * @param \stdClass $forum * @param \stdClass $discussion * @param \stdClass $post * @param \stdClass $a The list of strings for this post * @param \core\message\message $message The message to be sent * @return \stdClass */ protected function get_message_headers($course, $forum, $discussion, $post, $a, $message) { $cleanforumname = str_replace('"', "'", strip_tags(format_string($forum->name))); $viewurl = new \moodle_url('/mod/forum/view.php', ['f' => $forum->id]); $headers = [ // Headers to make emails easier to track. 'List-Id: "' . $cleanforumname . '" ' . generate_email_messageid('moodleforum' . $forum->id), 'List-Help: ' . $viewurl->out(), 'Message-ID: ' . forum_get_email_message_id($post->id, $this->recipient->id), 'X-Course-Id: ' . $course->id, 'X-Course-Name: '. format_string($course->fullname, true), // Headers to help prevent auto-responders. 'Precedence: Bulk', 'X-Auto-Response-Suppress: All', 'Auto-Submitted: auto-generated', 'List-Unsubscribe: <' . $message->get_unsubscribediscussionlink() . '>', ]; $rootid = forum_get_email_message_id($discussion->firstpost, $this->recipient->id); if ($post->parent) { // This post is a reply, so add reply header (RFC 2822). $parentid = forum_get_email_message_id($post->parent, $this->recipient->id); $headers[] = "In-Reply-To: $parentid"; // If the post is deeply nested we also reference the parent message id and // the root message id (if different) to aid threading when parts of the email // conversation have been deleted (RFC1036). if ($post->parent != $discussion->firstpost) { $headers[] = "References: $rootid $parentid"; } else { $headers[] = "References: $parentid"; } } else { // If the message IDs that Moodle creates are overwritten then referencing these // IDs here will enable then to still thread correctly with the first email. $headers[] = "In-Reply-To: $rootid"; $headers[] = "References: $rootid"; } // MS Outlook / Office uses poorly documented and non standard headers, including // Thread-Topic which overrides the Subject and shouldn't contain Re: or Fwd: etc. $aclone = (object) (array) $a; $aclone->subject = $discussion->name; $threadtopic = html_to_text(get_string('postmailsubject', 'forum', $aclone), 0); $headers[] = "Thread-Topic: $threadtopic"; $headers[] = "Thread-Index: " . substr($rootid, 1, 28); return $headers; } /** * Get a no-reply address for this user to reply to the current post. * * @param \stdClass $course * @param \stdClass $forum * @param \stdClass $discussion * @param \stdClass $post * @param \stdClass $cm * @param \context $context * @return string */ protected function get_reply_address($course, $forum, $discussion, $post, $cm, $context) { if ($this->can_post($course, $forum, $discussion, $post, $cm, $context)) { // Generate a reply-to address from using the Inbound Message handler. $this->inboundmanager->set_data($post->id); return $this->inboundmanager->generate($this->recipient->id); } // TODO Check if we can return a string. // This will be controlled by the event. return null; } /** * Check whether the user can post. * * @param \stdClass $course * @param \stdClass $forum * @param \stdClass $discussion * @param \stdClass $post * @param \stdClass $cm * @param \context $context * @return bool */ protected function can_post($course, $forum, $discussion, $post, $cm, $context) { if (!isset($this->canpostto[$discussion->id])) { $this->canpostto[$discussion->id] = forum_user_can_post($forum, $discussion, $this->recipient, $cm, $course, $context); } return $this->canpostto[$discussion->id]; } /** * Check whether the user can view full names of other users. * * @param \stdClass $course * @param \stdClass $forum * @param \stdClass $discussion * @param \stdClass $post * @param \stdClass $cm * @param \context $context * @return bool */ protected function can_view_fullnames($course, $forum, $discussion, $post, $cm, $context) { if (!isset($this->viewfullnames[$forum->id])) { $this->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $context, $this->recipient->id); } return $this->viewfullnames[$forum->id]; } /** * Removes properties from user record that are not necessary for sending post notifications. * * @param \stdClass $user */ protected function minimise_user_record(\stdClass $user) { // We store large amount of users in one huge array, make sure we do not store info there we do not actually // need in mail generation code or messaging. unset($user->institution); unset($user->department); unset($user->address); unset($user->city); unset($user->currentlogin); unset($user->description); unset($user->descriptionformat); } } PK-B\L59uuclasses/subscriptions.phpnu[. /** * Forum subscription manager. * * @package mod_forum * @copyright 2014 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum; defined('MOODLE_INTERNAL') || die(); /** * Forum subscription manager. * * @copyright 2014 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class subscriptions { /** * The status value for an unsubscribed discussion. * * @var int */ const FORUM_DISCUSSION_UNSUBSCRIBED = -1; /** * The subscription cache for forums. * * The first level key is the user ID * The second level is the forum ID * The Value then is bool for subscribed of not. * * @var array[] An array of arrays. */ protected static $forumcache = array(); /** * The list of forums which have been wholly retrieved for the forum subscription cache. * * This allows for prior caching of an entire forum to reduce the * number of DB queries in a subscription check loop. * * @var bool[] */ protected static $fetchedforums = array(); /** * The subscription cache for forum discussions. * * The first level key is the user ID * The second level is the forum ID * The third level key is the discussion ID * The value is then the users preference (int) * * @var array[] */ protected static $forumdiscussioncache = array(); /** * The list of forums which have been wholly retrieved for the forum discussion subscription cache. * * This allows for prior caching of an entire forum to reduce the * number of DB queries in a subscription check loop. * * @var bool[] */ protected static $discussionfetchedforums = array(); /** * Whether a user is subscribed to this forum, or a discussion within * the forum. * * If a discussion is specified, then report whether the user is * subscribed to posts to this particular discussion, taking into * account the forum preference. * * If it is not specified then only the forum preference is considered. * * @param int $userid The user ID * @param \stdClass $forum The record of the forum to test * @param int $discussionid The ID of the discussion to check * @param $cm The coursemodule record. If not supplied, this will be calculated using get_fast_modinfo instead. * @return boolean */ public static function is_subscribed($userid, $forum, $discussionid = null, $cm = null) { // If forum is force subscribed and has allowforcesubscribe, then user is subscribed. if (self::is_forcesubscribed($forum)) { if (!$cm) { $cm = get_fast_modinfo($forum->course)->instances['forum'][$forum->id]; } if (has_capability('mod/forum:allowforcesubscribe', \context_module::instance($cm->id), $userid)) { return true; } } if ($discussionid === null) { return self::is_subscribed_to_forum($userid, $forum); } $subscriptions = self::fetch_discussion_subscription($forum->id, $userid); // Check whether there is a record for this discussion subscription. if (isset($subscriptions[$discussionid])) { return ($subscriptions[$discussionid] != self::FORUM_DISCUSSION_UNSUBSCRIBED); } return self::is_subscribed_to_forum($userid, $forum); } /** * Whether a user is subscribed to this forum. * * @param int $userid The user ID * @param \stdClass $forum The record of the forum to test * @return boolean */ protected static function is_subscribed_to_forum($userid, $forum) { return self::fetch_subscription_cache($forum->id, $userid); } /** * Helper to determine whether a forum has it's subscription mode set * to forced subscription. * * @param \stdClass $forum The record of the forum to test * @return bool */ public static function is_forcesubscribed($forum) { return ($forum->forcesubscribe == FORUM_FORCESUBSCRIBE); } /** * Helper to determine whether a forum has it's subscription mode set to disabled. * * @param \stdClass $forum The record of the forum to test * @return bool */ public static function subscription_disabled($forum) { return ($forum->forcesubscribe == FORUM_DISALLOWSUBSCRIBE); } /** * Helper to determine whether the specified forum can be subscribed to. * * @param \stdClass $forum The record of the forum to test * @return bool */ public static function is_subscribable($forum) { return (isloggedin() && !isguestuser() && !\mod_forum\subscriptions::is_forcesubscribed($forum) && !\mod_forum\subscriptions::subscription_disabled($forum)); } /** * Set the forum subscription mode. * * By default when called without options, this is set to FORUM_FORCESUBSCRIBE. * * @param \stdClass $forum The record of the forum to set * @param int $status The new subscription state * @return bool true * @throws dml_exception A DML specific exception is thrown for any errors. */ public static function set_subscription_mode($forum, $status = FORUM_FORCESUBSCRIBE): bool { global $DB; if (is_numeric($forum)) { debugging(__METHOD__.': Argument #1 ($forum) must be a stdClass record of a forum', DEBUG_DEVELOPER); $forum = $DB->get_record("forum", ["id" => $forum], '*', MUST_EXIST); } $DB->set_field("forum", "forcesubscribe", $status, ["id" => $forum->id]); if ($forum->forcesubscribe != $status) { // Trigger event if subscription mode has been changed. $event = \mod_forum\event\subscription_mode_updated::create([ "context" => forum_get_context($forum->id), "objectid" => $forum->id, "other" => ["oldvalue" => $forum->forcesubscribe, "newvalue" => $status], ]); $event->add_record_snapshot("forum", $forum); $event->trigger(); } return true; } /** * Returns the current subscription mode for the forum. * * @param \stdClass $forum The record of the forum to set * @return int The forum subscription mode */ public static function get_subscription_mode($forum) { return $forum->forcesubscribe; } /** * Returns an array of forums that the current user is subscribed to and is allowed to unsubscribe from * * @return array An array of unsubscribable forums */ public static function get_unsubscribable_forums() { global $USER, $DB; // Get courses that $USER is enrolled in and can see. $courses = enrol_get_my_courses(); if (empty($courses)) { return array(); } $courseids = array(); foreach($courses as $course) { $courseids[] = $course->id; } list($coursesql, $courseparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED, 'c'); // Get all forums from the user's courses that they are subscribed to and which are not set to forced. // It is possible for users to be subscribed to a forum in subscription disallowed mode so they must be listed // here so that that can be unsubscribed from. $sql = "SELECT f.id, cm.id as cm, cm.visible, f.course FROM {forum} f JOIN {course_modules} cm ON cm.instance = f.id JOIN {modules} m ON m.name = :modulename AND m.id = cm.module LEFT JOIN {forum_subscriptions} fs ON (fs.forum = f.id AND fs.userid = :userid) WHERE f.forcesubscribe <> :forcesubscribe AND fs.id IS NOT NULL AND cm.course $coursesql"; $params = array_merge($courseparams, array( 'modulename'=>'forum', 'userid' => $USER->id, 'forcesubscribe' => FORUM_FORCESUBSCRIBE, )); $forums = $DB->get_recordset_sql($sql, $params); $unsubscribableforums = array(); foreach($forums as $forum) { if (empty($forum->visible)) { // The forum is hidden - check if the user can view the forum. $context = \context_module::instance($forum->cm); if (!has_capability('moodle/course:viewhiddenactivities', $context)) { // The user can't see the hidden forum to cannot unsubscribe. continue; } } $unsubscribableforums[] = $forum; } $forums->close(); return $unsubscribableforums; } /** * Get the list of potential subscribers to a forum. * * @param context_module $context the forum context. * @param integer $groupid the id of a group, or 0 for all groups. * @param string $fields the list of fields to return for each user. As for get_users_by_capability. * @param string $sort sort order. As for get_users_by_capability. * @return array list of users. */ public static function get_potential_subscribers($context, $groupid, $fields, $sort = '') { global $DB; // Only active enrolled users or everybody on the frontpage. list($esql, $params) = get_enrolled_sql($context, 'mod/forum:allowforcesubscribe', $groupid, true); if (!$sort) { list($sort, $sortparams) = users_order_by_sql('u'); $params = array_merge($params, $sortparams); } $sql = "SELECT $fields FROM {user} u JOIN ($esql) je ON je.id = u.id WHERE u.auth <> 'nologin' AND u.suspended = 0 AND u.confirmed = 1 ORDER BY $sort"; return $DB->get_records_sql($sql, $params); } /** * Fetch the forum subscription data for the specified userid and forum. * * @param int $forumid The forum to retrieve a cache for * @param int $userid The user ID * @return boolean */ public static function fetch_subscription_cache($forumid, $userid) { if (isset(self::$forumcache[$userid]) && isset(self::$forumcache[$userid][$forumid])) { return self::$forumcache[$userid][$forumid]; } self::fill_subscription_cache($forumid, $userid); if (!isset(self::$forumcache[$userid]) || !isset(self::$forumcache[$userid][$forumid])) { return false; } return self::$forumcache[$userid][$forumid]; } /** * Fill the forum subscription data for the specified userid and forum. * * If the userid is not specified, then all subscription data for that forum is fetched in a single query and used * for subsequent lookups without requiring further database queries. * * @param int $forumid The forum to retrieve a cache for * @param int $userid The user ID * @return void */ public static function fill_subscription_cache($forumid, $userid = null) { global $DB; if (!isset(self::$fetchedforums[$forumid])) { // This forum has not been fetched as a whole. if (isset($userid)) { if (!isset(self::$forumcache[$userid])) { self::$forumcache[$userid] = array(); } if (!isset(self::$forumcache[$userid][$forumid])) { if ($DB->record_exists('forum_subscriptions', array( 'userid' => $userid, 'forum' => $forumid, ))) { self::$forumcache[$userid][$forumid] = true; } else { self::$forumcache[$userid][$forumid] = false; } } } else { $subscriptions = $DB->get_recordset('forum_subscriptions', array( 'forum' => $forumid, ), '', 'id, userid'); foreach ($subscriptions as $id => $data) { if (!isset(self::$forumcache[$data->userid])) { self::$forumcache[$data->userid] = array(); } self::$forumcache[$data->userid][$forumid] = true; } self::$fetchedforums[$forumid] = true; $subscriptions->close(); } } } /** * Fill the forum subscription data for all forums that the specified userid can subscribe to in the specified course. * * @param int $courseid The course to retrieve a cache for * @param int $userid The user ID * @return void */ public static function fill_subscription_cache_for_course($courseid, $userid) { global $DB; if (!isset(self::$forumcache[$userid])) { self::$forumcache[$userid] = array(); } $sql = "SELECT f.id AS forumid, s.id AS subscriptionid FROM {forum} f LEFT JOIN {forum_subscriptions} s ON (s.forum = f.id AND s.userid = :userid) WHERE f.course = :course AND f.forcesubscribe <> :subscriptionforced"; $subscriptions = $DB->get_recordset_sql($sql, array( 'course' => $courseid, 'userid' => $userid, 'subscriptionforced' => FORUM_FORCESUBSCRIBE, )); foreach ($subscriptions as $id => $data) { self::$forumcache[$userid][$id] = !empty($data->subscriptionid); } $subscriptions->close(); } /** * Returns a list of user objects who are subscribed to this forum. * * @param stdClass $forum The forum record. * @param int $groupid The group id if restricting subscriptions to a group of users, or 0 for all. * @param context_module $context the forum context, to save re-fetching it where possible. * @param string $fields requested user fields (with "u." table prefix). * @param boolean $includediscussionsubscriptions Whether to take discussion subscriptions and unsubscriptions into consideration. * @return array list of users. */ public static function fetch_subscribed_users($forum, $groupid = 0, $context = null, $fields = null, $includediscussionsubscriptions = false) { global $CFG, $DB; if (empty($fields)) { $userfieldsapi = \core_user\fields::for_name(); $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $fields ="u.id, u.username, $allnames, u.maildisplay, u.mailformat, u.maildigest, u.imagealt, u.email, u.emailstop, u.city, u.country, u.lastaccess, u.lastlogin, u.picture, u.timezone, u.theme, u.lang, u.trackforums, u.mnethostid"; } // Retrieve the forum context if it wasn't specified. $context = forum_get_context($forum->id, $context); if (self::is_forcesubscribed($forum)) { $results = self::get_potential_subscribers($context, $groupid, $fields); } else { // Only active enrolled users or everybody on the frontpage. list($esql, $params) = get_enrolled_sql($context, '', $groupid, true); $params['forumid'] = $forum->id; list($sort, $sortparams) = users_order_by_sql('u'); $params = array_merge($params, $sortparams); if ($includediscussionsubscriptions) { $params['sforumid'] = $forum->id; $params['dsforumid'] = $forum->id; $params['unsubscribed'] = self::FORUM_DISCUSSION_UNSUBSCRIBED; $sql = "SELECT $fields FROM ( SELECT userid FROM {forum_subscriptions} s WHERE s.forum = :sforumid UNION SELECT userid FROM {forum_discussion_subs} ds WHERE ds.forum = :dsforumid AND ds.preference <> :unsubscribed ) subscriptions JOIN {user} u ON u.id = subscriptions.userid JOIN ($esql) je ON je.id = u.id WHERE u.auth <> 'nologin' AND u.suspended = 0 AND u.confirmed = 1 ORDER BY $sort"; } else { $sql = "SELECT $fields FROM {user} u JOIN ($esql) je ON je.id = u.id JOIN {forum_subscriptions} s ON s.userid = u.id WHERE s.forum = :forumid AND u.auth <> 'nologin' AND u.suspended = 0 AND u.confirmed = 1 ORDER BY $sort"; } $results = $DB->get_records_sql($sql, $params); } // Guest user should never be subscribed to a forum. unset($results[$CFG->siteguest]); // Apply the activity module availability resetrictions. $cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course); $modinfo = get_fast_modinfo($forum->course); $info = new \core_availability\info_module($modinfo->get_cm($cm->id)); $results = $info->filter_user_list($results); return $results; } /** * Retrieve the discussion subscription data for the specified userid and forum. * * This is returned as an array of discussions for that forum which contain the preference in a stdClass. * * @param int $forumid The forum to retrieve a cache for * @param int $userid The user ID * @return array of stdClass objects with one per discussion in the forum. */ public static function fetch_discussion_subscription($forumid, $userid = null) { self::fill_discussion_subscription_cache($forumid, $userid); if (!isset(self::$forumdiscussioncache[$userid]) || !isset(self::$forumdiscussioncache[$userid][$forumid])) { return array(); } return self::$forumdiscussioncache[$userid][$forumid]; } /** * Fill the discussion subscription data for the specified userid and forum. * * If the userid is not specified, then all discussion subscription data for that forum is fetched in a single query * and used for subsequent lookups without requiring further database queries. * * @param int $forumid The forum to retrieve a cache for * @param int $userid The user ID * @return void */ public static function fill_discussion_subscription_cache($forumid, $userid = null) { global $DB; if (!isset(self::$discussionfetchedforums[$forumid])) { // This forum hasn't been fetched as a whole yet. if (isset($userid)) { if (!isset(self::$forumdiscussioncache[$userid])) { self::$forumdiscussioncache[$userid] = array(); } if (!isset(self::$forumdiscussioncache[$userid][$forumid])) { $subscriptions = $DB->get_recordset('forum_discussion_subs', array( 'userid' => $userid, 'forum' => $forumid, ), null, 'id, discussion, preference'); self::$forumdiscussioncache[$userid][$forumid] = array(); foreach ($subscriptions as $id => $data) { self::add_to_discussion_cache($forumid, $userid, $data->discussion, $data->preference); } $subscriptions->close(); } } else { $subscriptions = $DB->get_recordset('forum_discussion_subs', array( 'forum' => $forumid, ), null, 'id, userid, discussion, preference'); foreach ($subscriptions as $id => $data) { self::add_to_discussion_cache($forumid, $data->userid, $data->discussion, $data->preference); } self::$discussionfetchedforums[$forumid] = true; $subscriptions->close(); } } } /** * Add the specified discussion and user preference to the discussion * subscription cache. * * @param int $forumid The ID of the forum that this preference belongs to * @param int $userid The ID of the user that this preference belongs to * @param int $discussion The ID of the discussion that this preference relates to * @param int $preference The preference to store */ protected static function add_to_discussion_cache($forumid, $userid, $discussion, $preference) { if (!isset(self::$forumdiscussioncache[$userid])) { self::$forumdiscussioncache[$userid] = array(); } if (!isset(self::$forumdiscussioncache[$userid][$forumid])) { self::$forumdiscussioncache[$userid][$forumid] = array(); } self::$forumdiscussioncache[$userid][$forumid][$discussion] = $preference; } /** * Reset the discussion cache. * * This cache is used to reduce the number of database queries when * checking forum discussion subscription states. */ public static function reset_discussion_cache() { self::$forumdiscussioncache = array(); self::$discussionfetchedforums = array(); } /** * Reset the forum cache. * * This cache is used to reduce the number of database queries when * checking forum subscription states. */ public static function reset_forum_cache() { self::$forumcache = array(); self::$fetchedforums = array(); } /** * Adds user to the subscriber list. * * @param int $userid The ID of the user to subscribe * @param \stdClass $forum The forum record for this forum. * @param \context_module|null $context Module context, may be omitted if not known or if called for the current * module set in page. * @param boolean $userrequest Whether the user requested this change themselves. This has an effect on whether * discussion subscriptions are removed too. * @return bool|int Returns true if the user is already subscribed, or the forum_subscriptions ID if the user was * successfully subscribed. */ public static function subscribe_user($userid, $forum, $context = null, $userrequest = false) { global $DB; if (self::is_subscribed($userid, $forum)) { return true; } $sub = new \stdClass(); $sub->userid = $userid; $sub->forum = $forum->id; $result = $DB->insert_record("forum_subscriptions", $sub); if ($userrequest) { $discussionsubscriptions = $DB->get_recordset('forum_discussion_subs', array('userid' => $userid, 'forum' => $forum->id)); $DB->delete_records_select('forum_discussion_subs', 'userid = :userid AND forum = :forumid AND preference <> :preference', array( 'userid' => $userid, 'forumid' => $forum->id, 'preference' => self::FORUM_DISCUSSION_UNSUBSCRIBED, )); // Reset the subscription caches for this forum. // We know that the there were previously entries and there aren't any more. if (isset(self::$forumdiscussioncache[$userid]) && isset(self::$forumdiscussioncache[$userid][$forum->id])) { foreach (self::$forumdiscussioncache[$userid][$forum->id] as $discussionid => $preference) { if ($preference != self::FORUM_DISCUSSION_UNSUBSCRIBED) { unset(self::$forumdiscussioncache[$userid][$forum->id][$discussionid]); } } } } // Reset the cache for this forum. self::$forumcache[$userid][$forum->id] = true; $context = forum_get_context($forum->id, $context); $params = array( 'context' => $context, 'objectid' => $result, 'relateduserid' => $userid, 'other' => array('forumid' => $forum->id), ); $event = event\subscription_created::create($params); if ($userrequest && $discussionsubscriptions) { foreach ($discussionsubscriptions as $subscription) { $event->add_record_snapshot('forum_discussion_subs', $subscription); } $discussionsubscriptions->close(); } $event->trigger(); return $result; } /** * Removes user from the subscriber list * * @param int $userid The ID of the user to unsubscribe * @param \stdClass $forum The forum record for this forum. * @param \context_module|null $context Module context, may be omitted if not known or if called for the current * module set in page. * @param boolean $userrequest Whether the user requested this change themselves. This has an effect on whether * discussion subscriptions are removed too. * @return boolean Always returns true. */ public static function unsubscribe_user($userid, $forum, $context = null, $userrequest = false) { global $DB; $sqlparams = array( 'userid' => $userid, 'forum' => $forum->id, ); $DB->delete_records('forum_digests', $sqlparams); if ($forumsubscription = $DB->get_record('forum_subscriptions', $sqlparams)) { $DB->delete_records('forum_subscriptions', array('id' => $forumsubscription->id)); if ($userrequest) { $discussionsubscriptions = $DB->get_recordset('forum_discussion_subs', $sqlparams); $DB->delete_records('forum_discussion_subs', array('userid' => $userid, 'forum' => $forum->id, 'preference' => self::FORUM_DISCUSSION_UNSUBSCRIBED)); // We know that the there were previously entries and there aren't any more. if (isset(self::$forumdiscussioncache[$userid]) && isset(self::$forumdiscussioncache[$userid][$forum->id])) { self::$forumdiscussioncache[$userid][$forum->id] = array(); } } // Reset the cache for this forum. self::$forumcache[$userid][$forum->id] = false; $context = forum_get_context($forum->id, $context); $params = array( 'context' => $context, 'objectid' => $forumsubscription->id, 'relateduserid' => $userid, 'other' => array('forumid' => $forum->id), ); $event = event\subscription_deleted::create($params); $event->add_record_snapshot('forum_subscriptions', $forumsubscription); if ($userrequest && $discussionsubscriptions) { foreach ($discussionsubscriptions as $subscription) { $event->add_record_snapshot('forum_discussion_subs', $subscription); } $discussionsubscriptions->close(); } $event->trigger(); } return true; } /** * Subscribes the user to the specified discussion. * * @param int $userid The userid of the user being subscribed * @param \stdClass $discussion The discussion to subscribe to * @param \context_module|null $context Module context, may be omitted if not known or if called for the current * module set in page. * @return boolean Whether a change was made */ public static function subscribe_user_to_discussion($userid, $discussion, $context = null) { global $DB; // First check whether the user is subscribed to the discussion already. $subscription = $DB->get_record('forum_discussion_subs', array('userid' => $userid, 'discussion' => $discussion->id)); if ($subscription) { if ($subscription->preference != self::FORUM_DISCUSSION_UNSUBSCRIBED) { // The user is already subscribed to the discussion. Ignore. return false; } } // No discussion-level subscription. Check for a forum level subscription. if ($DB->record_exists('forum_subscriptions', array('userid' => $userid, 'forum' => $discussion->forum))) { if ($subscription && $subscription->preference == self::FORUM_DISCUSSION_UNSUBSCRIBED) { // The user is subscribed to the forum, but unsubscribed from the discussion, delete the discussion preference. $DB->delete_records('forum_discussion_subs', array('id' => $subscription->id)); unset(self::$forumdiscussioncache[$userid][$discussion->forum][$discussion->id]); } else { // The user is already subscribed to the forum. Ignore. return false; } } else { if ($subscription) { $subscription->preference = time(); $DB->update_record('forum_discussion_subs', $subscription); } else { $subscription = new \stdClass(); $subscription->userid = $userid; $subscription->forum = $discussion->forum; $subscription->discussion = $discussion->id; $subscription->preference = time(); $subscription->id = $DB->insert_record('forum_discussion_subs', $subscription); self::$forumdiscussioncache[$userid][$discussion->forum][$discussion->id] = $subscription->preference; } } $context = forum_get_context($discussion->forum, $context); $params = array( 'context' => $context, 'objectid' => $subscription->id, 'relateduserid' => $userid, 'other' => array( 'forumid' => $discussion->forum, 'discussion' => $discussion->id, ), ); $event = event\discussion_subscription_created::create($params); $event->trigger(); return true; } /** * Unsubscribes the user from the specified discussion. * * @param int $userid The userid of the user being unsubscribed * @param \stdClass $discussion The discussion to unsubscribe from * @param \context_module|null $context Module context, may be omitted if not known or if called for the current * module set in page. * @return boolean Whether a change was made */ public static function unsubscribe_user_from_discussion($userid, $discussion, $context = null) { global $DB; // First check whether the user's subscription preference for this discussion. $subscription = $DB->get_record('forum_discussion_subs', array('userid' => $userid, 'discussion' => $discussion->id)); if ($subscription) { if ($subscription->preference == self::FORUM_DISCUSSION_UNSUBSCRIBED) { // The user is already unsubscribed from the discussion. Ignore. return false; } } // No discussion-level preference. Check for a forum level subscription. if (!$DB->record_exists('forum_subscriptions', array('userid' => $userid, 'forum' => $discussion->forum))) { if ($subscription && $subscription->preference != self::FORUM_DISCUSSION_UNSUBSCRIBED) { // The user is not subscribed to the forum, but subscribed from the discussion, delete the discussion subscription. $DB->delete_records('forum_discussion_subs', array('id' => $subscription->id)); unset(self::$forumdiscussioncache[$userid][$discussion->forum][$discussion->id]); } else { // The user is not subscribed from the forum. Ignore. return false; } } else { if ($subscription) { $subscription->preference = self::FORUM_DISCUSSION_UNSUBSCRIBED; $DB->update_record('forum_discussion_subs', $subscription); } else { $subscription = new \stdClass(); $subscription->userid = $userid; $subscription->forum = $discussion->forum; $subscription->discussion = $discussion->id; $subscription->preference = self::FORUM_DISCUSSION_UNSUBSCRIBED; $subscription->id = $DB->insert_record('forum_discussion_subs', $subscription); } self::$forumdiscussioncache[$userid][$discussion->forum][$discussion->id] = $subscription->preference; } $context = forum_get_context($discussion->forum, $context); $params = array( 'context' => $context, 'objectid' => $subscription->id, 'relateduserid' => $userid, 'other' => array( 'forumid' => $discussion->forum, 'discussion' => $discussion->id, ), ); $event = event\discussion_subscription_deleted::create($params); $event->trigger(); return true; } /** * Gets the default subscription value for the logged in user. * * @param \stdClass $forum The forum record * @param \context $context The course context * @param \cm_info $cm cm_info * @param int|null $discussionid The discussion we are checking against * @return bool Default subscription * @throws coding_exception */ public static function get_user_default_subscription($forum, $context, $cm, ?int $discussionid) { global $USER; $manageactivities = has_capability('moodle/course:manageactivities', $context); if (\mod_forum\subscriptions::subscription_disabled($forum) && !$manageactivities) { // User does not have permission to subscribe to this discussion at all. $discussionsubscribe = false; } else if (\mod_forum\subscriptions::is_forcesubscribed($forum)) { // User does not have permission to unsubscribe from this discussion at all. $discussionsubscribe = true; } else { if (isset($discussionid) && self::is_subscribed($USER->id, $forum, $discussionid, $cm)) { // User is subscribed to the discussion - continue the subscription. $discussionsubscribe = true; } else if (!isset($discussionid) && \mod_forum\subscriptions::is_subscribed($USER->id, $forum, null, $cm)) { // Starting a new discussion, and the user is subscribed to the forum - subscribe to the discussion. $discussionsubscribe = true; } else { // User is not subscribed to either forum or discussion. Follow user preference. $discussionsubscribe = $USER->autosubscribe ?? false; } } return $discussionsubscribe; } } PK-B\vt%(classes/completion/custom_completion.phpnu[. declare(strict_types=1); namespace mod_forum\completion; use core_completion\activity_custom_completion; /** * Activity custom completion subclass for the forum activity. * * Class for defining mod_forum's custom completion rules and fetching the completion statuses * of the custom completion rules for a given forum instance and a user. * * @package mod_forum * @copyright Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class custom_completion extends activity_custom_completion { /** * Fetches the completion state for a given completion rule. * * @param string $rule The completion rule. * @return int The completion state. */ public function get_state(string $rule): int { global $DB; $this->validate_rule($rule); $userid = $this->userid; $forumid = $this->cm->instance; if (!$forum = $DB->get_record('forum', ['id' => $forumid])) { throw new \moodle_exception('Unable to find forum with id ' . $forumid); } $postcountparams = ['userid' => $userid, 'forumid' => $forumid]; $postcountsql = "SELECT COUNT(*) FROM {forum_posts} fp JOIN {forum_discussions} fd ON fp.discussion = fd.id WHERE fp.userid = :userid AND fd.forum = :forumid"; if ($rule == 'completiondiscussions') { $status = $forum->completiondiscussions <= $DB->count_records('forum_discussions', ['forum' => $forumid, 'userid' => $userid]); } else if ($rule == 'completionreplies') { $status = $forum->completionreplies <= $DB->get_field_sql($postcountsql . ' AND fp.parent <> 0', $postcountparams); } else if ($rule == 'completionposts') { $status = $forum->completionposts <= $DB->get_field_sql($postcountsql, $postcountparams); } return $status ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE; } /** * Fetch the list of custom completion rules that this module defines. * * @return array */ public static function get_defined_custom_rules(): array { return [ 'completiondiscussions', 'completionreplies', 'completionposts', ]; } /** * Returns an associative array of the descriptions of custom completion rules. * * @return array */ public function get_custom_rule_descriptions(): array { $completiondiscussions = $this->cm->customdata['customcompletionrules']['completiondiscussions'] ?? 0; $completionreplies = $this->cm->customdata['customcompletionrules']['completionreplies'] ?? 0; $completionposts = $this->cm->customdata['customcompletionrules']['completionposts'] ?? 0; return [ 'completiondiscussions' => get_string('completiondetail:discussions', 'forum', $completiondiscussions), 'completionreplies' => get_string('completiondetail:replies', 'forum', $completionreplies), 'completionposts' => get_string('completiondetail:posts', 'forum', $completionposts), ]; } /** * Returns an array of all completion rules, in the order they should be displayed to users. * * @return array */ public function get_sort_order(): array { return [ 'completionview', 'completiondiscussions', 'completionreplies', 'completionposts', 'completionusegrade', 'completionpassgrade', ]; } } PK-B\qr6++/classes/analytics/indicator/cognitive_depth.phpnu[. /** * Cognitive depth indicator - forum. * * @package mod_forum * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\analytics\indicator; defined('MOODLE_INTERNAL') || die(); /** * Cognitive depth indicator - forum. * * @package mod_forum * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cognitive_depth extends activity_base { /** * Returns the name. * * If there is a corresponding '_help' string this will be shown as well. * * @return \lang_string */ public static function get_name(): \lang_string { return new \lang_string('indicator:cognitivedepth', 'mod_forum'); } public function get_indicator_type() { return self::INDICATOR_COGNITIVE; } public function get_cognitive_depth_level(\cm_info $cm) { return self::COGNITIVE_LEVEL_4; } /** * feedback_check_grades * * @return bool */ protected function feedback_check_grades() { return false; } /** * feedback_replied_events * * @return string[] */ protected function feedback_replied_events() { return array('\mod_forum\event\assessable_uploaded'); } } PK-B\OH  -classes/analytics/indicator/activity_base.phpnu[. /** * Activity base class. * * @package mod_forum * @copyright 2017 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\analytics\indicator; defined('MOODLE_INTERNAL') || die(); /** * Activity base class. * * @package mod_forum * @copyright 2017 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class activity_base extends \core_analytics\local\indicator\community_of_inquiry_activity { /** * feedback_viewed_events * * @return string[] */ protected function feedback_viewed_events() { // We could add any forum event, but it will make feedback_post_action slower. return array('\mod_forum\event\assessable_uploaded', '\mod_forum\event\course_module_viewed', '\mod_forum\event\discussion_viewed'); } /** * feedback_post_action * * @param \cm_info $cm * @param int $contextid * @param int $userid * @param string[] $eventnames * @param int $after * @return bool */ protected function feedback_post_action(\cm_info $cm, $contextid, $userid, $eventnames, $after = false) { if (empty($this->activitylogs[$contextid][$userid])) { return false; } $logs = $this->activitylogs[$contextid][$userid]; if (empty($logs['\mod_forum\event\assessable_uploaded'])) { // No feedback viewed if there is no submission. return false; } // First user post time. $firstpost = $logs['\mod_forum\event\assessable_uploaded']->timecreated[0]; // We consider feedback any other user post in any of this forum discussions. foreach ($this->activitylogs[$contextid] as $anotheruserid => $logs) { if ($anotheruserid == $userid) { continue; } if (empty($logs['\mod_forum\event\assessable_uploaded'])) { continue; } $firstpostsenttime = $logs['\mod_forum\event\assessable_uploaded']->timecreated[0]; if (parent::feedback_post_action($cm, $contextid, $userid, $eventnames, $firstpostsenttime)) { return true; } // Continue with the next user. } return false; } } PK-B\/.classes/analytics/indicator/social_breadth.phpnu[. /** * Social breadth indicator - forum. * * @package mod_forum * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\analytics\indicator; defined('MOODLE_INTERNAL') || die(); /** * Social breadth indicator - forum. * * @package mod_forum * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class social_breadth extends activity_base { /** * Returns the name. * * If there is a corresponding '_help' string this will be shown as well. * * @return \lang_string */ public static function get_name(): \lang_string { return new \lang_string('indicator:socialbreadth', 'mod_forum'); } public function get_indicator_type() { return self::INDICATOR_SOCIAL; } public function get_social_breadth_level(\cm_info $cm) { return self::SOCIAL_LEVEL_2; } } PK-B\fN settracking.phpnu[. /** * Set tracking option for the forum. * * @package mod_forum * @copyright 2005 mchurch * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once("../../config.php"); require_once("lib.php"); $id = required_param('id',PARAM_INT); // The forum to subscribe or unsubscribe to $returnpage = optional_param('returnpage', 'index.php', PARAM_FILE); // Page to return to. require_sesskey(); if (! $forum = $DB->get_record("forum", array("id" => $id))) { throw new \moodle_exception('invalidforumid', 'forum'); } if (! $course = $DB->get_record("course", array("id" => $forum->course))) { throw new \moodle_exception('invalidcoursemodule'); } if (! $cm = get_coursemodule_from_instance("forum", $forum->id, $course->id)) { throw new \moodle_exception('invalidcoursemodule'); } require_login($course, false, $cm); $returnpageurl = new moodle_url('/mod/forum/' . $returnpage, array('id' => $course->id, 'f' => $forum->id)); $returnto = forum_go_back_to($returnpageurl); if (!forum_tp_can_track_forums($forum)) { redirect($returnto); } $info = new stdClass(); $info->name = fullname($USER); $info->forum = format_string($forum->name); $eventparams = array( 'context' => context_module::instance($cm->id), 'relateduserid' => $USER->id, 'other' => array('forumid' => $forum->id), ); if (forum_tp_is_tracked($forum) ) { if (forum_tp_stop_tracking($forum->id)) { $event = \mod_forum\event\readtracking_disabled::create($eventparams); $event->trigger(); redirect($returnto, get_string("nownottracking", "forum", $info), 1); } else { throw new \moodle_exception('cannottrack', '', get_local_referer(false)); } } else { // subscribe if (forum_tp_start_tracking($forum->id)) { $event = \mod_forum\event\readtracking_enabled::create($eventparams); $event->trigger(); redirect($returnto, get_string("nowtracking", "forum", $info), 1); } else { throw new \moodle_exception('cannottrack', '', get_local_referer(false)); } }PK-B\f0templates/form-user-selector-suggestion.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/form-user-selector-suggestion Moodle template for the list of valid options in an autocomplate form element. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * fullname string Users full name * email string user email field Example context (json): { "fullname": "Admin User", "extrafields": [ { "name": "email", "value": "admin@example.com" }, { "name": "phone1", "value": "0123456789" } ] } }} {{fullname}} {{#extrafields}} {{value}} {{/extrafields}} PK-B\yl#templates/discussion_times.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/discussion_times Template to display the discussion times. Note that this template is used only as a partial from within discussion_list, which requires the language strings be passed through the cleanstr helper Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * start int - The discussion start timestamp * end int - The discussion end timestamp * visible boolean - Whether the discussion is visible to students or not. Example context (json): { "start": 1, "end": 1, "visible": true } }}
    {{#start}}
  • {{#cleanstr}} displaystartdate, mod_forum, {{#userdate}}{{.}}, {{#str}}strftimerecentfull {{/str}}{{/userdate}} {{/cleanstr}}
  • {{/start}} {{#end}}
  • {{#cleanstr}} displayenddate, mod_forum, {{#userdate}}{{.}}, {{#str}}strftimerecentfull {{/str}}{{/userdate}} {{/cleanstr}}
  • {{/end}}
  • {{#visible}} {{#cleanstr}} timedvisible, mod_forum {{/cleanstr}} {{/visible}} {{^visible}} {{#cleanstr}} timedhidden, mod_forum {{/cleanstr}} {{/visible}}
PK-B\vLЂ1templates/frontpage_news_discussion_list.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/frontpage_news_discussion_list Template which defines a forum post for sending in a single-post HTML email. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }} {{< mod_forum/news_discussion_list }} {{$discussion_create_text}} {{#str}}addanewtopic, forum{{/str}} {{/discussion_create_text}} {{/ mod_forum/news_discussion_list }} PK-B\g JJ.templates/discussion_favourite_toggle.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/discussion_favourite_toggle Template to display the discussion subscription toggle. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * TODO Example context (json): { } }} {{#capabilities.favourite}} {{$favouritecontent}} {{#userstate.favourited}} {{#str}}removefromfavourites, mod_forum{{/str}} {{/userstate.favourited}} {{^userstate.favourited}} {{#str}}addtofavourites, mod_forum{{/str}} {{/userstate.favourited}} {{/favouritecontent}} {{/capabilities.favourite}}PK-B\-templates/discussion_settings_drawer.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/discussion_settings_drawer This template will render the discussion settings drawer. Classes required for JS: * none Data attributes required for JS: * All data attributes are required Context variables required for this template: * userid The logged in user id * urls The URLs for the popover Example context (json): {} }} {{ {{> mod_forum/settings_header}}
{{> mod_forum/discussion_settings_body_content}}
{{/drawercontent}} {{/core/drawer}} PK-B\ &templates/grades/grade_button.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/grades/grade_button Template which defines a forum post for sending in a single-post HTML email. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }} {{#js}} require(['mod_forum/grades/grader'], function(Grader) { Grader.registerLaunchListeners(); }); {{/js}} PK-B\o+templates/grades/view_grade_button.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/grades/view_grade_button Template which defines a forum post for sending in a single-post HTML email. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }} {{#js}} require(['mod_forum/grades/grader'], function(Grader) { Grader.registerLaunchListeners(); }); {{/js}} PK-B\v{'51templates/grades/grader/discussion/posts.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/grades/grader/discussion/posts Template to render a list of posts for a discussion. Classes required for JS: * none Data attributes required for JS: * data-region="posts" Example context (json): { } }}
{{#discussions}}

{{#str}} discussionstartedby, mod_forum, {{authorfullname}} {{/str}}

{{#userdate}} {{timecreated}}, {{#str}} strftimedate, core_langconfig {{/str}} {{/userdate}}

{{name}}

{{#posts}}
{{#parent}}
{{#experimentaldisplaymode}}{{> mod_forum/forum_discussion_nested_v2_first_post }}{{/experimentaldisplaymode}} {{^experimentaldisplaymode}}{{> mod_forum/forum_discussion_nested_post }}{{/experimentaldisplaymode}}
{{/parent}} {{#post}}
{{#experimentaldisplaymode}}{{> mod_forum/forum_discussion_nested_v2_first_post }}{{/experimentaldisplaymode}} {{^experimentaldisplaymode}}{{> mod_forum/forum_discussion_nested_post }}{{/experimentaldisplaymode}}
{{/post}}
{{/posts}}

{{/discussions}} {{^discussions}}
{{#pix}} no-posts, mod_forum {{/pix}}

{{#str}} noposts, mod_forum {{/str}}

{{/discussions}}
{{#js}} require(['mod_forum/grades/expandconversation'], function(Conversation) { Conversation.registerEventListeners(document.querySelector('#post-region-{{uniqid}}')); }); {{/js}} PK-B\DkLL6templates/grades/grader/discussion/post_modal.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/grades/grader/discussion/post_modal Template to render a single post from a discussion. Classes required for JS: * none Data attributes required for JS: * data-region="posts-modal" Example context (json): { } }}
{{#posts}} {{#experimentaldisplaymode}}{{> mod_forum/forum_discussion_nested_v2_first_post }}{{/experimentaldisplaymode}} {{^experimentaldisplaymode}}{{> mod_forum/forum_discussion_nested_post }}{{/experimentaldisplaymode}} {{/posts}}
PK-B\7templates/forum_post_emaildigestfull_htmlemail.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_post_emaildigestfull_htmlemail Template which defines a forum post for sending in a single-post HTML email. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * courselink * coursename * forumindexlink * forumviewlink * forumname * discussionlink * discussionname * showdiscussionname * firstpost * subject * authorlink * authorpicture * authorfullname * postdate * grouppicture * attachments * message * parentpostlink * canreply * replylink * permalink * unsubscribeforumlink * unsubscribediscussionlink Example context (json): { "courselink": "https://example.com/course/view.php?id=2", "coursename": "Example course", "forumindexlink": "https://example.com/mod/forum/index.php?id=2", "forumviewlink": "https://example.com/mod/forum/view.php?f=2", "forumname": "Lorem ipsum dolor", "discussionlink": "https://example.com/mod/forum/discuss.php?d=70", "discussionname": "Is Lorem ipsum Latin?", "showdiscussionname": 1, "firstpost": 1, "subject": "Is Lorem ipsum Latin?", "authorlink": "https://example.com/user/view.php?id=2&course=2", "authorpicture": "\"Picture", "authorfullname": "Lucius Caecilius lucundus", "postdate": "Sunday, 13 September 2015, 2:22 pm", "grouppicture": "", "attachments": "", "message": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et auctor libero. Quisque porta egestas neque, et bibendum libero dignissim at. Nulla facilisi. Morbi eget accumsan felis. Nunc et vulputate odio, vel venenatis nisl. Nunc maximus ipsum sed tincidunt mollis. Integer nunc erat, luctus sit amet arcu tincidunt, volutpat dignissim mi. Sed ut magna quam. Mauris accumsan porta turpis sed aliquam. Etiam at justo tristique, imperdiet augue quis, consectetur sapien. Ut nec erat malesuada sem suscipit lobortis. Vivamus posuere nibh eu ipsum porta fringilla. Sed vitae dapibus ipsum, ac condimentum enim. Sed dignissim ante at elit mollis, ac tempor lacus iaculis. Etiam nec lectus vitae nibh vulputate volutpat. Nulla quis tellus aliquam, commodo nisi et, dictum est.


", "parentpostlink": "", "canreply": 1, "replylink": "https://example.com/mod/forum/post.php?reply=2", "permalink": "https://example.com/mod/forum/discuss.php?d=2#2", "unsubscribeforumlink": "https://example.com/mod/forum/subscribe.php?id=2", "unsubscribediscussionlink": "https://example.com/mod/discussion/subscribe.php?id=2&d=2" } }} {{> mod_forum/forum_post_email_htmlemail_body }} PK-B\2:dd"templates/discussion_list.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/discussion_list Template to display the list of forum discussions. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * TODO Example context (json): { } }}
{{#notifications}} {{> core/notification}} {{/notifications}} {{{groupchangemenu}}}
{{#forum.capabilities.create}} {{#enablediscussioncreation}} {{$discussion_create_text}} {{#str}}addanewdiscussion, forum{{/str}} {{/discussion_create_text}} {{/enablediscussioncreation}} {{/forum.capabilities.create}} {{^forum.capabilities.create}} {{#forum.capabilities.selfenrol}} {{#enablediscussioncreation}} {{$discussion_create_text}} {{#str}}addanewdiscussion, forum{{/str}} {{/discussion_create_text}} {{/enablediscussioncreation}} {{/forum.capabilities.selfenrol}} {{/forum.capabilities.create}} {{#forum.capabilities.grade}} {{#forum.state.gradingenabled}} {{> mod_forum/grades/grade_button }} {{/forum.state.gradingenabled}} {{/forum.capabilities.grade}} {{^forum.capabilities.grade}} {{#forum.state.gradingenabled}} {{> mod_forum/grades/view_grade_button }} {{/forum.state.gradingenabled}} {{/forum.capabilities.grade}}
{{#forum.capabilities.create}}
{{{newdiscussionhtml}}}
{{/forum.capabilities.create}} {{#state.hasdiscussions}} {{$discussion_top_pagination}} {{{ pagination }}} {{/discussion_top_pagination}} {{$discussion_list_output}}
{{$discussion_list_header}} {{#forum.state.groupmode}} {{/forum.state.groupmode}} {{#forum.capabilities.viewdiscussions}} {{/forum.capabilities.viewdiscussions}} {{#forum.capabilities.subscribe}} {{/forum.capabilities.subscribe}} {{/discussion_list_header}} {{$discussion_list_body}} {{#summaries}} {{! The discussion class is only required for behat tests to identify the row. }} {{#forum.state.groupmode}} {{/forum.state.groupmode}} {{#forum.capabilities.viewdiscussions}} {{/forum.capabilities.viewdiscussions}} {{#forum.capabilities.subscribe}} {{/forum.capabilities.subscribe}} {{/summaries}} {{/discussion_list_body}}
{{#str}} showingcountoftotaldiscussions, mod_forum, {"count": "{{visiblediscussioncount}}", "total":"{{totaldiscussioncount}}"} {{/str}}
{{#str}}status{{/str}} {{#state.sortorder.isdiscussiondesc}} {{#str}}discussion, mod_forum{{/str}} {{#pix}}t/downlong, core, {{#str}}desc, core{{/str}}{{/pix}} {{/state.sortorder.isdiscussiondesc}} {{#state.sortorder.isdiscussionasc}} {{#str}}discussion, mod_forum{{/str}} {{#pix}}t/uplong, core, {{#str}}asc, core{{/str}}{{/pix}} {{/state.sortorder.isdiscussionasc}} {{^state.sortorder.isdiscussiondesc}} {{^state.sortorder.isdiscussionasc}} {{#str}}discussion, mod_forum{{/str}} {{/state.sortorder.isdiscussionasc}} {{/state.sortorder.isdiscussiondesc}} {{#state.sortorder.isgroupdesc}} {{#str}}group{{/str}} {{#pix}}t/downlong, core, {{#str}}desc, core{{/str}}{{/pix}} {{/state.sortorder.isgroupdesc}} {{#state.sortorder.isgroupasc}} {{#str}}group{{/str}} {{#pix}}t/uplong, core, {{#str}}asc, core{{/str}}{{/pix}} {{/state.sortorder.isgroupasc}} {{^state.sortorder.isgroupdesc}} {{^state.sortorder.isgroupasc}} {{#str}}group{{/str}} {{/state.sortorder.isgroupasc}} {{/state.sortorder.isgroupdesc}} {{#state.sortorder.isstarterdesc}} {{#str}}startedby, mod_forum{{/str}} {{#pix}}t/downlong, core, {{#str}}desc, core{{/str}}{{/pix}} {{/state.sortorder.isstarterdesc}} {{#state.sortorder.isstarterasc}} {{#str}}startedby, mod_forum{{/str}} {{#pix}}t/uplong, core, {{#str}}asc, core{{/str}}{{/pix}} {{/state.sortorder.isstarterasc}} {{^state.sortorder.isstarterdesc}} {{^state.sortorder.isstarterasc}} {{#str}}startedby, mod_forum{{/str}} {{/state.sortorder.isstarterasc}} {{/state.sortorder.isstarterdesc}} {{#state.sortorder.islastpostdesc}} {{#str}}lastpost, mod_forum{{/str}} {{#pix}}t/downlong, core, {{#str}}desc, core{{/str}}{{/pix}} {{/state.sortorder.islastpostdesc}} {{#state.sortorder.islastpostasc}} {{#str}}lastpost, mod_forum{{/str}} {{#pix}}t/uplong, core, {{#str}}asc, core{{/str}}{{/pix}} {{/state.sortorder.islastpostasc}} {{^state.sortorder.islastpostdesc}} {{^state.sortorder.islastpostasc}} {{#str}}lastpost, mod_forum{{/str}} {{/state.sortorder.islastpostasc}} {{/state.sortorder.islastpostdesc}} {{#state.sortorder.isrepliesdesc}} {{#str}}replies, mod_forum{{/str}} {{#pix}}t/downlong, core, {{#str}}desc, core{{/str}}{{/pix}} {{/state.sortorder.isrepliesdesc}} {{#state.sortorder.isrepliesasc}} {{#str}}replies, mod_forum{{/str}} {{#pix}}t/uplong, core, {{#str}}asc, core{{/str}}{{/pix}} {{/state.sortorder.isrepliesasc}} {{^state.sortorder.isrepliesdesc}} {{^state.sortorder.isrepliesasc}} {{#str}}replies, mod_forum{{/str}} {{/state.sortorder.isrepliesasc}} {{/state.sortorder.isrepliesdesc}} {{#forum.userstate.tracked}} {{#pix}}t/markasread, core, {{#str}}markallread, mod_forum{{/str}}{{/pix}} {{/forum.userstate.tracked}} {{#str}} notsubscribed, mod_forum {{/str}} {{#str}}actions{{/str}}
{{#discussion}} {{> mod_forum/forum_discussion_favourite_toggle}} {{/discussion}}
{{#discussion.pinned}} {{#pix}}i/pinned, mod_forum, {{#str}}discussionpinned, mod_forum{{/str}}{{/pix}} {{/discussion.pinned}} {{#shortentext}}100, {{{discussion.name}}}{{/shortentext}}
{{#str}}locked, forum{{/str}} {{#discussion.timed.istimed}} {{/discussion.timed.istimed}}
{{#discussion.group}} {{#str}} pictureof, core, {{name}} {{/str}} {{#urls.userlist}} {{#shortentext}}30, {{name}}{{/shortentext}} {{/urls.userlist}} {{^urls.userlist}} {{name}} {{/urls.userlist}} {{/discussion.group}} {{#firstpostauthor}}
{{#str}}pictureof, moodle, {{fullname}}{{/str}}
{{fullname}}
{{< core/time_element }} {{$elementid}}created-{{discussion.id}}{{/elementid}} {{$timestampval}}{{discussion.times.created}}{{/timestampval}} {{$userdateformatval}}{{#str}}strftimedatemonthabbr, langconfig{{/str}}{{/userdateformatval}} {{/core/time_element}}
{{/firstpostauthor}}
{{! TODO Check q&a, eachuser }} {{#latestpostauthor}} {{/latestpostauthor}} {{replies}} {{#forum.userstate.tracked}} {{#unread}} {{! TODO Rewrite as AJAX}} {{unread}} {{/unread}} {{/forum.userstate.tracked}} {{#discussion}}
{{/discussion}}
{{#discussion}}
{{#hasanyactions}}
{{> mod_forum/forum_action_menu}}
{{/hasanyactions}}
{{/discussion}}
{{/discussion_list_output}} {{$discussion_bottom_pagination}} {{{ pagination }}} {{/discussion_bottom_pagination}} {{#can_create_discussion}} {{/can_create_discussion}} {{/state.hasdiscussions}} {{^state.hasdiscussions}}
{{$no_discussions_text}} {{#str}}nodiscussions, forum{{/str}} {{/no_discussions_text}}
{{/state.hasdiscussions}}
{{#js}} require(['jquery', 'mod_forum/discussion_list'], function($, View) { var root = $('#discussion-list-{{uniqid}}'); View.init(root); }); {{/js}} PK-B\al!templates/setting_switch.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/setting_switch This template will render a toggle switch, which can be used for the forum settings drawer. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * none Example context (json): {} }}
PK-B\F||'templates/blog_discussion_list.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/blog_discussion_list Template which defines a forum post for sending in a single-post HTML email. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }} {{< mod_forum/social_discussion_list }} {{$discussion_create_text}} {{#str}}addanewtopic, forum{{/str}} {{/discussion_create_text}} {{/ mod_forum/social_discussion_list }} PK-B\;`3templates/local/grades/local/grader/status.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/local/grades/local/grader/status Classes required for JS: * none Data attributes required for JS: * Context variables required for this template: * Example context (json): { "status": "Graded", "index": 1, "total": 10 } }}

{{#status}} {{#str}}graded, forum{{/str}} {{/status}} {{^status}} {{#str}}notgraded, forum{{/str}} {{/status}}

{{#str}} indexoutoftotal, mod_forum, {"index": "{{index}}", "total":"{{total}}"}{{/str}}
PK-B\w| | 8templates/local/grades/local/grader/user_picker.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/local/grades/local/grader/user_picker Classes required for JS: * none Data attributes required for JS: * data-region="user_picker/user" * data-action="change-user" * data-direction="-1" * data-direction="1" * data-action="search-user" Context variables required for this template: * none Example context (json): { } }}
PK-B\*"*"Gtemplates/local/grades/local/grader/module_content_placeholder.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/local/grades/local/grader/module_content_placeholder Classes required for JS: * none Data attributes required for JS: * Context variables required for this template: * Example context (json): {} }}




PK-B\}4templates/local/grades/local/grader/content.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/local/grades/local/grader/content Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * none Example context (json): { } }}
{{> mod_forum/local/grades/local/grader/module_content_placeholder }}
PK-B\fDtemplates/local/grades/local/grader/user_picker_placeholder.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/local/grades/local/grader/user_picker_placeholder Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * none Example context (json): { } }}
PK-B\ Dtemplates/local/grades/local/grader/user_picker/user_search.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/local/grades/local/grader/user_picker/user_search Classes required for JS: * none Data attributes required for JS: * data-action="select-user" Context variables required for this template: * expandedUsers: Array of users to show, limited to 10 * profileimage: Profile image for the user * fullname: User's full name * id: User's ID * hasCollapsed: T/F if there are more users to show * collapsedUsers: Array of users after index 9 Example context (json): { "expandedUsers": [ { "id": 4, "fullname": "Phillip J. Fry", "profileimage": "/pluginfile.php/4/user/icon/boost/f1?rev=58" }, { "id": 5, "fullname": "Turanga Leela", "profileimage": "/pluginfile.php/5/user/icon/boost/f1?rev=58" } ], "hasCollapsed": true, "collapsedUsers": [ { "id": 14, "fullname": "Bender B. Rodriguez", "profileimage": "/pluginfile.php/14/user/icon/boost/f1?rev=58" } ] } }}
{{#users}} {{/users}} {{^users}}
{{#pix}} no-search-results, mod_forum {{/pix}}
{{#str}}nousersmatch, mod_forum{{/str}}
{{/users}}
PK-B\_)OO=templates/local/grades/local/grader/user_picker/user.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/local/grades/local/grader/user_picker/user Classes required for JS: * none Data attributes required for JS: * data-userid="42" * data-region="index" Context variables required for this template: * id: The ID of the user * fullname: The user's full name * displayIndex: The index of the user in the userpicker array + 1 * total: The length of the user array in the userpicker Example context (json): { "id": 4, "fullname": "Phillip J. Fry", "displayIndex": 3, "total": 7 } }} {{#profileimageurl}} {{/profileimageurl}}
{{fullname}}
PK-B\/7templates/local/grades/local/grader/navigation.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/local/grades/local/grader/navigation Classes required for JS: * none Data attributes required for JS: * data-action="togglefullscreen" * data-action="closegrader" * data-action="savegrade" * data-action="expandgrader" Context variables required for this template: * moduleName: The name of the module this grading interface is for i.e. "Chef the Forum" Example context (json): { "moduleName": "Chef the Forum" } }} PK-B\499>templates/local/grades/local/grader/grade_placeholder.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/local/grades/local/grader/grade_placeholder Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * none Example context (json): { } }}
PK-B\4nRR?templates/local/grades/local/grader/status_placeholder.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/local/grades/local/grader/status_placeholder Classes required for JS: * none Data attributes required for JS: * Context variables required for this template: * Example context (json): {} }}
PK-B\:C?templates/local/grades/local/grader/gradingpanel/error.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/local/grades/local/grader/gradingpanel/error Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * error.message: The returned message on error Example context (json): { "error": { "message": "The item (Safe) in (magic school bus trips) does not exist" } } }}
{{{error.message}}}
PK-B\0334templates/local/grades/local/grader/grading.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/local/grades/local/grader/grading Classes required for JS: * none Data attributes required for JS: * data-region="user_picker" * data-region="grade" * data-region="grade-errors" Context variables required for this template: * none Example context (json): { } }} {{
{{> mod_forum/local/grades/local/grader/status_placeholder }}
{{> mod_forum/local/grades/local/grader/user_picker_placeholder }}

{{#pix}} i/grading, core {{/pix}}{{#str}} gradingmodulename, core_grades, {{moduleName}} {{/str}}

{{> mod_forum/local/grades/local/grader/grade_placeholder }}

{{#pix}} i/gradingnotifications, core {{/pix}}{{#str}}notifications, core{{/str}}

{{#helpicon}} {{>core/help_icon}} {{/helpicon}}

{{/drawercontent}} {{/core/drawer}} PK-B\iǥN{ { *templates/local/grades/view_grade.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/local/grades/view_grade Classes required for JS: * none Data attributes required for JS: * data-region="grade-template" Context variables required for this template: * none Example context (json): { "templatename": "core_grades/grades/grader/gradingpanel/point", "grade": { "usergrade": 55, "maxgrade": 100, "timemodified": 1573543598 }, "hasgrade": true } }}
{{#grade}}
{{#str}}gradenoun{{/str}}:

{{usergrade}}

{{#str}}gradedby, forum{{/str}}:

{{#gradedby}} {{gradedby}} {{/gradedby}} {{^gradedby}} - {{/gradedby}}

{{#str}}date{{/str}}:

{{#userdate}}{{timemodified}}, {{#str}} strftimedate, langconfig {{/str}}{{/userdate}}

{{/grade}}
{{#str}}gradingstatus, forum{{/str}}

{{#hasgrade}} {{#str}}graded, forum{{/str}} {{/hasgrade}} {{^hasgrade}} {{#str}}notgraded, forum{{/str}} {{/hasgrade}}

PK-B\0  &templates/local/grades/grader.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/local/grades/grader Classes required for JS: * TODO Data attributes required for JS: * data-first-userid="350" * data-cmid="1337" Context variables required for this template: * userid: ID of the first user to jump to * cmid: ID of the course module we want to grade Example context (json): { "userid": 350, "cmid": 1337 } }}
{{> mod_forum/local/grades/local/grader/navigation }}
{{> mod_forum/local/grades/local/grader/content }} {{#drawer}}{{> mod_forum/local/grades/local/grader/grading }}{{/drawer}}
{{#js}} require(['jquery', 'core/drawer'], function($, Drawer) { var root = $('#grader-container-{{uniqid}}'); var drawer = root.find('[data-region="right-hand-drawer"]'); var expandButtons = root.find('[data-action="expand-grading-drawer"]'); var collapseButtons = root.find('[data-action="collapse-grading-drawer"]'); expandButtons.click(function() { collapseButtons.removeClass('active'); expandButtons.addClass('active'); collapseButtons.attr('aria-expanded', true); expandButtons.attr('aria-expanded', true); Drawer.show(drawer); }); collapseButtons.click(function() { expandButtons.removeClass('active'); collapseButtons.addClass('active'); collapseButtons.attr('aria-expanded', false); expandButtons.attr('aria-expanded', false); Drawer.hide(drawer); }); }); {{/js}} PK-B\vB(templates/qanda_discussion_list.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/qanda_discussion_list Template which defines a forum post for sending in a single-post HTML email. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }} {{< mod_forum/discussion_list }} {{$discussion_create_text}} {{#str}}addanewquestion, forum{{/str}} {{/discussion_create_text}} {{$no_discussions_text}} ({{#str}}noquestions, forum{{/str}}) {{/no_discussions_text}} {{/ mod_forum/discussion_list }} PK-B\L͐$templates/forum_action_menu.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_action_menu This template renders action menu for a forum discussion. Context variables required for this template: * capabilities Object - Uses the following attributes: * manage boolean - Whether to render the lock action. * favourite boolean - Whether to render the star/unstar action. * pin boolean - Whether to render the star/unstar action. * subscribe boolean - Whether to render the subscribe action. * id int - The discussion ID. * forumid int - The forum ID. * istimelocked boolean - Whether this forum is time locked. * settings Object - Uses the following attributes: * excludetext boolean - Whether to show an icon only. * togglemoreicon - Whether to show a toggle-more icon or not. Example context (json): { "capabilities": { "manage": true, "favourite": true, "pin": true, "subscribe": true }, "id": 1, "forumid": 1, "istimelocked": false, "settingsselector": true, "settings": { "excludetext": false, "togglemoreicon": false } } }} PK-B\]zhh7templates/forum_post_emaildigestfull_textemail.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_post_emaildigestfull_textemail Template which defines a forum post for sending in a single-post text email. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * discussionlink * subject * permalink * authorfullname * postdate * message * attachments * forumindexlink }} {{ discussionlink }} {{{ subject }}} ({{{ permalink }}}) {{# str }} bynameondate, forum, { "name": {{# quote }}{{{ authorfullname }}}{{/ quote }}, "date": {{# quote }}{{ postdate }}{{/ quote }} } {{/ str }} --------------------------------------------------------------------- {{{ message }}} {{# attachments }} {{{ attachments }}} {{/ attachments }} --------------------------------------------------------------------- {{# str }} digestmailpostlink, forum, {{{ forumindexlink }}} {{/ str }} PK-B\@ "templates/forum_actionbar.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_actionbar Simple search input. Context variables required for this template: * see mod/forum/classes/output/forum_actionbar.php Example context (json): { "action": "https://moodle.local/mod/forum/search.php", "inputname": "search", "searchstring": "Search settings", "query": "themedesigner", "hiddenfields": [{ "name": "id", "value": "11" }], "helpicon": { "heading": "Search", "text": "

For basic searching .

", "alt": "Help with search", "icon": [{ "attributes": [{ "name": "alt", "value": "Help with Search" }, { "name": "src", "value": "http://localhost/theme/image.php?theme=boost&component=core&image=help" }] }] }, "newdiscussionbtn": "Add discussion topic", "subscribetoforum": "http://localhost/moodle/mod/forum/subscribe.php?id=7&sesskey=abcd&returnurl=http//localhost/moodle/mod/forum/view.php?id=4", "unsubscribefromforum": "http://localhost/moodle/mod/forum/subscribe.php?id=7&sesskey=abcd&returnurl=http//localhost/moodle/mod/forum/view.php?id=4" } }}
PK-B\jh)templates/forum_discussion_posts.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_discussion_posts Template to render a list of posts for a discussion. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }} {{#posts}} {{> mod_forum/forum_discussion_post }} {{/posts}} PK-B\T8templates/forum_post_emaildigestbasic_htmlemail.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_post_emaildigestbasic_htmlemail Template which defines a forum post for sending in a single-post HTML email. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * permalink * subject * authorlink * authorfullname * postdate Example context (json): { "permalink": "https://example.com/mod/forum/discuss.php?d=2#2", "subject": "Is Lorem ipsum Latin?", "authorlink": "https://example.com/user/view.php?id=2&course=2", "authorfullname": "Lucius Caecilius lucundus", "postdate": "Sunday, 13 September 2015, 2:22 pm" } }}
{{{ subject }}} {{# str }} bynameondate, forum, { "name": {{# quote }}{{ authorfullname }}{{/ quote }}, "date": {{# quote }}{{ postdate }}{{/ quote }} } {{/ str }}
PK-B\č"templates/inpage_reply_v2.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/inpage_reply_v2 In page reply HTML for the nested v2 discussion display mode. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { "postid": 0, "reply_url": "", "sesskey": "", "parentsubject": "" } }} PK-B\F  1templates/discussion_subscription_toggle.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/discussion_subscription_toggle Template to display the discussion subscription toggle. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * capabilities Object - Uses the "subscribe" attribute to determine whether to render the subscribe action. * urls Object - Uses the "subscribe" attribute for the subscription URl. * id int - The discussion ID. * forumid int - The forum ID. * userstate Object - Uses the "subscribed" attribute to determine the action that will be performed upon toggling. Example context (json): { "capabilities": { "subscribe": true }, "urls": { "subscribe": "#" }, "id": 1, "forumid": 1, "userstate": { "subscribed": true } } }} {{#capabilities.subscribe}} {{#userstate.subscribed}} {{#str}}unsubscribediscussion, forum{{/str}} {{/userstate.subscribed}} {{^userstate.subscribed}} {{#str}}subscribediscussion, forum{{/str}} {{/userstate.subscribed}} {{/capabilities.subscribe}} PK-B\4LZ4templates/forum_discussion_favourite_toggle.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_discussion_favourite_toggle Template which defines a forum post for sending in a single-post HTML email. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }} {{< mod_forum/discussion_favourite_toggle }} {{$classes}}btn btn-link{{/classes}} {{$role}}button{{/role}} {{$favouritecontent}} {{#userstate.favourited}} {{#pix}}t/star, mod_forum, {{#str}}removefromfavourites, mod_forum{{/str}}{{/pix}} {{/userstate.favourited}} {{^userstate.favourited}} {{#pix}}t/emptystar, core, {{#str}}addtofavourites, mod_forum{{/str}}{{/pix}} {{/userstate.favourited}} {{/favouritecontent}} {{/ mod_forum/discussion_favourite_toggle }} PK-B\J  8templates/forum_discussion_nested_v2_post_reply.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_discussion_nested_v2_post_reply Template to render a single post from a discussion. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }} {{< mod_forum/forum_discussion_nested_v2_first_post }} {{$subject}}

{{{subject}}}

{{/subject}} {{$footer}} {{^isdeleted}} {{^readonly}} {{#capabilities.reply}}
{{/capabilities.reply}} {{/readonly}} {{/isdeleted}} {{/footer}} {{$replies}}
{{#hasreplies}} {{#replies}} {{> mod_forum/forum_discussion_nested_v2_post_reply }} {{/replies}} {{/hasreplies}}
{{/replies}} {{/ mod_forum/forum_discussion_nested_v2_first_post }} PK-B\"templates/settings_header.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/settings_header This template will render the header for the discussion/forum settings drawer. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * none Example context (json): {} }}

{{#str}} settings, core {{/str}}

PK-B\'ʷ -templates/forum_post_email_textemail.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_post_email_textemail Template which defines a forum post for sending in a single-post text email. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * coursename * forumname * showdiscussionname * discussionname * permalink * subject * authorfullname * postdate * message * attachments * canreply * replylink * unsubscribeforumlink * unsubscribediscussionlink * forumindexlink * isprivatereply }} {{{ coursename }}} -> {{# str }} forums, forum {{/ str }} -> {{{ forumname }}}{{# showdiscussionname }} -> {{{ discussionname }}} {{/ showdiscussionname }} {{ permalink }} {{{ subject }}} {{# str }} bynameondate, forum, { "name": {{# quote }}{{{ authorfullname }}}{{/ quote }}, "date": {{# quote}}{{ postdate }}{{/ quote }} } {{/ str }} {{# isprivatereply }} {{# str }} postisprivatereply, forum {{/ str }} {{/ isprivatereply }} --------------------------------------------------------------------- {{{ message }}} {{{ attachments }}} --------------------------------------------------------------------- {{# str }} postmailinfo, forum, {{{ coursename }}} {{/ str }} {{# canreply }} {{^ isprivatereply }} {{# str }} postmailreply, forum, {{{ replylink }}} {{/ str }} {{/ isprivatereply }} {{/ canreply }} {{# unsubscribeforumlink }} {{# str }} unsubscribelink, forum, {{{ unsubscribeforumlink }}} {{/ str }} {{/ unsubscribeforumlink }} {{# unsubscribediscussionlink }} {{# str }} unsubscribediscussionlink, forum, {{{ unsubscribediscussionlink }}} {{/ str }} {{/ unsubscribediscussionlink }} {{# str }} digestmailpostlink, forum, {{{ forumindexlink }}} {{/ str }} PK-B\1mm$templates/quick_search_form.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/quick_search_form This template renders the search form within forums. Example context (json): { "action": "https://domain.example/mod/forum/search.php", "helpicon": "", "query": "find this post", "inputname": "search" } }} {{#helpicon}} {{>core/help_icon}} {{/helpicon}} {{>core/search_input}} PK-B\(888templates/forum_post_emaildigestbasic_textemail.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_post_emaildigestbasic_textemail Template which defines a forum post for sending in a single-post text email. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * discussionlink * subject * authorfullname * postdate }} {{ discussionlink }} {{{ subject }}} {{# str }} bynameondate, forum, { "name": {{# quote }}{{{ authorfullname }}}{{/ quote }}, "date": {{# quote }}{{ postdate }}{{/ quote }} } {{/ str }} --------------------------------------------------------------------- PK-B\/<;;(templates/discussion_pin_toggle.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/discussion_pin_toggle Template to display the discussion subscription toggle. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * TODO Example context (json): { } }} {{#capabilities.pin}} {{#pinned}} {{#str}}unpindiscussion, mod_forum{{/str}} {{/pinned}} {{^pinned}} {{#str}}pindiscussion, mod_forum{{/str}} {{/pinned}} {{/capabilities.pin}}PK-B\uN88"templates/big_search_form.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/big_search_form Big search form. Example context (json): { "actionurl": "https://example.com/mod/forum/search.php", "courseid": "2", "words": "apples", "phrase": "Lorem ipsum dolor", "notwords": "Not these words", "showfullwords": [ { "fullwords": "Exactly" } ], "datefromchecked": 1, "datetochecked": "", "forumoptions": [ { "name": "Forum One", "value": "23" }, { "name": "Forum Two", "value": "34" } ], "subject": "Help me please", "user": "Helpy McUser" } }}
{{#showfullwords}}
{{/showfullwords}}
{{#str}}searchdatefrom, forum{{/str}}
{{{datefromfields}}}
{{#str}}searchdateto, forum{{/str}}
{{{datetofields}}}
{{^guestuser}}
{{/guestuser}} {{#tagsenabled}}
{{/tagsenabled}}
{{#js}} require(['jquery'], function($) { var toggleDateFields = function(prefix, disabled) { $('#searchform select[name^=' + prefix + ']').prop('disabled', disabled); $('#searchform input[name^=h' + prefix + ']').val(disabled ? 1 : 0); }; toggleDateFields('from', !$("#searchform input[name='timefromrestrict']").prop('checked')); $("#searchform input[name='timefromrestrict']").click(function() { toggleDateFields('from', !this.checked); }); toggleDateFields('to', !$("#searchform input[name='timetorestrict']").prop('checked')); $("#searchform input[name='timetorestrict']").click(function() { toggleDateFields('to', !this.checked); }); }); {{/js}} PK-B\templates/mark_as_read.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/mark_as_read Template to mark as read menu item. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * urls Object - Uses the 'markasread' attribute for the URL. Example context (json): { "urls": { "markasread": "#" } } }} {{#str}}markasread, mod_forum{{/str}} PK-B\8templates/forum_post_subject_with_context_links.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_post_subject_with_context_links Template to render a breadcrumb-style subject. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { "coursename": "Test course", "forumname": "Test forum", "discussionname": "Test discussion", "subject": "Test post subject", "urls": { "viewcourse": "#", "viewforum": "#", "viewdiscussion": "#", "viewparent": "#" }, "showdiscussionname": true, "hasparent": true } }} {{#coursename}} {{#urls.viewcourse}} {{{coursename}}} -> {{/urls.viewcourse}} {{^urls.viewcourse}} {{coursename}} -> {{/urls.viewcourse}} {{/coursename}} {{#urls.viewforum}} {{{forumname}}} {{/urls.viewforum}} {{^urls.viewforum}} {{{forumname}}} {{/urls.viewforum}} {{#showdiscussionname}} {{#urls.viewdiscussion}} -> {{{discussionname}}} {{/urls.viewdiscussion}} {{^urls.viewdiscussion}} {{{discussionname}}} {{/urls.viewdiscussion}} {{#hasparent}} {{#urls.viewparent}} -> {{{subject}}} {{/urls.viewparent}} {{^urls.viewparent}} {{{subject}}} {{/urls.viewparent}} {{/hasparent}} {{/showdiscussionname}} PK-B\8It0templates/forum_discussion_nested_posts.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_discussion_nested_posts Template to render a list of posts in a nested format. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }} {{#posts}} {{> mod_forum/forum_discussion_nested_post }} {{/posts}}PK-B\3'uu1templates/forum_posts_with_context_links.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_posts_with_context_links Template to render a list of posts with breadcrumb style subject and a link in the footer to the full discussion. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }} {{#posts}} {{< mod_forum/forum_discussion_post }} {{$subject}} {{> mod_forum/forum_post_subject_with_context_links }} {{/subject}} {{$replies}}{{/replies}} {{$footer}} {{/footer}} {{/ mod_forum/forum_discussion_post }} {{/posts}} PK-B\ -templates/forum_discussion_nested_v2.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_discussion_nested_v2 Template for displaying a single forum discussion. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { "html": { "hasanyactions": true, "posts": "
", "modeselectorform": "
", "subscribe": null, "movediscussion": null, "pindiscussion": null, "neighbourlinks": "
", "exportdiscussion": null }, "throttlingwarningmsg": "You are approaching the posting threshold. You have posted 3 times in the last 1 week and the limit is 4 posts." } }}
{{#html}} {{> mod_forum/discussion_settings_drawer}} {{/html}} {{#notifications}} {{> core/notification}} {{/notifications}}
{{{html.modeselectorform}}}
{{{html.posts}}}
{{#js}} require(['jquery', 'mod_forum/discussion_nested_v2'], function($, Discussion) { var root = $('#discussion-container-{{uniqid}}'); Discussion.init(root, { {{#throttlingwarningmsg}} throttlingwarningmsg: {{#quote}}{{throttlingwarningmsg}}{{/quote}}, {{/throttlingwarningmsg}} {{#loggedinuser}} loggedinuser: { profileimageurl: '{{{profileimageurl}}}', fullname: '{{fullname}}', firstname: '{{firstname}}' } {{/loggedinuser}} }); }); {{/js}} PK-B\@2templates/forum_post_email_htmlemail_body.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_post_email_htmlemail_body Template which defines the body component of a forum post for sending in a single-post HTML email. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * courselink * coursename * forumindexlink * forumviewlink * forumname * discussionlink * discussionname * showdiscussionname * firstpost * subject * authorlink * authorpicture * authorfullname * postdate * grouppicture * attachments * message * parentpostlink * canreply * replylink * permalink * unsubscribeforumlink * unsubscribediscussionlink * isprivatereply Example context (json): { "courselink": "https://example.com/course/view.php?id=2", "coursename": "Example course", "forumindexlink": "https://example.com/mod/forum/index.php?id=2", "forumviewlink": "https://example.com/mod/forum/view.php?f=2", "forumname": "Lorem ipsum dolor", "discussionlink": "https://example.com/mod/forum/discuss.php?d=70", "discussionname": "Is Lorem ipsum Latin?", "showdiscussionname": 1, "firstpost": 1, "subject": "Is Lorem ipsum Latin?", "authorlink": "https://example.com/user/view.php?id=2&course=2", "authorpicture": "\"Picture", "authorfullname": "Lucius Caecilius lucundus", "postdate": "Sunday, 13 September 2015, 2:22 pm", "grouppicture": "", "attachments": "", "message": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et auctor libero. Quisque porta egestas neque, et bibendum libero dignissim at. Nulla facilisi. Morbi eget accumsan felis. Nunc et vulputate odio, vel venenatis nisl. Nunc maximus ipsum sed tincidunt mollis. Integer nunc erat, luctus sit amet arcu tincidunt, volutpat dignissim mi. Sed ut magna quam. Mauris accumsan porta turpis sed aliquam. Etiam at justo tristique, imperdiet augue quis, consectetur sapien. Ut nec erat malesuada sem suscipit lobortis. Vivamus posuere nibh eu ipsum porta fringilla. Sed vitae dapibus ipsum, ac condimentum enim. Sed dignissim ante at elit mollis, ac tempor lacus iaculis. Etiam nec lectus vitae nibh vulputate volutpat. Nulla quis tellus aliquam, commodo nisi et, dictum est.


", "parentpostlink": "", "canreply": 1, "replylink": "https://example.com/mod/forum/post.php?reply=2", "permalink": "https://example.com/mod/forum/discuss.php?d=2#2", "unsubscribeforumlink": "https://example.com/mod/forum/subscribe.php?id=2", "unsubscribediscussionlink": "https://example.com/mod/discussion/subscribe.php?id=2&d=2" } }}
{{{ authorpicture }}}
{{{ subject }}}
{{# str }} bynameondate, forum, { "name": {{# quote }}{{ authorfullname }}{{/ quote }}, "date": {{# quote }}{{ postdate }}{{/ quote }} } {{/ str }}
{{# isprivatereply }}
{{# str }} postisprivatereply, forum {{/ str }}
{{/ isprivatereply }}
{{# grouppicture }} {{{ grouppicture }}} {{/ grouppicture }} {{^ grouppicture }}   {{/ grouppicture }} {{# attachments }}
{{{ attachments }}}
{{/ attachments }} {{{ message }}}
{{^ firstpost }} {{# str }} parent, forum {{/ str }} {{# canreply }} {{^ isprivatereply }} | {{/ isprivatereply }} {{/ canreply }} {{/ firstpost }} {{# canreply }} {{^ isprivatereply }} {{# str }} reply, forum {{/ str }} {{/ isprivatereply }} {{/ canreply }}
PK-B\0mJJ(templates/forum_discussion_post.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_discussion_post Template to render a single post from a discussion. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }}
{{! The firstpost and starter classes below aren't used for anything other than to identify the first post in behat. }}
{{#isfirstunread}}{{/isfirstunread}}
{{^isdeleted}} {{#author}}
{{#urls.profileimage}} {{/urls.profileimage}}
{{/author}} {{/isdeleted}}
{{#parentauthorname}} {{#str}} inreplyto, mod_forum, {{.}} {{/str}} {{/parentauthorname}} {{$subjectheading}}

{{$subject}}{{{subject}}}{{/subject}}

{{/subjectheading}} {{^isdeleted}}
{{#html.authorsubheading}}{{{.}}}{{/html.authorsubheading}} {{^html.authorsubheading}} {{< core/time_element }} {{$elementid}}created-{{id}}-{{uniqid}}{{/elementid}} {{$timestampval}}{{timecreated}}{{/timestampval}} {{$userdateformatval}}{{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdateformatval}} {{/core/time_element}} {{/html.authorsubheading}}
{{/isdeleted}} {{#isprivatereply}}
{{#str}}postisprivatereply, forum{{/str}}
{{/isprivatereply}} {{#hasreplycount}} {{#str}} numberofreplies, mod_forum, {{replycount}} {{/str}} {{/hasreplycount}}
{{^isdeleted}} {{#author}}
{{#groups}} {{#urls.group}} {{#urls.image}} {{/urls.image}} {{/urls.group}} {{^urls.group}} {{#urls.image}} {{#str}} pictureof, core, {{name}} {{/str}} {{/urls.image}} {{/urls.group}} {{/groups}}
{{/author}} {{/isdeleted}}
{{{message}}}
{{^isdeleted}} {{#haswordcount}}

{{#str}} numwords, core, {{wordcount}} {{/str}}

{{/haswordcount}} {{#attachments}} {{#isimage}}
{{#str}} attachmentname, mod_forum, {{filename}} {{/str}} {{#urls.export}} {{#pix}} t/portfolioadd, core {{/pix}} {{/urls.export}} {{#html.plagiarism}}
{{{.}}}
{{/html.plagiarism}}
{{/isimage}} {{/attachments}} {{{html.taglist}}} {{#attachments}} {{^isimage}}
{{#pix}} {{icon}}, core {{/pix}} {{filename}} {{#urls.export}} {{#pix}} t/portfolioadd, core {{/pix}} {{/urls.export}} {{#html.plagiarism}}
{{{.}}}
{{/html.plagiarism}}
{{/isimage}} {{/attachments}} {{^readonly}}
{{#html.rating}}
{{{.}}}
{{/html.rating}} {{$actions}} {{/actions}}
{{/readonly}} {{$footer}}{{/footer}} {{/isdeleted}}
{{$replies}}
{{#hasreplies}} {{#replies}} {{> mod_forum/forum_discussion_post }} {{/replies}} {{/hasreplies}}
{{/replies}}
PK-B\ A`3templates/frontpage_social_discussion_list.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/frontpage_social_discussion_list Template which defines a forum post for sending in a single-post HTML email. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }} {{< mod_forum/social_discussion_list }} {{$discussion_create_text}} {{#str}}addanewtopic, forum{{/str}} {{/discussion_create_text}} {{$discussion_top_pagination}}{{/discussion_top_pagination}} {{$discussion_bottom_pagination}} {{#hasmore}} {{#str}}oldertopics, mod_forum{{/str}}... {{/hasmore}} {{/discussion_bottom_pagination}} {{/ mod_forum/social_discussion_list }} PK-B\o)templates/single_discussion_list.mustachenu[ {{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/single_discussion_list Template to render a discussion for a single discussion forum. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }} {{< mod_forum/discussion_list }} {{$discussion_create_text}} {{#str}}addanewtopic, forum{{/str}} {{/discussion_create_text}} {{$discussion_list_output}} {{#posts}} {{> mod_forum/forum_discussion_post }} {{/posts}} {{/discussion_list_output}} {{/ mod_forum/discussion_list }} PK-B\ templates/inpage_reply.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/inpage_reply Template which defines a forum post for sending in a single-post HTML email. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { "postid": 0, "reply_url": "", "sesskey": "", "parentsubject": "", "throttlingwarningmsg": "You are approaching the posting threshold. You have posted 3 times in the last 1 week and the limit is 4 posts." } }}
{{#throttlingwarningmsg}} {{/throttlingwarningmsg}}
{{#canreplyprivately}}
{{/canreplyprivately}}
PK-B\9/{{2templates/forum_discussion_threaded_posts.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_discussion_threaded_posts Template to render a list of posts in a threaded format. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }} {{#posts}} {{< mod_forum/forum_discussion_post }} {{$replies}} {{! The forumthread class is only added for behat. }}
{{#replies}} {{> mod_forum/forum_discussion_threaded_post }} {{/replies}}
{{/replies}} {{/ mod_forum/forum_discussion_post }} {{/posts}} PK-B\ ,templates/forum_subscription_action.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_subscription_action Actions bar for the subscribers page UI. Classes required for JS: * none Data attributes required for JS: * data-region Context variables required for this template: * see mod/forum/classes/output/subscription_actionbar.php Example context (json): { "subscriptionoptions": { "formid": "selectsubscriptions", "classes": "urlselect float-end", "disabled": false, "sesskey": "abcdef", "action": "http://localhost/course/jumpto.php", "helpicon": false, "options": [ { "name": "Subscription disabled", "value": "" }, { "name": "Optional subscrition", "value": "/mod/forum/subscribe.php?id=3&mode=0&sesskey=abc123" } ] }, "viewandmanageselect": { "formid": "selectviewandmanagesubscribers", "classes": "urlselect", "label": "Subscribers", "disabled": false, "sesskey": "abcdef", "action": "http://localhost/course/jumpto.php", "options": [ { "name": "View subscribers", "value": "/mod/forum/subscribers.php?id=3&edit=off", "selected": true }, { "name": "Manage subscribers", "value": "/mod/forum/subscribers.php?id=3&edit=on", "selected": false } ] } } }}
{{#viewandmanageselect}} {{/viewandmanageselect}} {{#subscriptionoptions}} {{/subscriptionoptions}}

PK-B\F>  'templates/forum_search_results.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_search_results Template to render results for the forum post search. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { "posts": [ { "id": 1, "subject": "Re: Test discussion", "message": "Reply to post by Sam Student", "author": { "id": 2, "fullname": "Sam Student" }, "discussionid": 3, "capabilities": { "view": true, "edit": true, "delete": true, "split": true, "reply": true }, "html": { "authorsubheading": "by Sam Student<\/a> -
{{! The firstpost and starter classes below aren't used for anything other than to identify the first post in behat. }}
{{#isfirstunread}}{{/isfirstunread}}
{{^isdeleted}} {{#author}} {{#urls.profileimage}} {{/urls.profileimage}} {{/author}} {{/isdeleted}} {{#unread}}
{{#pix}} i/flagged, core {{/pix}}
{{/unread}}
{{^isdeleted}}
{{#author}}

{{#parentauthorname}} {{#isprivatereply}} {{#str}} authorreplyingprivatelytoauthor, mod_forum, { "respondant":"{{fullname}}", "author":"{{parentauthorname}}" } {{/str}} {{/isprivatereply}} {{^isprivatereply}} {{#str}} authorreplyingtoauthor, mod_forum, { "respondant":"{{fullname}}", "author":"{{parentauthorname}}" } {{/str}} {{/isprivatereply}} {{/parentauthorname}} {{^parentauthorname}} {{fullname}} {{/parentauthorname}}

{{/author}} {{< core/time_element }} {{$elementid}}created-{{id}}-{{uniqid}}{{/elementid}} {{$elementclass}}text-muted{{/elementclass}} {{$timestampval}}{{timecreated}}{{/timestampval}} {{$userdateformatval}}{{#str}} strftimerecentfull, core_langconfig {{/str}}{{/userdateformatval}} {{/core/time_element}}
{{#author.groups}} {{#urls.image}}
{{#urls.group}} {{/urls.group}} {{^urls.group}} {{#str}} pictureof, core, {{name}} {{/str}} {{/urls.group}}
{{/urls.image}} {{/author.groups}} {{^readonly}} {{#showactionmenu}} {{/showactionmenu}} {{/readonly}}
{{/isdeleted}} {{$subject}}

{{{subject}}}

{{/subject}} {{#hasreplycount}} {{#str}} numberofreplies, mod_forum, {{replycount}} {{/str}} {{/hasreplycount}}
{{{message}}}
{{^isdeleted}} {{#attachments}} {{#isimage}}
{{#str}} attachmentname, mod_forum, {{filename}} {{/str}} {{#urls.export}} {{#pix}} t/portfolioadd, core {{/pix}} {{/urls.export}} {{#html.plagiarism}}
{{{.}}}
{{/html.plagiarism}}
{{/isimage}} {{/attachments}} {{#attachments}} {{^isimage}}
{{#pix}} {{icon}}, core {{/pix}}
{{/isimage}} {{/attachments}}
{{^readonly}} {{#html.rating}}
{{{.}}}
{{/html.rating}} {{/readonly}}
{{#haswordcount}} {{#str}} numwords, core, {{wordcount}} {{/str}} {{/haswordcount}} {{#html.taglist}}
{{{.}}}
{{/html.taglist}}
{{/isdeleted}} {{$footer}} {{^isdeleted}} {{^readonly}}
{{#capabilities.reply}} {{/capabilities.reply}} {{^capabilities.reply}} {{#discussionlocked}} {{/discussionlocked}} {{/capabilities.reply}}
{{/readonly}} {{/isdeleted}} {{#discussionlocked}}
{{#str}} locked, mod_forum {{/str}}
{{/discussionlocked}} {{/footer}}
{{$replies}}
{{#hasreplies}} {{#replies}} {{> mod_forum/forum_discussion_nested_v2_post_reply }} {{/replies}} {{/hasreplies}}
{{/replies}}
PK-B\j3/templates/forum_discussion_nested_post.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_discussion_nested_post Template to render a post in a nested format. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }} {{< mod_forum/forum_discussion_post }} {{$replies}}
{{#replies}} {{> mod_forum/forum_discussion_nested_post }} {{/replies}}
{{/replies}} {{/ mod_forum/forum_discussion_post }} PK-B\F׎1templates/forum_new_discussion_actionbar.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_new_discussion_actionbar Add button to create a new discussion in the forum activity. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * see mod/forum/classes/output/activity_actionbar.php Example context (json): { "forum": { "id": 3, "name": "Forum test", "state": [], "userstate": "", "capabilities": [{ "viewdiscussions": true, "create": true, "selfenrol": false }], "urls": [{ "create": "http://localhost/mod/forum/post.php?forum=11" }] } } }} {{#forum.capabilities.create}} {{#str}}addanewdiscussion, forum{{/str}} {{/forum.capabilities.create}} {{^forum.capabilities.create}} {{#forum.capabilities.selfenrol}} {{#str}}addanewdiscussion, forum{{/str}} {{/forum.capabilities.selfenrol}} {{/forum.capabilities.create}} PK-B\w'templates/news_discussion_list.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/news_discussion_list Template to render a list of discussions for a news forum Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }} {{< mod_forum/discussion_list }} {{$discussion_create_text}} {{#str}}addanewtopic, forum{{/str}} {{/discussion_create_text}} {{$no_discussions_text}} ({{#str}}nonews, forum{{/str}}) {{/no_discussions_text}} {{/ mod_forum/discussion_list }} PK-B\Y3templates/forum_discussion_nested_v2_posts.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_discussion_nested_v2_posts Template to render a list of posts for a discussion. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }} {{#posts}} {{> mod_forum/forum_discussion_nested_v2_first_post }} {{/posts}} PK-B\1A)templates/discussion_lock_toggle.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/discussion_lock_toggle Template to display the discussion subscription toggle. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * none Example context (json): { "id": 0, "locked": 1 } }} {{#locked}} {{#str}}unlockdiscussion, forum{{/str}} {{/locked}} {{^locked}} {{#str}}lockdiscussion, forum{{/str}} {{/locked}} PK-B\;#templates/forum_discussion.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_discussion Template for displaying a single forum discussion. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { "html": { "hasanyactions": true, "posts": "
", "modeselectorform": "
", "subscribe": null, "movediscussion": null, "pindiscussion": null, "neighbourlinks": "
", "exportdiscussion": null }, "throttlingwarningmsg": "You are approaching the posting threshold. You have posted 3 times in the last 1 week and the limit is 4 posts." } }}
{{#html}}
{{{exportdiscussion}}}
{{{modeselectorform}}}
{{{movediscussion}}}
{{#hasanyactions}} {{/hasanyactions}}
{{/html}} {{#notifications}} {{> core/notification}} {{/notifications}} {{{html.posts}}} {{#html.neighbourlinks}}{{{.}}}{{/html.neighbourlinks}}
{{#js}} require( [ 'jquery', 'core/templates', 'mod_forum/discussion', 'mod_forum/posts_list', 'mod_forum/lock_toggle', 'mod_forum/favourite_toggle', 'mod_forum/pin_toggle', 'mod_forum/subscription_toggle' ], function( $, Templates, Discussion, PostsList, LockToggle, FavouriteToggle, Pin, SubscribeToggle ) { var root = $("[data-content='forum-discussion']"); Discussion.init(root); PostsList.init(root, {{#quote}}{{throttlingwarningmsg}}{{/quote}}); root = $('[data-container="discussion-tools"]'); LockToggle.init(root, true); FavouriteToggle.init(root, true, function(toggleElement, context) { return Templates.render('mod_forum/discussion_favourite_toggle', context) .then(function(html, js) { return Templates.replaceNode(toggleElement, html, js); }); }); Pin.init(root, true, function(toggleElement, context) { return Templates.render('mod_forum/discussion_pin_toggle', context) .then(function(html, js) { return Templates.replaceNode(toggleElement, html, js); }); }); SubscribeToggle.init(root, true, function(toggleElement, context) { return Templates.render('mod_forum/discussion_subscription_toggle', context) .then(function(html, js) { return Templates.replaceNode(toggleElement, html, js); }); }); }); {{/js}} PK-B\1templates/forum_discussion_threaded_post.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_discussion_threaded_post Template to render a single post from a discussion in a threaded format. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }}
{{subject}} {{^isdeleted}} {{{html.authorsubheading}}} {{/isdeleted}}
{{#replies}} {{> mod_forum/forum_discussion_threaded_post }} {{/replies}}
PK-B\V. . )templates/social_discussion_list.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/social_discussion_list Template which defines a forum post for sending in a single-post HTML email. Classes required for JS: * none Data attributes required for JS: * none Example context (json): { } }} {{< mod_forum/discussion_list }} {{$discussion_create_text}} {{#str}}addanewdiscussion, forum{{/str}} {{/discussion_create_text}} {{$discussion_list_output}} {{#posts}} {{< mod_forum/forum_discussion_post }} {{$footer}} {{/footer}} {{$replyoutput}}{{/replyoutput}} {{/ mod_forum/forum_discussion_post }} {{/posts}} {{/discussion_list_output}} {{/ mod_forum/discussion_list }} PK-B\LPݻ-templates/forum_post_email_htmlemail.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_forum/forum_post_email_htmlemail Template which defines a forum post for sending in a single-post HTML email. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * courselink * coursename * forumindexlink * forumviewlink * forumname * discussionlink * discussionname * showdiscussionname * firstpost * subject * authorlink * authorpicture * authorfullname * postdate * grouppicture * attachments * message * parentpostlink * canreply * replylink * permalink * unsubscribeforumlink * unsubscribediscussionlink Example context (json): { "courselink": "https://example.com/course/view.php?id=2", "coursename": "Example course", "forumindexlink": "https://example.com/mod/forum/index.php?id=2", "forumviewlink": "https://example.com/mod/forum/view.php?f=2", "forumname": "Lorem ipsum dolor", "discussionlink": "https://example.com/mod/forum/discuss.php?d=70", "discussionname": "Is Lorem ipsum Latin?", "showdiscussionname": 1, "firstpost": 1, "subject": "Is Lorem ipsum Latin?", "authorlink": "https://example.com/user/view.php?id=2&course=2", "authorpicture": "\"Picture", "authorfullname": "Lucius Caecilius lucundus", "postdate": "Sunday, 13 September 2015, 2:22 pm", "grouppicture": "", "attachments": "", "message": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et auctor libero. Quisque porta egestas neque, et bibendum libero dignissim at. Nulla facilisi. Morbi eget accumsan felis. Nunc et vulputate odio, vel venenatis nisl. Nunc maximus ipsum sed tincidunt mollis. Integer nunc erat, luctus sit amet arcu tincidunt, volutpat dignissim mi. Sed ut magna quam. Mauris accumsan porta turpis sed aliquam. Etiam at justo tristique, imperdiet augue quis, consectetur sapien. Ut nec erat malesuada sem suscipit lobortis. Vivamus posuere nibh eu ipsum porta fringilla. Sed vitae dapibus ipsum, ac condimentum enim. Sed dignissim ante at elit mollis, ac tempor lacus iaculis. Etiam nec lectus vitae nibh vulputate volutpat. Nulla quis tellus aliquam, commodo nisi et, dictum est.


", "parentpostlink": "", "canreply": 1, "replylink": "https://example.com/mod/forum/post.php?reply=2", "permalink": "https://example.com/mod/forum/discuss.php?d=2#2", "unsubscribeforumlink": "https://example.com/mod/forum/subscribe.php?id=2", "unsubscribediscussionlink": "https://example.com/mod/discussion/subscribe.php?id=2&d=2" } }} {{> mod_forum/forum_post_email_htmlemail_body }}
PK-B\so&o& upgrade.txtnu[=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in /mod/forum/*, information provided here is intended especially for developers. === 4.5 === * Final deprecation of forum_update_subscriptions_button(). === 4.4 === * The mod_forum_get_forum_discussions_paginated web service has been removed from core. === 4.3 === * The forum_print_discussion_header function has been deprecated and will not be replaced. === 4.1 === * The following code, previously used (exclusively) by upgrade steps is not available anymore because of the upgrade cleanup performed for this version. See MDL-71747 for more info: - \mod_forum\task\refresh_forum_post_counts adhoc task. === 4.0 === * The forum_count_replies() function has been removed from core. * The mod_forum_get_forum_discussion_posts web service has been removed from core. * Final deprecations of the following functions: * forum_make_mail_text * forum_make_mail_html * forum_make_mail_post * forum_cron_minimise_user_record * forum_cron * forum_print_discussion * forum_post_nesting_cache * forum_should_start_post_nesting * forum_should_end_post_nesting * forum_print_post_start * forum_print_post_end * forum_print_post * forum_print_posts_flat * forum_print_posts_threaded * forum_print_posts_nested * forum_print_latest_discussions * forum_get_user_grades * The method forum_update_subscriptions_button() has been deprecated as it is no longer used. The 'Manage subscribers' button has been replaced with tertiary navigation. === 3.11 === * The get_forum_discussions_paginated web service has been deprecated in favour of get_forum_discussions. * The forum post exporter now includes a "timemodified" field for each post, which is included in several WS methods: * mod_forum_get_discussion_posts * get_discussion_posts_by_userid * get_discussion_post * add_discussion_post === 3.10 === * Changes in external function mod_forum_external::get_discussion_posts_by_userid Now returns the posts of a given user checking the current user capabilities ($USER, the user who is requesting the posts). Previously, it returned the posts checking the capabilities of the user that created the posts. === 3.8 === * The following functions have been finally deprecated and can not be used anymore: * forum_scale_used() * In order for the forum summary report to calculate word count and character count data, those details are now stored for each post in the database when posts are created or updated. For posts that existed prior to a Moodle 3.8 upgrade, these are calculated by the refresh_forum_post_counts ad-hoc task in chunks of 5000 posts by default. Site admins are able to modify this default, by setting $CFG->forumpostcountchunksize to the required integer value. === 3.7 === * Changed the forum discussion rendering to use templates rather than print functions. * Added new forum entities, factories, exporters, renderers, and vaults in the local namespace to better encapsulate the forum data. * Deprecated all of the forum_print_* functions in lib.php. * The forum_print_latest_discussions function has been deprecated and will not be replaced. * The get_forum_discussion_posts web service has been deprecated in favour of get_discussion_posts. * The forum_count_replies function has been deprecated in favour of get_reply_count_for_post_id_in_discussion_id in the Post vault. * External function get_forums_by_courses now returns two additional fields "duedate" and "cutoffdate" containing the due date and the cutoff date for posting to the forums respectively. * External function get_forum_discussion_posts now returns an additional field "tags" returning the post tags. * New external function mod_forum_external::get_forum_discussions returns a list of forum discussions optionally sorted and paginated. Use mod_forum_external::get_forum_discussions instead. * External function mod_forum_external::add_discussion_post() has a new 'messageformat' param to be able to handle other formats different from FORMAT_HTML (that continues being the default one). Also a new 'topreferredformat' option enables the automatic conversion between any format and the preferred format (given the editor preference) before contents are saved. === 3.6 === * forum_print_post should be surrounded with calls to forum_print_post_start and forum_print_post_end to create the proper HTML structure for the post. === 3.4 === * External function get_forum_discussion_posts now returns an additional field "ratinginfo" containing rating information. === 3.3 === * External function get_forums_by_courses now returns and additional field "istracked" that indicates if the user is tracking the related forum. * The legacy forum.js file has been removed, this includes the js functions: forum_produce_subscribe_link, forum_produce_tracking_link, lockoptions_timetoitems, lockoptions_timefromitems, lockoptions, lockoption, unlockoption * External function can_add_discussion now returns two additional fields: "canpindiscussions" that indicates if the user can add pinned discussions. "cancreateattachment" that indicates if the user can add attachments to the discussion. === 3.2 === * The setting $CFG->forum_replytouser has been removed in favour of a centralized noreplyaddress setting. Please use $CFG->noreplyaddress setting instead. * The following functions have been finally deprecated and should no longer be used. - forum_count_unrated_posts - forum_tp_count_discussion_read_records - forum_get_user_discussions - forum_tp_count_forum_posts - forum_tp_count_forum_read_records - forum_get_open_modes - forum_get_child_posts - forum_get_discussion_posts - forum_get_ratings - forum_get_tracking_link - forum_tp_count_discussion_unread_posts - forum_convert_to_roles - forum_tp_get_read_records - forum_tp_get_discussion_read_records - forum_user_enrolled - forum_user_can_view_post - forum_shorten_post - forum_is_subscribed - forum_subscribe - forum_unsubscribe - forum_subscribed_users - forum_is_forcesubscribed - forum_forcesubscribe - forum_get_forcesubscribed - forum_get_subscribed_forums - forum_get_optional_subscribed_forums - forum_get_potential_subscribers * External functions that were returning file information now return the following file fields: filename, filepath, mimetype, filesize, timemodified and fileurl. Those fields are now marked as VALUE_OPTIONAL for backwards compatibility. === 3.1 === * The inteface to forum_get_email_message_id() has changed and no longer needs the $host argument. === 3.0 === * External function get_forums_by_courses now returns and additional field "cancreatediscussions" that indicates if the user can create discussions in the forum. * A new optional parameter (groupid) has been added to get_forum_discussions. This parameter can override the automatically calculated current group. * New constant FORUM_POSTS_ALL_USER_GROUPS, to be used as parameter in functions where we'd need to retrieve all the user posts. === 2.8 === * The following functions have all been marked as deprecated. Many of these have not been supported in many releases and should not be relied upon: forum_count_unrated_posts forum_tp_count_discussion_read_records forum_get_user_discussions forum_tp_count_forum_posts forum_tp_count_forum_read_records forum_get_open_modes forum_get_child_posts forum_get_discussion_posts forum_get_ratings forum_get_tracking_link forum_tp_count_discussion_unread_posts forum_convert_to_roles forum_tp_get_read_records forum_tp_get_discussion_read_records forum_user_enrolled forum_user_can_view_post * The following functions have been deprecated and replaced. Please see the phpdoc for each on the new function to use instead: * forum_is_subscribed * forum_subscribe * forum_unsubscribe * forum_subscribed_users * forum_is_forcesubscribed * forum_forcesubscribe * forum_get_forcesubscribed * forum_get_subscribed_forums * forum_get_optional_subscribed_forums * forum_get_potential_subscribers * External function mod_forum_external::get_forum_discussions has been deprecated. Use mod_forum_external::get_forum_discussions_paginated instead. === 2.6 === * The file post_form.php should not be included, the class it contained has been moved so that it can benefit from autoloading. * The function forum_shorten_post() has been deprecated. It was doing a poor job of shortening forum post text and the shorten_text() function does a much better job. * The constant FORUM_TRACKING_ON has been deprecated and replaced by FORUM_TRACKING_FORCED. The value between them is maintained, so they are interchangeable, but code should migrate to the new name. === 2.5 === The function forum_check_throttling has been changed so that a warning object is returned when a user has reached the 'Post threshold for warning' or 'Post threshold for blocking' setting, rather than echoing this on screen. This allows the warning to be moved inside the post form as it can be added as a HTML element, where it is more noticeable. False is returned if there is no need to warn, or restrict the user from posting - see MDL-39182. === 2.3.5, 2.4.2 === * mod/forum:allowforcesubscribe capability will be forcefully assigned to frontpage role, as it was mistakenly missed off when the capability was initially created. If you don't want users with frontpage role to get forum (with forcesubscribe) emails, then please remove this capability for frontpage role. PK-B\&vv locallib.phpnu[. /** * Library of functions for forum outside of the core api * * @package mod_forum * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // Event types. define('FORUM_EVENT_TYPE_DUE', 'due'); require_once($CFG->dirroot . '/mod/forum/lib.php'); require_once($CFG->libdir . '/portfolio/caller.php'); /** * @package mod_forum * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class forum_portfolio_caller extends portfolio_module_caller_base { protected $postid; protected $discussionid; protected $attachment; private $post; private $forum; private $discussion; private $posts; private $keyedfiles; // just using multifiles isn't enough if we're exporting a full thread /** @var context_module context instance. */ private $modcontext; /** * @return array */ public static function expected_callbackargs() { return array( 'postid' => false, 'discussionid' => false, 'attachment' => false, ); } /** * @param array $callbackargs */ function __construct($callbackargs) { parent::__construct($callbackargs); if (!$this->postid && !$this->discussionid) { throw new portfolio_caller_exception('mustprovidediscussionorpost', 'forum'); } } /** * @global object */ public function load_data() { global $DB; if ($this->postid) { if (!$this->post = $DB->get_record('forum_posts', array('id' => $this->postid))) { throw new portfolio_caller_exception('invalidpostid', 'forum'); } } $dparams = array(); if ($this->discussionid) { $dbparams = array('id' => $this->discussionid); } else if ($this->post) { $dbparams = array('id' => $this->post->discussion); } else { throw new portfolio_caller_exception('mustprovidediscussionorpost', 'forum'); } if (!$this->discussion = $DB->get_record('forum_discussions', $dbparams)) { throw new portfolio_caller_exception('invaliddiscussionid', 'forum'); } if (!$this->forum = $DB->get_record('forum', array('id' => $this->discussion->forum))) { throw new portfolio_caller_exception('invalidforumid', 'forum'); } if (!$this->cm = get_coursemodule_from_instance('forum', $this->forum->id)) { throw new portfolio_caller_exception('invalidcoursemodule'); } $this->modcontext = context_module::instance($this->cm->id); $fs = get_file_storage(); if ($this->post) { if ($this->attachment) { // Make sure the requested file belongs to this post. $file = $fs->get_file_by_id($this->attachment); if ($file->get_contextid() != $this->modcontext->id || $file->get_itemid() != $this->post->id) { throw new portfolio_caller_exception('filenotfound'); } $this->set_file_and_format_data($this->attachment); } else { $attach = $fs->get_area_files($this->modcontext->id, 'mod_forum', 'attachment', $this->post->id, 'timemodified', false); $embed = $fs->get_area_files($this->modcontext->id, 'mod_forum', 'post', $this->post->id, 'timemodified', false); $files = array_merge($attach, $embed); $this->set_file_and_format_data($files); } if (!empty($this->multifiles)) { $this->keyedfiles[$this->post->id] = $this->multifiles; } else if (!empty($this->singlefile)) { $this->keyedfiles[$this->post->id] = array($this->singlefile); } } else { // whole thread $fs = get_file_storage(); $this->posts = forum_get_all_discussion_posts($this->discussion->id, 'p.created ASC'); $this->multifiles = array(); foreach ($this->posts as $post) { $attach = $fs->get_area_files($this->modcontext->id, 'mod_forum', 'attachment', $post->id, 'timemodified', false); $embed = $fs->get_area_files($this->modcontext->id, 'mod_forum', 'post', $post->id, 'timemodified', false); $files = array_merge($attach, $embed); if ($files) { $this->keyedfiles[$post->id] = $files; } else { continue; } $this->multifiles = array_merge($this->multifiles, array_values($this->keyedfiles[$post->id])); } } if (empty($this->multifiles) && !empty($this->singlefile)) { $this->multifiles = array($this->singlefile); // copy_files workaround } // depending on whether there are files or not, we might have to change richhtml/plainhtml if (empty($this->attachment)) { if (!empty($this->multifiles)) { $this->add_format(PORTFOLIO_FORMAT_RICHHTML); } else { $this->add_format(PORTFOLIO_FORMAT_PLAINHTML); } } } /** * @global object * @return string */ function get_return_url() { global $CFG; return $CFG->wwwroot . '/mod/forum/discuss.php?d=' . $this->discussion->id; } /** * @global object * @return array */ function get_navigation() { global $CFG; $navlinks = array(); $navlinks[] = array( 'name' => format_string($this->discussion->name), 'link' => $CFG->wwwroot . '/mod/forum/discuss.php?d=' . $this->discussion->id, 'type' => 'title' ); return array($navlinks, $this->cm); } /** * either a whole discussion * a single post, with or without attachment * or just an attachment with no post * * @global object * @global object * @uses PORTFOLIO_FORMAT_RICH * @return mixed */ function prepare_package() { global $CFG; // set up the leap2a writer if we need it $writingleap = false; if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) { $leapwriter = $this->exporter->get('format')->leap2a_writer(); $writingleap = true; } if ($this->attachment) { // simplest case first - single file attachment $this->copy_files(array($this->singlefile), $this->attachment); if ($writingleap) { // if we're writing leap, make the manifest to go along with the file $entry = new portfolio_format_leap2a_file($this->singlefile->get_filename(), $this->singlefile); $leapwriter->add_entry($entry); return $this->exporter->write_new_file($leapwriter->to_xml(), $this->exporter->get('format')->manifest_name(), true); } } else if (empty($this->post)) { // exporting whole discussion $content = ''; // if we're just writing HTML, start a string to add each post to $ids = array(); // if we're writing leap2a, keep track of all entryids so we can add a selection element foreach ($this->posts as $post) { $posthtml = $this->prepare_post($post); if ($writingleap) { $ids[] = $this->prepare_post_leap2a($leapwriter, $post, $posthtml); } else { $content .= $posthtml . '

'; } } $this->copy_files($this->multifiles); $name = 'discussion.html'; $manifest = ($this->exporter->get('format') instanceof PORTFOLIO_FORMAT_RICH); if ($writingleap) { // add on an extra 'selection' entry $selection = new portfolio_format_leap2a_entry('forumdiscussion' . $this->discussionid, get_string('discussion', 'forum') . ': ' . $this->discussion->name, 'selection'); $leapwriter->add_entry($selection); $leapwriter->make_selection($selection, $ids, 'Grouping'); $content = $leapwriter->to_xml(); $name = $this->get('exporter')->get('format')->manifest_name(); } $this->get('exporter')->write_new_file($content, $name, $manifest); } else { // exporting a single post $posthtml = $this->prepare_post($this->post); $content = $posthtml; $name = 'post.html'; $manifest = ($this->exporter->get('format') instanceof PORTFOLIO_FORMAT_RICH); if ($writingleap) { $this->prepare_post_leap2a($leapwriter, $this->post, $posthtml); $content = $leapwriter->to_xml(); $name = $this->exporter->get('format')->manifest_name(); } $this->copy_files($this->multifiles); $this->get('exporter')->write_new_file($content, $name, $manifest); } } /** * helper function to add a leap2a entry element * that corresponds to a single forum post, * including any attachments * * the entry/ies are added directly to the leapwriter, which is passed by ref * * @param portfolio_format_leap2a_writer $leapwriter writer object to add entries to * @param object $post the stdclass object representing the database record * @param string $posthtml the content of the post (prepared by {@link prepare_post} * * @return int id of new entry */ private function prepare_post_leap2a(portfolio_format_leap2a_writer $leapwriter, $post, $posthtml) { $entry = new portfolio_format_leap2a_entry('forumpost' . $post->id, $post->subject, 'resource', $posthtml); $entry->published = $post->created; $entry->updated = $post->modified; $entry->author = $post->author; if (is_array($this->keyedfiles) && array_key_exists($post->id, $this->keyedfiles) && is_array($this->keyedfiles[$post->id])) { $leapwriter->link_files($entry, $this->keyedfiles[$post->id], 'forumpost' . $post->id . 'attachment'); } $entry->add_category('web', 'resource_type'); $leapwriter->add_entry($entry); return $entry->id; } /** * @param array $files * @param mixed $justone false of id of single file to copy * @return bool|void */ private function copy_files($files, $justone=false) { if (empty($files)) { return; } foreach ($files as $f) { if ($justone && $f->get_id() != $justone) { continue; } $this->get('exporter')->copy_existing_file($f); if ($justone && $f->get_id() == $justone) { return true; // all we need to do } } } /** * this is a very cut down version of what is in forum_make_mail_post * * @global object * @param int $post * @return string */ private function prepare_post($post, $fileoutputextras=null) { global $DB; static $users; if (empty($users)) { $users = array($this->user->id => $this->user); } if (!array_key_exists($post->userid, $users)) { $users[$post->userid] = $DB->get_record('user', array('id' => $post->userid)); } // add the user object on to the post so we can pass it to the leap writer if necessary $post->author = $users[$post->userid]; $viewfullnames = true; // format the post body $options = portfolio_format_text_options(); $options->context = $this->modcontext; $format = $this->get('exporter')->get('format'); $formattedtext = format_text($post->message, $post->messageformat, $options); $formattedtext = portfolio_rewrite_pluginfile_urls($formattedtext, $this->modcontext->id, 'mod_forum', 'post', $post->id, $format); $output = ''; $output .= ''; if ($post->parent) { $output .= ''; $output .= '
';// can't print picture. $output .= ''; } else { $output .= ''; } $output .= '
'.format_string($post->subject).'
'; $fullname = fullname($users[$post->userid], $viewfullnames); $by = new stdClass(); $by->name = $fullname; $by->date = userdate($post->modified, '', core_date::get_user_timezone($this->user)); $output .= '
'.get_string('bynameondate', 'forum', $by).'
'; $output .= '
'; $output .= ''; $output .= $formattedtext; if (is_array($this->keyedfiles) && array_key_exists($post->id, $this->keyedfiles) && is_array($this->keyedfiles[$post->id]) && count($this->keyedfiles[$post->id]) > 0) { $output .= '
'; $output .= '
' . get_string('attachments', 'forum') . ':

'; foreach ($this->keyedfiles[$post->id] as $file) { $output .= $format->file_output($file) . '
'; } $output .= "
"; } $output .= '
'."\n\n"; return $output; } /** * @return string */ function get_sha1() { $filesha = ''; try { $filesha = $this->get_sha1_file(); } catch (portfolio_caller_exception $e) { } // no files if ($this->post) { return sha1($filesha . ',' . $this->post->subject . ',' . $this->post->message); } else { $sha1s = array($filesha); foreach ($this->posts as $post) { $sha1s[] = sha1($post->subject . ',' . $post->message); } return sha1(implode(',', $sha1s)); } } function expected_time() { $filetime = $this->expected_time_file(); if ($this->posts) { $posttime = portfolio_expected_time_db(count($this->posts)); if ($filetime < $posttime) { return $posttime; } } return $filetime; } /** * @uses CONTEXT_MODULE * @return bool */ function check_permissions() { $context = context_module::instance($this->cm->id); if ($this->post) { return (has_capability('mod/forum:exportpost', $context) || ($this->post->userid == $this->user->id && has_capability('mod/forum:exportownpost', $context))); } return has_capability('mod/forum:exportdiscussion', $context); } /** * @return string */ public static function display_name() { return get_string('modulename', 'forum'); } public static function base_supported_formats() { return array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_RICHHTML, PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_LEAP2A); } } /** * Class representing the virtual node with all itemids in the file browser * * @category files * @copyright 2012 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class forum_file_info_container extends file_info { /** @var file_browser */ protected $browser; /** @var stdClass */ protected $course; /** @var stdClass */ protected $cm; /** @var string */ protected $component; /** @var stdClass */ protected $context; /** @var array */ protected $areas; /** @var string */ protected $filearea; /** * Constructor (in case you did not realize it ;-) * * @param file_browser $browser * @param stdClass $course * @param stdClass $cm * @param stdClass $context * @param array $areas * @param string $filearea */ public function __construct($browser, $course, $cm, $context, $areas, $filearea) { parent::__construct($browser, $context); $this->browser = $browser; $this->course = $course; $this->cm = $cm; $this->component = 'mod_forum'; $this->context = $context; $this->areas = $areas; $this->filearea = $filearea; } /** * @return array with keys contextid, filearea, itemid, filepath and filename */ public function get_params() { return array( 'contextid' => $this->context->id, 'component' => $this->component, 'filearea' => $this->filearea, 'itemid' => null, 'filepath' => null, 'filename' => null, ); } /** * Can new files or directories be added via the file browser * * @return bool */ public function is_writable() { return false; } /** * Should this node be considered as a folder in the file browser * * @return bool */ public function is_directory() { return true; } /** * Returns localised visible name of this node * * @return string */ public function get_visible_name() { return $this->areas[$this->filearea]; } /** * Returns list of children nodes * * @return array of file_info instances */ public function get_children() { return $this->get_filtered_children('*', false, true); } /** * Help function to return files matching extensions or their count * * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg') * @param bool|int $countonly if false returns the children, if an int returns just the * count of children but stops counting when $countonly number of children is reached * @param bool $returnemptyfolders if true returns items that don't have matching files inside * @return array|int array of file_info instances or the count */ private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) { global $DB; $params = array('contextid' => $this->context->id, 'component' => $this->component, 'filearea' => $this->filearea); $sql = 'SELECT DISTINCT itemid FROM {files} WHERE contextid = :contextid AND component = :component AND filearea = :filearea'; if (!$returnemptyfolders) { $sql .= ' AND filename <> :emptyfilename'; $params['emptyfilename'] = '.'; } list($sql2, $params2) = $this->build_search_files_sql($extensions); $sql .= ' '.$sql2; $params = array_merge($params, $params2); if ($countonly !== false) { $sql .= ' ORDER BY itemid DESC'; } $rs = $DB->get_recordset_sql($sql, $params); $children = array(); foreach ($rs as $record) { if (($child = $this->browser->get_file_info($this->context, 'mod_forum', $this->filearea, $record->itemid)) && ($returnemptyfolders || $child->count_non_empty_children($extensions))) { $children[] = $child; } if ($countonly !== false && count($children) >= $countonly) { break; } } $rs->close(); if ($countonly !== false) { return count($children); } return $children; } /** * Returns list of children which are either files matching the specified extensions * or folders that contain at least one such file. * * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg') * @return array of file_info instances */ public function get_non_empty_children($extensions = '*') { return $this->get_filtered_children($extensions, false); } /** * Returns the number of children which are either files matching the specified extensions * or folders containing at least one such file. * * @param string|array $extensions, for example '*' or array('.gif','.jpg') * @param int $limit stop counting after at least $limit non-empty children are found * @return int */ public function count_non_empty_children($extensions = '*', $limit = 1) { return $this->get_filtered_children($extensions, $limit); } /** * Returns parent file_info instance * * @return file_info or null for root */ public function get_parent() { return $this->browser->get_file_info($this->context); } } /** * Returns forum posts tagged with a specified tag. * * This is a callback used by the tag area mod_forum/forum_posts to search for forum posts * tagged with a specific tag. * * @param core_tag_tag $tag * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag * are displayed on the page and the per-page limit may be bigger * @param int $fromctx context id where the link was displayed, may be used by callbacks * to display items in the same context first * @param int $ctx context id where to search for records * @param bool $rec search in subcontexts as well * @param int $page 0-based number of page being displayed * @return \core_tag\output\tagindex */ function mod_forum_get_tagged_posts($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) { global $OUTPUT; $perpage = $exclusivemode ? 20 : 5; // Build the SQL query. $ctxselect = context_helper::get_preload_record_columns_sql('ctx'); $query = "SELECT fp.id, fp.subject, fd.forum, fp.discussion, f.type, fd.timestart, fd.timeend, fd.groupid, fd.firstpost, fp.parent, fp.userid, cm.id AS cmid, c.id AS courseid, c.shortname, c.fullname, $ctxselect FROM {forum_posts} fp JOIN {forum_discussions} fd ON fp.discussion = fd.id JOIN {forum} f ON f.id = fd.forum JOIN {modules} m ON m.name='forum' JOIN {course_modules} cm ON cm.module = m.id AND cm.instance = f.id JOIN {tag_instance} tt ON fp.id = tt.itemid JOIN {course} c ON cm.course = c.id JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component AND cm.deletioninprogress = 0 AND fp.id %ITEMFILTER% AND c.id %COURSEFILTER%"; $params = array('itemtype' => 'forum_posts', 'tagid' => $tag->id, 'component' => 'mod_forum', 'coursemodulecontextlevel' => CONTEXT_MODULE); if ($ctx) { $context = $ctx ? context::instance_by_id($ctx) : context_system::instance(); $query .= $rec ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid'; $params['contextid'] = $context->id; $params['path'] = $context->path.'/%'; } $query .= " ORDER BY "; if ($fromctx) { // In order-clause specify that modules from inside "fromctx" context should be returned first. $fromcontext = context::instance_by_id($fromctx); $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),'; $params['fromcontextid'] = $fromcontext->id; $params['frompath'] = $fromcontext->path.'/%'; } $query .= ' c.sortorder, cm.id, fp.id'; $totalpages = $page + 1; // Use core_tag_index_builder to build and filter the list of items. $builder = new core_tag_index_builder('mod_forum', 'forum_posts', $query, $params, $page * $perpage, $perpage + 1); while ($item = $builder->has_item_that_needs_access_check()) { context_helper::preload_from_record($item); $courseid = $item->courseid; if (!$builder->can_access_course($courseid)) { $builder->set_accessible($item, false); continue; } $modinfo = get_fast_modinfo($builder->get_course($courseid)); // Set accessibility of this item and all other items in the same course. $builder->walk(function ($taggeditem) use ($courseid, $modinfo, $builder, $item) { // Checking permission for Q&A forums performs additional DB queries, do not do them in bulk. if ($taggeditem->courseid == $courseid && ($taggeditem->type != 'qanda' || $taggeditem->id == $item->id)) { $cm = $modinfo->get_cm($taggeditem->cmid); $forum = (object)['id' => $taggeditem->forum, 'course' => $taggeditem->courseid, 'type' => $taggeditem->type ]; $discussion = (object)['id' => $taggeditem->discussion, 'timestart' => $taggeditem->timestart, 'timeend' => $taggeditem->timeend, 'groupid' => $taggeditem->groupid, 'firstpost' => $taggeditem->firstpost ]; $post = (object)['id' => $taggeditem->id, 'parent' => $taggeditem->parent, 'userid' => $taggeditem->userid, 'groupid' => $taggeditem->groupid ]; $accessible = forum_user_can_see_post($forum, $discussion, $post, null, $cm); $builder->set_accessible($taggeditem, $accessible); } }); } $items = $builder->get_items(); if (count($items) > $perpage) { $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists. array_pop($items); } // Build the display contents. if ($items) { $tagfeed = new core_tag\output\tagfeed(); foreach ($items as $item) { context_helper::preload_from_record($item); $modinfo = get_fast_modinfo($item->courseid); $cm = $modinfo->get_cm($item->cmid); $pageurl = new moodle_url('/mod/forum/discuss.php', array('d' => $item->discussion), 'p' . $item->id); $pagename = format_string($item->subject, true, array('context' => context_module::instance($item->cmid))); $pagename = html_writer::link($pageurl, $pagename); $courseurl = course_get_url($item->courseid, $cm->sectionnum); $cmname = html_writer::link($cm->url, $cm->get_formatted_name()); $coursename = format_string($item->fullname, true, array('context' => context_course::instance($item->courseid))); $coursename = html_writer::link($courseurl, $coursename); $icon = html_writer::link($pageurl, html_writer::empty_tag('img', array('src' => $cm->get_icon_url()))); $tagfeed->add($icon, $pagename, $cmname.'
'.$coursename); } $content = $OUTPUT->render_from_template('core_tag/tagfeed', $tagfeed->export_for_template($OUTPUT)); return new core_tag\output\tagindex($tag, 'mod_forum', 'forum_posts', $content, $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages); } } /** * Update the calendar entries for this forum activity. * * @param stdClass $forum the row from the database table forum. * @param int $cmid The coursemodule id * @return bool */ function forum_update_calendar($forum, $cmid) { global $DB, $CFG; require_once($CFG->dirroot.'/calendar/lib.php'); $event = new stdClass(); if (!empty($forum->duedate)) { $event->name = get_string('calendardue', 'forum', $forum->name); $event->description = format_module_intro('forum', $forum, $cmid, false); $event->format = FORMAT_HTML; $event->courseid = $forum->course; $event->modulename = 'forum'; $event->instance = $forum->id; $event->type = CALENDAR_EVENT_TYPE_ACTION; $event->eventtype = FORUM_EVENT_TYPE_DUE; $event->timestart = $forum->duedate; $event->timesort = $forum->duedate; $event->visible = instance_is_visible('forum', $forum); } $event->id = $DB->get_field('event', 'id', array('modulename' => 'forum', 'instance' => $forum->id, 'eventtype' => FORUM_EVENT_TYPE_DUE)); if ($event->id) { $calendarevent = calendar_event::load($event->id); if (!empty($forum->duedate)) { // Calendar event exists so update it. $calendarevent->update($event); } else { // Calendar event is no longer needed. $calendarevent->delete(); } } else if (!empty($forum->duedate)) { // Event doesn't exist so create one. calendar_event::create($event); } return true; }PK-B\,++ subscribe.phpnu[. /** * Subscribe to or unsubscribe from a forum or manage forum subscription mode * * This script can be used by either individual users to subscribe to or * unsubscribe from a forum (no 'mode' param provided), or by forum managers * to control the subscription mode (by 'mode' param). * This script can be called from a link in email so the sesskey is not * required parameter. However, if sesskey is missing, the user has to go * through a confirmation page that redirects the user back with the * sesskey. * * @package mod_forum * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require(__DIR__.'/../../config.php'); require_once($CFG->dirroot.'/mod/forum/lib.php'); $id = required_param('id', PARAM_INT); // The forum to set subscription on. $mode = optional_param('mode', null, PARAM_INT); // The forum's subscription mode. $user = optional_param('user', 0, PARAM_INT); // The userid of the user to subscribe, defaults to $USER. $discussionid = optional_param('d', null, PARAM_INT); // The discussionid to subscribe. $sesskey = optional_param('sesskey', null, PARAM_RAW); $returnurl = optional_param('returnurl', null, PARAM_LOCALURL); $edit = optional_param('edit', 'off', PARAM_ALPHANUM); $url = new moodle_url('/mod/forum/subscribe.php', array('id'=>$id)); if (!is_null($mode)) { $url->param('mode', $mode); } if ($user !== 0) { $url->param('user', $user); } if (!is_null($sesskey)) { $url->param('sesskey', $sesskey); } if (!is_null($discussionid)) { $url->param('d', $discussionid); if (!$discussion = $DB->get_record('forum_discussions', array('id' => $discussionid, 'forum' => $id))) { throw new \moodle_exception('invaliddiscussionid', 'forum'); } } $PAGE->set_url($url); $forum = $DB->get_record('forum', array('id' => $id), '*', MUST_EXIST); $course = $DB->get_record('course', array('id' => $forum->course), '*', MUST_EXIST); $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id, false, MUST_EXIST); $context = context_module::instance($cm->id); if ($user) { require_sesskey(); if (!has_capability('mod/forum:managesubscriptions', $context)) { throw new \moodle_exception('nopermissiontosubscribe', 'forum'); } $user = $DB->get_record('user', array('id' => $user), '*', MUST_EXIST); } else { $user = $USER; } if (isset($cm->groupmode) && empty($course->groupmodeforce)) { $groupmode = $cm->groupmode; } else { $groupmode = $course->groupmode; } $issubscribed = \mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussionid, $cm); // For a user to subscribe when a groupmode is set, they must have access to at least one group. if ($groupmode && !$issubscribed && !has_capability('moodle/site:accessallgroups', $context)) { if (!groups_get_all_groups($course->id, $USER->id)) { throw new \moodle_exception('cannotsubscribe', 'forum'); } } require_login($course, false, $cm); if (is_null($mode) and !is_enrolled($context, $USER, '', true)) { // Guests and visitors can't subscribe - only enrolled $PAGE->set_title($course->shortname); $PAGE->set_heading($course->fullname); if (isguestuser()) { echo $OUTPUT->header(); echo $OUTPUT->confirm(get_string('subscribeenrolledonly', 'forum').'

'.get_string('liketologin'), get_login_url(), new moodle_url('/mod/forum/view.php', array('f'=>$id))); echo $OUTPUT->footer(); exit; } else { // There should not be any links leading to this place, just redirect. redirect( new moodle_url('/mod/forum/view.php', array('f'=>$id)), get_string('subscribeenrolledonly', 'forum'), null, \core\output\notification::NOTIFY_ERROR ); } } $returnto = optional_param('backtoindex',0,PARAM_INT) ? "index.php?id=".$course->id : "view.php?f=$id"; if ($returnurl) { $returnto = $returnurl; } $subscribersurl = new moodle_url('/mod/forum/subscribers.php', ['id' => $id, 'edit' => $edit]); if (!is_null($mode) and has_capability('mod/forum:managesubscriptions', $context)) { require_sesskey(); switch ($mode) { case FORUM_CHOOSESUBSCRIBE : // 0 \mod_forum\subscriptions::set_subscription_mode($forum, FORUM_CHOOSESUBSCRIBE); redirect( $subscribersurl, get_string('everyonecannowchoose', 'forum'), null, \core\output\notification::NOTIFY_SUCCESS ); break; case FORUM_FORCESUBSCRIBE : // 1 \mod_forum\subscriptions::set_subscription_mode($forum, FORUM_FORCESUBSCRIBE); redirect( $subscribersurl, get_string('everyoneisnowsubscribed', 'forum'), null, \core\output\notification::NOTIFY_SUCCESS ); break; case FORUM_INITIALSUBSCRIBE : // 2 \mod_forum\subscriptions::set_subscription_mode($forum, FORUM_INITIALSUBSCRIBE); if ($forum->forcesubscribe <> FORUM_INITIALSUBSCRIBE) { // Reload the forum again to get the updated forcesubscribe field. $forum = $DB->get_record('forum', array('id' => $id), '*', MUST_EXIST); $users = \mod_forum\subscriptions::get_potential_subscribers($context, 0, 'u.id, u.email', ''); foreach ($users as $user) { \mod_forum\subscriptions::subscribe_user($user->id, $forum, $context); } } redirect( $subscribersurl, get_string('everyoneisnowsubscribed', 'forum'), null, \core\output\notification::NOTIFY_SUCCESS ); break; case FORUM_DISALLOWSUBSCRIBE : // 3 \mod_forum\subscriptions::set_subscription_mode($forum, FORUM_DISALLOWSUBSCRIBE); redirect( $subscribersurl, get_string('noonecansubscribenow', 'forum'), null, \core\output\notification::NOTIFY_SUCCESS ); break; default: throw new \moodle_exception(get_string('invalidforcesubscribe', 'forum')); } } if (\mod_forum\subscriptions::is_forcesubscribed($forum)) { $subscribersurl->param('notification', 'everyoneisnowsubscribed'); redirect($subscribersurl); } $info = new stdClass(); $info->name = fullname($user); $info->forum = format_string($forum->name); if ($issubscribed) { if (is_null($sesskey)) { // We came here via link in email. $PAGE->set_title($course->shortname); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); $viewurl = new moodle_url('/mod/forum/view.php', array('f' => $id)); if ($discussionid) { $a = new stdClass(); $a->forum = format_string($forum->name); $a->discussion = format_string($discussion->name); echo $OUTPUT->confirm(get_string('confirmunsubscribediscussion', 'forum', $a), $PAGE->url, $viewurl); } else { echo $OUTPUT->confirm(get_string('confirmunsubscribe', 'forum', format_string($forum->name)), $PAGE->url, $viewurl); } echo $OUTPUT->footer(); exit; } require_sesskey(); if ($discussionid === null) { if (\mod_forum\subscriptions::unsubscribe_user($user->id, $forum, $context, true)) { redirect( $returnto, get_string('nownotsubscribed', 'forum', $info), null, \core\output\notification::NOTIFY_SUCCESS ); } else { throw new \moodle_exception('cannotunsubscribe', 'forum', get_local_referer(false)); } } else { if (\mod_forum\subscriptions::unsubscribe_user_from_discussion($user->id, $discussion, $context)) { $info->discussion = $discussion->name; redirect( $returnto, get_string('discussionnownotsubscribed', 'forum', $info), null, \core\output\notification::NOTIFY_SUCCESS ); } else { throw new \moodle_exception('cannotunsubscribe', 'forum', get_local_referer(false)); } } } else { // subscribe if (\mod_forum\subscriptions::subscription_disabled($forum) && !has_capability('mod/forum:managesubscriptions', $context)) { throw new \moodle_exception('disallowsubscribe', 'forum', get_local_referer(false)); } if (!has_capability('mod/forum:viewdiscussion', $context)) { throw new \moodle_exception('noviewdiscussionspermission', 'forum', get_local_referer(false)); } if (is_null($sesskey)) { // We came here via link in email. $PAGE->set_title($course->shortname); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); $viewurl = new moodle_url('/mod/forum/view.php', array('f' => $id)); if ($discussionid) { $a = new stdClass(); $a->forum = format_string($forum->name); $a->discussion = format_string($discussion->name); echo $OUTPUT->confirm(get_string('confirmsubscribediscussion', 'forum', $a), $PAGE->url, $viewurl); } else { echo $OUTPUT->confirm(get_string('confirmsubscribe', 'forum', format_string($forum->name)), $PAGE->url, $viewurl); } echo $OUTPUT->footer(); exit; } require_sesskey(); if ($discussionid == null) { \mod_forum\subscriptions::subscribe_user($user->id, $forum, $context, true); redirect( $returnto, get_string('nowsubscribed', 'forum', $info), null, \core\output\notification::NOTIFY_SUCCESS ); } else { $info->discussion = $discussion->name; \mod_forum\subscriptions::subscribe_user_to_discussion($user->id, $discussion, $context); redirect( $returnto, get_string('discussionnowsubscribed', 'forum', $info), null, \core\output\notification::NOTIFY_SUCCESS ); } } PK-B\o %%subscribers.phpnu[. /** * This file is used to display and organise forum subscribers * * @package mod_forum * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once("../../config.php"); require_once("lib.php"); $id = required_param('id',PARAM_INT); // forum $group = optional_param('group',0,PARAM_INT); // change of group $edit = optional_param('edit',-1,PARAM_BOOL); // Turn editing on and off $url = new moodle_url('/mod/forum/subscribers.php', array('id'=>$id)); if ($group !== 0) { $url->param('group', $group); } if ($edit === 1) { $url->param('edit', 'on'); } else { $url->param('edit', 'off'); } $PAGE->set_url($url); $forum = $DB->get_record('forum', array('id'=>$id), '*', MUST_EXIST); $course = $DB->get_record('course', array('id'=>$forum->course), '*', MUST_EXIST); if (! $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id)) { $cm->id = 0; } require_login($course, false, $cm); $context = context_module::instance($cm->id); if (!has_capability('mod/forum:viewsubscribers', $context)) { throw new \moodle_exception('nopermissiontosubscribe', 'forum'); } unset($SESSION->fromdiscussion); $params = array( 'context' => $context, 'other' => array('forumid' => $forum->id), ); $event = \mod_forum\event\subscribers_viewed::create($params); $event->trigger(); $forumoutput = $PAGE->get_renderer('mod_forum'); $currentgroup = groups_get_activity_group($cm); $options = array('forumid'=>$forum->id, 'currentgroup'=>$currentgroup, 'context'=>$context); $existingselector = new mod_forum_existing_subscriber_selector('existingsubscribers', $options); $subscriberselector = new mod_forum_potential_subscriber_selector('potentialsubscribers', $options); $subscriberselector->set_existing_subscribers($existingselector->find_users('')); if (data_submitted()) { require_sesskey(); $subscribe = (bool)optional_param('subscribe', false, PARAM_RAW); $unsubscribe = (bool)optional_param('unsubscribe', false, PARAM_RAW); /** It has to be one or the other, not both or neither */ if (!($subscribe xor $unsubscribe)) { throw new \moodle_exception('invalidaction'); } if ($subscribe) { $users = $subscriberselector->get_selected_users(); foreach ($users as $user) { if (!\mod_forum\subscriptions::subscribe_user($user->id, $forum)) { throw new \moodle_exception('cannotaddsubscriber', 'forum', '', $user->id); } } } else if ($unsubscribe) { $users = $existingselector->get_selected_users(); foreach ($users as $user) { if (!\mod_forum\subscriptions::unsubscribe_user($user->id, $forum)) { throw new \moodle_exception('cannotremovesubscriber', 'forum', '', $user->id); } } } $subscriberselector->invalidate_selected_users(); $existingselector->invalidate_selected_users(); $subscriberselector->set_existing_subscribers($existingselector->find_users('')); } $strsubscribers = get_string("subscribers", "forum"); $PAGE->navbar->add($strsubscribers, $url); $PAGE->set_title($strsubscribers); $PAGE->set_heading($COURSE->fullname); // Activate the secondary nav tab. $PAGE->set_secondary_active_tab("forumsubscriptions"); // Output starts from here. $actionbar = new \mod_forum\output\subscription_actionbar($id, $url, $forum, $edit); $PAGE->activityheader->disable(); echo $OUTPUT->header(); if (!$PAGE->has_secondary_navigation()) { echo $OUTPUT->heading(get_string('forum', 'forum') . ' ' . $strsubscribers); } echo $forumoutput->subscription_actionbar($actionbar); if ($edit === 1 && !\mod_forum\subscriptions::is_forcesubscribed($forum)) { echo $OUTPUT->heading(get_string('managesubscriptionson', 'forum'), 2); echo $forumoutput->subscriber_selection_form($existingselector, $subscriberselector); } else { $subscribers = \mod_forum\subscriptions::fetch_subscribed_users($forum, $currentgroup, $context); if (\mod_forum\subscriptions::is_forcesubscribed($forum)) { $subscribers = mod_forum_filter_hidden_users($cm, $context, $subscribers); } echo $forumoutput->subscriber_overview($subscribers, $forum, $course); } echo $OUTPUT->footer(); /** * Filters a list of users for whether they can see a given activity. * If the course module is hidden (closed-eye icon), then only users who have * the permission to view hidden activities will appear in the output list. * * @todo MDL-48625 This filtering should be handled in core libraries instead. * * @param stdClass $cm the course module record of the activity. * @param context_module $context the activity context, to save re-fetching it. * @param array $users the list of users to filter. * @return array the filtered list of users. */ function mod_forum_filter_hidden_users(stdClass $cm, context_module $context, array $users) { if ($cm->visible) { return $users; } else { // Filter for users that can view hidden activities. $filteredusers = array(); $hiddenviewers = get_users_by_capability($context, 'moodle/course:viewhiddenactivities'); foreach ($hiddenviewers as $hiddenviewer) { if (array_key_exists($hiddenviewer->id, $users)) { $filteredusers[$hiddenviewer->id] = $users[$hiddenviewer->id]; } } return $filteredusers; } } PK-B\>[oOoO index.phpnu[. /** * @package mod_forum * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/course/lib.php'); require_once($CFG->dirroot . '/mod/forum/lib.php'); require_once($CFG->libdir . '/rsslib.php'); $id = optional_param('id', 0, PARAM_INT); // Course id $subscribe = optional_param('subscribe', null, PARAM_INT); // Subscribe/Unsubscribe all forums $url = new moodle_url('/mod/forum/index.php', array('id' => $id)); if ($subscribe !== null) { require_sesskey(); $url->param('subscribe', $subscribe); } $PAGE->set_url($url); $PAGE->set_secondary_active_tab('coursehome'); if ($id) { if (!$course = $DB->get_record('course', array('id' => $id))) { throw new \moodle_exception('invalidcourseid'); } } else { $course = get_site(); } require_course_login($course); $PAGE->set_pagelayout('incourse'); $coursecontext = context_course::instance($course->id); unset($SESSION->fromdiscussion); $params = array( 'context' => context_course::instance($course->id) ); $event = \mod_forum\event\course_module_instance_list_viewed::create($params); $event->add_record_snapshot('course', $course); $event->trigger(); $strforums = get_string('forums', 'forum'); $strforum = get_string('forum', 'forum'); $strdescription = get_string('description'); $strdiscussions = get_string('discussions', 'forum'); $strsubscribed = get_string('subscribed', 'forum'); $strunreadposts = get_string('unreadposts', 'forum'); $strtracking = get_string('tracking', 'forum'); $strmarkallread = get_string('markallread', 'forum'); $strtrackforum = get_string('trackforum', 'forum'); $strnotrackforum = get_string('notrackforum', 'forum'); $strsubscribe = get_string('subscribe', 'forum'); $strunsubscribe = get_string('unsubscribe', 'forum'); $stryes = get_string('yes'); $strno = get_string('no'); $strrss = get_string('rss'); $stremaildigest = get_string('emaildigest'); $searchform = forum_search_form($course); // Start of the table for General Forums. $generaltable = new html_table(); $generaltable->head = array ($strforum, $strdescription, $strdiscussions); $generaltable->align = array ('left', 'left', 'center'); if ($usetracking = forum_tp_can_track_forums()) { $untracked = forum_tp_get_untracked_forums($USER->id, $course->id); $generaltable->head[] = $strunreadposts; $generaltable->align[] = 'center'; $generaltable->head[] = $strtracking; $generaltable->align[] = 'center'; } // Fill the subscription cache for this course and user combination. \mod_forum\subscriptions::fill_subscription_cache_for_course($course->id, $USER->id); $usesections = course_format_uses_sections($course->format); $table = new html_table(); // Parse and organise all the forums. Most forums are course modules but // some special ones are not. These get placed in the general forums // category with the forums in section 0. $forums = $DB->get_records_sql(" SELECT f.*, d.maildigest FROM {forum} f LEFT JOIN {forum_digests} d ON d.forum = f.id AND d.userid = ? WHERE f.course = ? ", array($USER->id, $course->id)); $generalforums = array(); $learningforums = array(); $modinfo = get_fast_modinfo($course); $showsubscriptioncolumns = false; foreach ($modinfo->get_instances_of('forum') as $forumid => $cm) { if (!$cm->uservisible or !isset($forums[$forumid])) { continue; } $forum = $forums[$forumid]; if (!$context = context_module::instance($cm->id, IGNORE_MISSING)) { // Shouldn't happen. continue; } if (!has_capability('mod/forum:viewdiscussion', $context)) { // User can't view this one - skip it. continue; } // Determine whether subscription options should be displayed. $forum->cansubscribe = mod_forum\subscriptions::is_subscribable($forum); $forum->cansubscribe = $forum->cansubscribe || has_capability('mod/forum:managesubscriptions', $context); $forum->issubscribed = mod_forum\subscriptions::is_subscribed($USER->id, $forum, null, $cm); $showsubscriptioncolumns = $showsubscriptioncolumns || $forum->issubscribed || $forum->cansubscribe; // Fill two type array - order in modinfo is the same as in course. if ($forum->type == 'news' or $forum->type == 'social') { $generalforums[$forum->id] = $forum; } else if ($course->id == SITEID or empty($cm->sectionnum)) { $generalforums[$forum->id] = $forum; } else { $learningforums[$forum->id] = $forum; } } if ($showsubscriptioncolumns) { // The user can subscribe to at least one forum. $generaltable->head[] = $strsubscribed; $generaltable->align[] = 'center'; $generaltable->head[] = $stremaildigest . ' ' . $OUTPUT->help_icon('emaildigesttype', 'mod_forum'); $generaltable->align[] = 'center'; } if ($show_rss = (($showsubscriptioncolumns || $course->id == SITEID) && isset($CFG->enablerssfeeds) && isset($CFG->forum_enablerssfeeds) && $CFG->enablerssfeeds && $CFG->forum_enablerssfeeds)) { $generaltable->head[] = $strrss; $generaltable->align[] = 'center'; } // Do course wide subscribe/unsubscribe if requested if (!is_null($subscribe)) { if (isguestuser() or !$showsubscriptioncolumns) { // There should not be any links leading to this place, just redirect. redirect( new moodle_url('/mod/forum/index.php', array('id' => $id)), get_string('subscribeenrolledonly', 'forum'), null, \core\output\notification::NOTIFY_ERROR ); } // Can proceed now, the user is not guest and is enrolled foreach ($modinfo->get_instances_of('forum') as $forumid => $cm) { $forum = $forums[$forumid]; $modcontext = context_module::instance($cm->id); $cansub = false; if (has_capability('mod/forum:viewdiscussion', $modcontext)) { $cansub = true; } if ($cansub && $cm->visible == 0 && !has_capability('mod/forum:managesubscriptions', $modcontext)) { $cansub = false; } if (!\mod_forum\subscriptions::is_forcesubscribed($forum)) { $subscribed = \mod_forum\subscriptions::is_subscribed($USER->id, $forum, null, $cm); $canmanageactivities = has_capability('moodle/course:manageactivities', $coursecontext, $USER->id); if (($canmanageactivities || \mod_forum\subscriptions::is_subscribable($forum)) && $subscribe && !$subscribed && $cansub) { \mod_forum\subscriptions::subscribe_user($USER->id, $forum, $modcontext, true); } else if (!$subscribe && $subscribed) { \mod_forum\subscriptions::unsubscribe_user($USER->id, $forum, $modcontext, true); } } } $returnto = forum_go_back_to(new moodle_url('/mod/forum/index.php', array('id' => $course->id))); $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id))); if ($subscribe) { redirect( $returnto, get_string('nowallsubscribed', 'forum', $shortname), null, \core\output\notification::NOTIFY_SUCCESS ); } else { redirect( $returnto, get_string('nowallunsubscribed', 'forum', $shortname), null, \core\output\notification::NOTIFY_SUCCESS ); } } if ($generalforums) { // Process general forums. foreach ($generalforums as $forum) { $cm = $modinfo->instances['forum'][$forum->id]; $context = context_module::instance($cm->id); $count = forum_count_discussions($forum, $cm, $course); if ($usetracking) { if ($forum->trackingtype == FORUM_TRACKING_OFF) { $unreadlink = '-'; $trackedlink = '-'; } else { if (isset($untracked[$forum->id])) { $unreadlink = '-'; } else if ($unread = forum_tp_count_forum_unread_posts($cm, $course)) { $unreadlink = ''.$unread.''; $icon = $OUTPUT->pix_icon('t/markasread', $strmarkallread); $unreadlink .= '' . $icon . ''; } else { $unreadlink = '0'; } if (($forum->trackingtype == FORUM_TRACKING_FORCED) && ($CFG->forum_allowforcedreadtracking)) { $trackedlink = $stryes; } else if ($forum->trackingtype === FORUM_TRACKING_OFF || ($USER->trackforums == 0)) { $trackedlink = '-'; } else { $aurl = new moodle_url('/mod/forum/settracking.php', array( 'id' => $forum->id, 'sesskey' => sesskey(), )); if (!isset($untracked[$forum->id])) { $trackedlink = $OUTPUT->single_button($aurl, $stryes, 'post', array('title' => $strnotrackforum)); } else { $trackedlink = $OUTPUT->single_button($aurl, $strno, 'post', array('title' => $strtrackforum)); } } } } $forum->intro = shorten_text(format_module_intro('forum', $forum, $cm->id), $CFG->forum_shortpost); $forumname = format_string($forum->name, true); if ($cm->visible) { $style = ''; } else { $style = 'class="dimmed"'; } $forumlink = "id\" $style>".format_string($forum->name,true).""; $discussionlink = "id\" $style>".$count.""; $row = array ($forumlink, $forum->intro, $discussionlink); if ($usetracking) { $row[] = $unreadlink; $row[] = $trackedlink; // Tracking. } if ($showsubscriptioncolumns) { $row[] = forum_get_subscribe_link($forum, $context, array('subscribed' => $stryes, 'unsubscribed' => $strno, 'forcesubscribed' => $stryes, 'cantsubscribe' => '-'), false, false, true); $row[] = forum_index_get_forum_subscription_selector($forum); } // If this forum has RSS activated, calculate it. if ($show_rss) { if ($forum->rsstype and $forum->rssarticles) { //Calculate the tooltip text if ($forum->rsstype == 1) { $tooltiptext = get_string('rsssubscriberssdiscussions', 'forum'); } else { $tooltiptext = get_string('rsssubscriberssposts', 'forum'); } if (!isloggedin() && $course->id == SITEID) { $userid = guest_user()->id; } else { $userid = $USER->id; } //Get html code for RSS link $row[] = rss_get_link($context->id, $userid, 'mod_forum', $forum->id, $tooltiptext); } else { $row[] = ' '; } } $generaltable->data[] = $row; } } // Start of the table for Learning Forums $learningtable = new html_table(); $learningtable->head = array ($strforum, $strdescription, $strdiscussions); $learningtable->align = array ('left', 'left', 'center'); if ($usetracking) { $learningtable->head[] = $strunreadposts; $learningtable->align[] = 'center'; $learningtable->head[] = $strtracking; $learningtable->align[] = 'center'; } if ($showsubscriptioncolumns) { $learningtable->head[] = $strsubscribed; $learningtable->align[] = 'center'; $learningtable->head[] = $stremaildigest . ' ' . $OUTPUT->help_icon('emaildigesttype', 'mod_forum'); $learningtable->align[] = 'center'; } if ($show_rss = (($showsubscriptioncolumns || $course->id == SITEID) && isset($CFG->enablerssfeeds) && isset($CFG->forum_enablerssfeeds) && $CFG->enablerssfeeds && $CFG->forum_enablerssfeeds)) { $learningtable->head[] = $strrss; $learningtable->align[] = 'center'; } // Now let's process the learning forums. if ($course->id != SITEID) { // Only real courses have learning forums // 'format_.'$course->format only applicable when not SITEID (format_site is not a format) $strsectionname = course_get_format($course)->get_generic_section_name(); // Add extra field for section number, at the front array_unshift($learningtable->head, $strsectionname); array_unshift($learningtable->align, 'center'); if ($learningforums) { $currentsection = ''; foreach ($learningforums as $forum) { $cm = $modinfo->instances['forum'][$forum->id]; $context = context_module::instance($cm->id); $count = forum_count_discussions($forum, $cm, $course); if ($usetracking) { if ($forum->trackingtype == FORUM_TRACKING_OFF) { $unreadlink = '-'; $trackedlink = '-'; } else { if (isset($untracked[$forum->id])) { $unreadlink = '-'; } else if ($unread = forum_tp_count_forum_unread_posts($cm, $course)) { $unreadlink = ''.$unread.''; $icon = $OUTPUT->pix_icon('t/markasread', $strmarkallread); $unreadlink .= '' . $icon . ''; } else { $unreadlink = '0'; } if (($forum->trackingtype == FORUM_TRACKING_FORCED) && ($CFG->forum_allowforcedreadtracking)) { $trackedlink = $stryes; } else if ($forum->trackingtype === FORUM_TRACKING_OFF || ($USER->trackforums == 0)) { $trackedlink = '-'; } else { $aurl = new moodle_url('/mod/forum/settracking.php', array('id' => $forum->id)); if (!isset($untracked[$forum->id])) { $trackedlink = $OUTPUT->single_button($aurl, $stryes, 'post', array('title' => $strnotrackforum)); } else { $trackedlink = $OUTPUT->single_button($aurl, $strno, 'post', array('title' => $strtrackforum)); } } } } $forum->intro = shorten_text(format_module_intro('forum', $forum, $cm->id), $CFG->forum_shortpost); if ($cm->sectionnum != $currentsection) { $printsection = get_section_name($course, $cm->sectionnum); if ($currentsection) { $learningtable->data[] = 'hr'; } $currentsection = $cm->sectionnum; } else { $printsection = ''; } $forumname = format_string($forum->name,true); if ($cm->visible) { $style = ''; } else { $style = 'class="dimmed"'; } $forumlink = "id\" $style>".format_string($forum->name,true).""; $discussionlink = "id\" $style>".$count.""; $row = array ($printsection, $forumlink, $forum->intro, $discussionlink); if ($usetracking) { $row[] = $unreadlink; $row[] = $trackedlink; // Tracking. } if ($showsubscriptioncolumns) { $row[] = forum_get_subscribe_link($forum, $context, array('subscribed' => $stryes, 'unsubscribed' => $strno, 'forcesubscribed' => $stryes, 'cantsubscribe' => '-'), false, false, true); $row[] = forum_index_get_forum_subscription_selector($forum); } //If this forum has RSS activated, calculate it if ($show_rss) { if ($forum->rsstype and $forum->rssarticles) { //Calculate the tolltip text if ($forum->rsstype == 1) { $tooltiptext = get_string('rsssubscriberssdiscussions', 'forum'); } else { $tooltiptext = get_string('rsssubscriberssposts', 'forum'); } //Get html code for RSS link $row[] = rss_get_link($context->id, $USER->id, 'mod_forum', $forum->id, $tooltiptext); } else { $row[] = ' '; } } $learningtable->data[] = $row; } } } // Output the page. $PAGE->navbar->add($strforums); $PAGE->set_title("$course->shortname: $strforums"); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); echo html_writer::start_div('input-group me-5'); echo $searchform; echo html_writer::end_div(); if (!isguestuser() && isloggedin() && $showsubscriptioncolumns) { // Show the subscribe all options only to non-guest, enrolled users. echo $OUTPUT->box_start('subscription'); $subscriptionlink = new moodle_url('/mod/forum/index.php', [ 'id' => $course->id, 'sesskey' => sesskey(), ]); // Subscribe all. $subscriptionlink->param('subscribe', 1); echo html_writer::tag('div', html_writer::link($subscriptionlink, get_string('allsubscribe', 'forum')), [ 'class' => 'helplink', ]); // Unsubscribe all. $subscriptionlink->param('subscribe', 0); echo html_writer::tag('div', html_writer::link($subscriptionlink, get_string('allunsubscribe', 'forum')), [ 'class' => 'helplink', ]); echo $OUTPUT->box_end(); echo $OUTPUT->box(' ', 'clearer'); } if ($generalforums) { echo $OUTPUT->heading(get_string('generalforums', 'forum'), 2); echo html_writer::table($generaltable); } if ($learningforums) { echo $OUTPUT->heading(get_string('learningforums', 'forum'), 2); echo html_writer::table($learningtable); } echo $OUTPUT->footer(); /** * Get the content of the forum subscription options for this forum. * * @param stdClass $forum The forum to return options for * @return string */ function forum_index_get_forum_subscription_selector($forum) { global $OUTPUT, $PAGE; if ($forum->cansubscribe || $forum->issubscribed) { if ($forum->maildigest === null) { $forum->maildigest = -1; } $renderer = $PAGE->get_renderer('mod_forum'); return $OUTPUT->render($renderer->render_digest_options($forum, $forum->maildigest)); } else { // This user can subscribe to some forums. Add the empty fields. return ''; } }; PK-B\ij((amd/src/discussion.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Module for viewing a discussion. * * @module mod_forum/discussion * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define( [ 'jquery', 'core/custom_interaction_events', 'mod_forum/selectors', 'core/pubsub', 'mod_forum/forum_events', 'core/str', 'core/notification', ], function( $, CustomEvents, Selectors, PubSub, ForumEvents, String, Notification ) { /** * Set the focus on the previous post in the list. Previous post is calculated * based on position in list as viewed top to bottom. * * @param {Object} currentPost The post that currently has focus */ var focusPreviousPost = function(currentPost) { // See if there is a previous sibling post. var prevPost = currentPost.prev(Selectors.post.post); if (prevPost.length) { // The previous post might have replies that appear visually between // it and the current post (see nested view) so if that's the case // then the last reply will be the previous post in the list. var replyPost = prevPost.find(Selectors.post.post).last(); if (replyPost.length) { // Focus the last reply. replyPost.focus(); } else { // No replies so we can focus straight on the sibling. prevPost.focus(); } } else { // If there are no siblings then jump up the tree to the parent // post and focus the first parent post we find. currentPost.parents(Selectors.post.post).first().focus(); } }; /** * Set the focus on the next post in the list. Previous post is calculated * based on position in list as viewed top to bottom. * * @param {Object} currentPost The post that currently has focus */ var focusNextPost = function(currentPost) { // The next post in the visual list would be the first reply to this one // so let's see if we have one. var replyPost = currentPost.find(Selectors.post.post).first(); if (replyPost.length) { // Got a reply. replyPost.focus(); } else { // If we don't have a reply then the next post in the visual list would // be a sibling post (replying to the same parent). var siblingPost = currentPost.next(Selectors.post.post); if (siblingPost.length) { siblingPost.focus(); } else { // No siblings either. That means we're the lowest level reply in a thread // so we need to walk back up the tree of posts and find an ancestor post that // has a sibling post we can focus. var parentPosts = currentPost.parents(Selectors.post.post).toArray(); for (var i = 0; i < parentPosts.length; i++) { var ancestorSiblingPost = $(parentPosts[i]).next(Selectors.post.post); if (ancestorSiblingPost.length) { ancestorSiblingPost.focus(); break; } } } } }; /** * Check if the element is inside the in page reply section. * * @param {Object} element The element to check * @return {Boolean} */ var isElementInInPageReplySection = function(element) { var inPageReply = $(element).closest(Selectors.post.inpageReplyContent); return inPageReply.length ? true : false; }; /** * Initialise the keyboard accessibility controls for the discussion. * * @param {Object} root The discussion root element */ var initAccessibilityKeyboardNav = function(root) { var posts = root.find(Selectors.post.post); // Take each post action out of the tab index. posts.each(function(index, post) { var actions = $(post).find(Selectors.post.action); var firstAction = actions.first(); actions.attr('tabindex', '-1'); firstAction.attr('tabindex', 0); }); CustomEvents.define(root, [ CustomEvents.events.up, CustomEvents.events.down, CustomEvents.events.next, CustomEvents.events.previous, CustomEvents.events.home, CustomEvents.events.end, ]); root.on(CustomEvents.events.up, function(e, data) { var activeElement = document.activeElement; if (isElementInInPageReplySection(activeElement)) { // Focus is currently inside the in page reply section so don't move focus // to another post. return; } var focusPost = $(activeElement).closest(Selectors.post.post); if (focusPost.length) { focusPreviousPost(focusPost); } else { root.find(Selectors.post.post).first().focus(); } data.originalEvent.preventDefault(); }); root.on(CustomEvents.events.down, function(e, data) { var activeElement = document.activeElement; if (isElementInInPageReplySection(activeElement)) { // Focus is currently inside the in page reply section so don't move focus // to another post. return; } var focusPost = $(activeElement).closest(Selectors.post.post); if (focusPost.length) { focusNextPost(focusPost); } else { root.find(Selectors.post.post).first().focus(); } data.originalEvent.preventDefault(); }); root.on(CustomEvents.events.home, function(e, data) { if (isElementInInPageReplySection(document.activeElement)) { // Focus is currently inside the in page reply section so don't move focus // to another post. return; } root.find(Selectors.post.post).first().focus(); data.originalEvent.preventDefault(); }); root.on(CustomEvents.events.end, function(e, data) { if (isElementInInPageReplySection(document.activeElement)) { // Focus is currently inside the in page reply section so don't move focus // to another post. return; } root.find(Selectors.post.post).last().focus(); data.originalEvent.preventDefault(); }); root.on(CustomEvents.events.next, Selectors.post.action, function(e, data) { var currentAction = $(e.target); var container = currentAction.closest(Selectors.post.actionsContainer); var actions = container.find(Selectors.post.action); var nextAction = currentAction.next(Selectors.post.action); actions.attr('tabindex', '-1'); if (!nextAction.length) { nextAction = actions.first(); } nextAction.attr('tabindex', 0); nextAction.focus(); data.originalEvent.preventDefault(); }); root.on(CustomEvents.events.previous, Selectors.post.action, function(e, data) { var currentAction = $(e.target); var container = currentAction.closest(Selectors.post.actionsContainer); var actions = container.find(Selectors.post.action); var nextAction = currentAction.prev(Selectors.post.action); actions.attr('tabindex', '-1'); if (!nextAction.length) { nextAction = actions.last(); } nextAction.attr('tabindex', 0); nextAction.focus(); data.originalEvent.preventDefault(); }); root.on(CustomEvents.events.home, Selectors.post.action, function(e, data) { var currentAction = $(e.target); var container = currentAction.closest(Selectors.post.actionsContainer); var actions = container.find(Selectors.post.action); var firstAction = actions.first(); actions.attr('tabindex', '-1'); firstAction.attr('tabindex', 0); firstAction.focus(); e.stopPropagation(); data.originalEvent.preventDefault(); }); root.on(CustomEvents.events.end, Selectors.post.action, function(e, data) { var currentAction = $(e.target); var container = currentAction.closest(Selectors.post.actionsContainer); var actions = container.find(Selectors.post.action); var lastAction = actions.last(); actions.attr('tabindex', '-1'); lastAction.attr('tabindex', 0); lastAction.focus(); e.stopPropagation(); data.originalEvent.preventDefault(); }); PubSub.subscribe(ForumEvents.SUBSCRIPTION_TOGGLED, function(data) { var subscribed = data.subscriptionState; var updateMessage = subscribed ? 'discussionsubscribed' : 'discussionunsubscribed'; String.get_string(updateMessage, "forum") .then(function(s) { return Notification.addNotification({ message: s, type: "info" }); }) .catch(Notification.exception); }); }; return { init: function(root) { initAccessibilityKeyboardNav(root); } }; }); PK-B\e$amd/src/grades/expandconversation.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * This module handles the creation of a Modal that shows the user's post in context of the entire discussion. * * @module mod_forum/grades/expandconversation * @copyright 2019 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import * as ForumSelectors from './grader/selectors'; import Repository from 'mod_forum/repository'; import {exception as showException} from "core/notification"; import Templates from 'core/templates'; import Modal from 'core/modal_cancel'; import * as ModalEvents from 'core/modal_events'; /** * Find the Node containing the gradable details from the provided node by searching up the tree. * * @param {HTMLElement} node * @returns {HTMLElement} */ const findGradableNode = node => node.closest(ForumSelectors.expandConversation); /** * Show the post in context in a modal. * * @param {HTMLElement} rootNode The button that has been clicked * @param {object} param * @param {bool} [param.focusOnClose=null] */ const showPostInContext = async(rootNode, { focusOnClose = null, } = {}) => { const postId = rootNode.dataset.postid; const discussionId = rootNode.dataset.discussionid; const discussionName = rootNode.dataset.name; const experimentalDisplayMode = rootNode.dataset.experimentalDisplayMode == "1"; const [ allPosts, modal, ] = await Promise.all([ Repository.getDiscussionPosts(parseInt(discussionId)), Modal.create({ title: discussionName, large: true, removeOnClose: true, returnElement: focusOnClose, }), ]); const postsById = new Map(allPosts.posts.map(post => { post.readonly = true; post.hasreplies = false; post.replies = []; return [post.id, post]; })); let posts = []; allPosts.posts.forEach(post => { if (post.parentid) { const parent = postsById.get(post.parentid); if (parent) { post.parentauthorname = parent.author.fullname; parent.hasreplies = true; parent.replies.push(post); } else { posts.push(post); } } else { posts.push(post); } }); modal.getRoot().on(ModalEvents.bodyRendered, () => { const relevantPost = modal.getRoot()[0].querySelector(`#p${postId}`); if (relevantPost) { relevantPost.scrollIntoView({behavior: "smooth"}); } }); modal.show(); // Note: We do not use await here because it messes with the Modal transitions. const templatePromise = Templates.render('mod_forum/grades/grader/discussion/post_modal', { posts, experimentaldisplaymode: experimentalDisplayMode }); modal.setBody(templatePromise); }; /** * Register event listeners for the expand conversations button. * * @param {HTMLElement} rootNode The root to listen to. */ export const registerEventListeners = (rootNode) => { rootNode.addEventListener('click', (e) => { const rootNode = findGradableNode(e.target); if (rootNode) { e.preventDefault(); try { showPostInContext(rootNode, { focusOnClose: e.target, }); } catch (err) { showException(err); } } }); }; PK-B\jܐramd/src/grades/grader.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * This module will tie together all of the different calls the gradable module will make. * * @module mod_forum/grades/grader * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import * as Selectors from './grader/selectors'; import Repository from 'mod_forum/repository'; import Templates from 'core/templates'; import * as Grader from '../local/grades/grader'; import Notification from 'core/notification'; import CourseRepository from 'core_course/repository'; import {relativeUrl} from 'core/url'; const templateNames = { contentRegion: 'mod_forum/grades/grader/discussion/posts', }; /** * Curried function with CMID set, this is then used in unified grader as a fetch a users content. * * @param {Number} cmid * @param {Bool} experimentalDisplayMode * @return {Function} */ const getContentForUserIdFunction = (cmid, experimentalDisplayMode) => (userid) => { /** * Given the parent function is called with the second param set execute the partially executed function. * * @param {Number} userid */ return Repository.getDiscussionByUserID(userid, cmid) .then(context => { // Rebuild the returned data for the template. context.discussions = context.discussions.map(discussionPostMapper); context.experimentaldisplaymode = experimentalDisplayMode ? true : false; return Templates.render(templateNames.contentRegion, context); }) .catch(Notification.exception); }; /** * Curried function with CMID set, this is then used in unified grader as a fetch users call. * The function curried fetches all users in a course for a given CMID. * * @param {Number} courseID * @param {Number} groupID * @param {Boolean} onlyActive Whether to fetch only the active enrolled users or all enrolled users in the course. * @return {Array} Array of users for a given context. */ const getGradableUsersForCourseidFunction = (courseID, groupID, onlyActive) => async() => { const context = await CourseRepository.getGradableUsersFromCourseID(courseID, groupID, onlyActive); return context.users; }; const findGradableNode = node => node.closest(Selectors.gradableItem); /** * For a discussion we need to manipulate it's posts to hide certain UI elements. * * @param {Object} discussion * @return {Array} name, id, posts */ const discussionPostMapper = (discussion) => { // Map postid => post. const parentMap = new Map(); discussion.posts.parentposts.forEach(post => parentMap.set(post.id, post)); const userPosts = discussion.posts.userposts.map(post => { post.readonly = true; post.hasreplies = false; post.replies = []; const parent = post.parentid ? parentMap.get(post.parentid) : null; if (parent) { parent.hasreplies = false; parent.replies = []; parent.readonly = true; post.parentauthorname = parent.author.fullname; } return { parent, post }; }); return { ...discussion, posts: userPosts, }; }; /** * Launch the Grader. * * @param {HTMLElement} rootNode the root HTML element describing what is to be graded * @param {object} param * @param {bool} [param.focusOnClose=null] */ const launchWholeForumGrading = async(rootNode, { focusOnClose = null, } = {}) => { const data = rootNode.dataset; const gradingPanelFunctions = await Grader.getGradingPanelFunctions( 'mod_forum', data.contextid, data.gradingComponent, data.gradingComponentSubtype, data.gradableItemtype ); const groupID = data.group ? data.group : 0; const onlyActive = data.gradeOnlyActiveUsers; await Grader.launch( getGradableUsersForCourseidFunction(data.courseId, groupID, onlyActive), getContentForUserIdFunction(data.cmid, data.experimentalDisplayMode == "1"), gradingPanelFunctions.getter, gradingPanelFunctions.setter, { groupid: data.groupid, initialUserId: data.initialuserid, moduleName: data.name, courseName: data.courseName, courseUrl: relativeUrl('/course/view.php', {id: data.courseId}), sendStudentNotifications: data.sendStudentNotifications, focusOnClose, } ); }; /** * Launch the Grader. * * @param {HTMLElement} rootNode the root HTML element describing what is to be graded * @param {object} param * @param {bool} [param.focusOnClose=null] */ const launchViewGrading = async(rootNode, { focusOnClose = null, } = {}) => { const data = rootNode.dataset; const gradingPanelFunctions = await Grader.getGradingPanelFunctions( 'mod_forum', data.contextid, data.gradingComponent, data.gradingComponentSubtype, data.gradableItemtype ); await Grader.view( gradingPanelFunctions.getter, data.userid, data.name, { focusOnClose, } ); }; /** * Register listeners to launch the grading panel. */ export const registerLaunchListeners = () => { document.addEventListener('click', async(e) => { if (e.target.matches(Selectors.launch)) { const rootNode = findGradableNode(e.target); if (!rootNode) { throw Error('Unable to find a gradable item'); } if (rootNode.matches(Selectors.gradableItems.wholeForum)) { // Note: The preventDefault must be before any async function calls because the function becomes async // at that point and the default action is implemented. e.preventDefault(); try { await launchWholeForumGrading(rootNode, { focusOnClose: e.target, }); } catch (error) { Notification.exception(error); } } else { throw Error('Unable to find a valid gradable item'); } } if (e.target.matches(Selectors.viewGrade)) { e.preventDefault(); const rootNode = findGradableNode(e.target); if (!rootNode) { throw Error('Unable to find a gradable item'); } if (rootNode.matches(Selectors.gradableItems.wholeForum)) { // Note: The preventDefault must be before any async function calls because the function becomes async // at that point and the default action is implemented. e.preventDefault(); try { await launchViewGrading(rootNode, { focusOnClose: e.target, }); } catch (error) { Notification.exception(error); } } else { throw Error('Unable to find a valid gradable item'); } } }); }; PK-B\nC"amd/src/grades/grader/selectors.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * This module will tie together all of the different calls the gradable module will make. * * @module mod_forum/grades/grader/selectors * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ export default { launch: '[data-grade-action="launch"]', gradableItem: '[data-gradable-itemtype]', gradableItems: { wholeForum: '[data-gradable-itemtype="forum"]', }, expandConversation: '[data-action="view-context"]', posts: '[data-region="posts"]', viewGrade: '[data-grade-action="view"]', }; PK-B\P 'Z#Z#amd/src/inpage_reply.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * This module handles the in page replying to forum posts. * * @module mod_forum/inpage_reply * @copyright 2019 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core/templates', 'core/notification', 'mod_forum/repository', 'mod_forum/selectors', 'core_form/changechecker', ], function( $, Templates, Notification, Repository, Selectors, FormChangeChecker ) { var DISPLAYCONSTANTS = { NESTED_V2: 4, THREADED: 2, NESTED: 3, FLAT_OLDEST_FIRST: 1, FLAT_NEWEST_FIRST: -1 }; var EVENTS = { POST_CREATED: 'mod_forum-post-created' }; /** * Moodle formats taken from the FORMAT_* constants declared in lib/weblib.php. * @type {Object} */ var CONTENT_FORMATS = { MOODLE: 0 }; /** * Show the loading icon for the submit button. * * @param {Object} button The submit button element */ var showSubmitButtonLoadingIcon = function(button) { var textContainer = button.find(Selectors.post.inpageSubmitBtnText); var loadingIconContainer = button.find(Selectors.post.loadingIconContainer); var width = button.outerWidth(); // Fix the width so that the button size doesn't change when we show the loading icon. button.css('width', width); textContainer.addClass('hidden'); loadingIconContainer.removeClass('hidden'); }; /** * Hide the loading icon for the submit button. * * @param {Object} button The submit button element */ var hideSubmitButtonLoadingIcon = function(button) { var textContainer = button.find(Selectors.post.inpageSubmitBtnText); var loadingIconContainer = button.find(Selectors.post.loadingIconContainer); // Reset the width back to it's default. button.css('width', ''); textContainer.removeClass('hidden'); loadingIconContainer.addClass('hidden'); }; /** * Register the event listeners for the submit/cancel buttons of the in page reply. * * @param {Object} root The discussion container element. */ var registerEventListeners = function(root) { root.on('click', Selectors.post.inpageSubmitBtn, function(e) { e.preventDefault(); var submitButton = $(e.currentTarget); var allButtons = submitButton.parent().find(Selectors.post.inpageReplyButton); var form = submitButton.parents(Selectors.post.inpageReplyForm).get(0); var message = form.elements.post.value.trim(); // For now, we consider the inline reply post written using the FORMAT_MOODLE (because a textarea is displayed). // In the future, other formats should be supported, letting users to use their preferred editor and format. var messageformat = CONTENT_FORMATS.MOODLE; // The message post will be converted from messageformat to FORMAT_HTML. var topreferredformat = true; var postid = form.elements.reply.value; var subject = form.elements.subject.value; var currentRoot = submitButton.closest(Selectors.post.post); var isprivatereply = form.elements.privatereply != undefined ? form.elements.privatereply.checked : false; var modeSelector = root.find(Selectors.post.modeSelect); var mode = modeSelector.length ? parseInt(modeSelector.get(0).value) : null; var newid; if (message.length) { showSubmitButtonLoadingIcon(submitButton); allButtons.prop('disabled', true); Repository.addDiscussionPost(postid, subject, message, messageformat, isprivatereply, topreferredformat) .then(function(context) { var message = context.messages.reduce(function(carry, message) { if (message.type == 'success') { carry += '

' + message.message + '

'; } return carry; }, ''); Notification.addNotification({ message: message, type: "success" }); return context; }) .then(function(context) { form.reset(); var post = context.post; newid = post.id; switch (mode) { case DISPLAYCONSTANTS.NESTED_V2: var capabilities = post.capabilities; var currentAuthorName = currentRoot.children() .not(Selectors.post.repliesContainer) .find(Selectors.post.authorName) .text(); post.parentauthorname = currentAuthorName; post.showactionmenu = capabilities.view || capabilities.controlreadstatus || capabilities.edit || capabilities.split || capabilities.delete || capabilities.export || post.urls.viewparent; return Templates.render('mod_forum/forum_discussion_nested_v2_post_reply', post); case DISPLAYCONSTANTS.THREADED: return Templates.render('mod_forum/forum_discussion_threaded_post', post); case DISPLAYCONSTANTS.NESTED: return Templates.render('mod_forum/forum_discussion_nested_post', post); default: return Templates.render('mod_forum/forum_discussion_post', post); } }) .then(function(html, js) { var repliesnode = currentRoot.find(Selectors.post.repliesContainer).first(); if (mode == DISPLAYCONSTANTS.FLAT_NEWEST_FIRST) { return Templates.prependNodeContents(repliesnode, html, js); } else { return Templates.appendNodeContents(repliesnode, html, js); } }) .then(function() { submitButton.trigger(EVENTS.POST_CREATED, newid); hideSubmitButtonLoadingIcon(submitButton); allButtons.prop('disabled', false); // Tell formchangechecker we submitted the form. FormChangeChecker.resetFormDirtyState(submitButton[0]); return currentRoot.find(Selectors.post.inpageReplyContent).hide(); }) .then(function() { location.href = "#p" + newid; // Reload the page, say if threshold is being set by user those would get reflected through the templates. location.reload(); }) .catch(function(error) { hideSubmitButtonLoadingIcon(submitButton); allButtons.prop('disabled', false); return Notification.exception(error); }); } }); root.on('click', Selectors.post.inpageCancelButton, function(e) { // Tell formchangechecker to reset the form state. FormChangeChecker.resetFormDirtyState(e.currentTarget); }); }; return { init: function(root) { registerEventListeners(root); }, CONTENT_FORMATS: CONTENT_FORMATS, EVENTS: EVENTS }; }); PK-B\*0amd/src/local/grades/local/grader/user_picker.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * This module will tie together all of the different calls the gradable module will make. * * @module mod_forum/local/grades/local/grader/user_picker * @copyright 2019 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Templates from 'core/templates'; import Selectors from './user_picker/selectors'; import {getString} from 'core/str'; const templatePath = 'mod_forum/local/grades/local/grader'; /** * The Grader User Picker. * * @class mod_forum/local/grades/local/grader/user_picker */ class UserPicker { /** * Constructor for the User Picker. * * @constructor mod_forum/local/grades/local/grader/user_picker * @param {Array} userList List of users * @param {Function} showUserCallback The callback used to display the user * @param {Function} preChangeUserCallback The callback to use before changing user */ constructor(userList, showUserCallback, preChangeUserCallback) { this.userList = userList; this.showUserCallback = showUserCallback; this.preChangeUserCallback = preChangeUserCallback; this.currentUserIndex = 0; // Ensure that render is bound correctly. this.render = this.render.bind(this); this.setUserId = this.setUserId.bind(this); } /** * Set the current userid without rendering the change. * To show the user, call showUser too. * * @param {Number} userId */ setUserId(userId) { // Determine the current index based on the user ID. const userIndex = this.userList.findIndex(user => { return user.id === parseInt(userId); }); if (userIndex === -1) { throw Error(`User with id ${userId} not found`); } this.currentUserIndex = userIndex; } /** * Render the user picker. */ async render() { // Create the root node. this.root = document.createElement('div'); const {html, js} = await this.renderNavigator(); Templates.replaceNodeContents(this.root, html, js); // Call the showUser function to show the first user immediately. await this.showUser(this.currentUser); // Ensure that the event listeners are all bound. this.registerEventListeners(); } /** * Render the navigator itself. * * @returns {Promise} */ renderNavigator() { return Templates.renderForPromise(`${templatePath}/user_picker`, {}); } /** * Render the current user details for the picker. * * @param {Object} context The data used to render the user picker. * @returns {Promise} */ renderUserChange(context) { return Templates.renderForPromise(`${templatePath}/user_picker/user`, context); } /** * Show the specified user in the picker. * * @param {Object} user */ async showUser(user) { const [{html, js}] = await Promise.all([this.renderUserChange(user), this.showUserCallback(user)]); const userRegion = this.root.querySelector(Selectors.regions.userRegion); Templates.replaceNodeContents(userRegion, html, js); // Update the hidden now-grading region so screen readers can announce the user that's currently being graded. const currentUserRegion = this.root.querySelector(Selectors.regions.currentUser); currentUserRegion.textContent = await getString('nowgradinguser', 'mod_forum', user.fullname); } /** * Register the event listeners for the user picker. */ registerEventListeners() { this.root.addEventListener('click', async(e) => { const button = e.target.closest(Selectors.actions.changeUser); if (button) { const result = await this.preChangeUserCallback(this.currentUser); if (!result.failed) { this.updateIndex(parseInt(button.dataset.direction)); await this.showUser(this.currentUser); } } }); } /** * Update the current user index. * * @param {Number} direction * @returns {Number}} */ updateIndex(direction) { this.currentUserIndex += direction; // Loop around the edges. if (this.currentUserIndex < 0) { this.currentUserIndex = this.userList.length - 1; } else if (this.currentUserIndex > this.userList.length - 1) { this.currentUserIndex = 0; } return this.currentUserIndex; } /** * Get the details of the user currently shown with the total number of users, and the 1-indexed count of the * current user. * * @returns {Object} */ get currentUser() { return { ...this.userList[this.currentUserIndex], total: this.userList.length, displayIndex: this.currentUserIndex + 1, }; } /** * Get the root node for the User Picker. * * @returns {HTMLElement} */ get rootNode() { return this.root; } } /** * Create a new user picker. * * @param {Array} users The list of users * @param {Function} showUserCallback The function to call to show a specific user * @param {Function} preChangeUserCallback The fucntion to call to save the grade for the current user * @param {Number} [currentUserID] The userid of the current user * @returns {UserPicker} */ export default async( users, showUserCallback, preChangeUserCallback, { initialUserId = null, } = {} ) => { const userPicker = new UserPicker(users, showUserCallback, preChangeUserCallback); if (initialUserId) { userPicker.setUserId(initialUserId); } await userPicker.render(); return userPicker; }; PK-B\dñ:amd/src/local/grades/local/grader/user_picker/selectors.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Define all of the selectors we will be using on the grading interface. * * @module mod_forum/local/grades/local/grader/user_picker/selectors * @copyright 2019 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ export default { regions: { currentUser: '[data-region="user_picker/current_user"]', userRegion: '[data-region="user_picker/user"]', }, actions: { changeUser: '[data-action="change-user"]', } }; PK-B\U[8^ ^ .amd/src/local/grades/local/grader/selectors.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Define all of the selectors we will be using on the grading interface. * * @module mod_forum/local/grades/local/grader/selectors * @copyright 2019 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * A small helper function to build queryable data selectors. * @param {String} name * @param {String} value * @return {string} */ const getDataSelector = (name, value) => { return `[data-${name}="${value}"]`; }; export default { buttons: { toggleFullscreen: getDataSelector('action', 'togglefullscreen'), closeGrader: getDataSelector('action', 'closegrader'), collapseGradingDrawer: getDataSelector('action', 'collapse-grading-drawer'), saveGrade: getDataSelector('action', 'savegrade'), selectUser: getDataSelector('action', 'select-user'), toggleSearch: getDataSelector('action', 'toggle-search') }, regions: { bodyContainer: getDataSelector('region', 'body-container'), moduleContainer: getDataSelector('region', 'module_content_container'), moduleReplace: getDataSelector('region', 'module_content'), pickerRegion: getDataSelector('region', 'user_picker'), gradingInfoContainer: getDataSelector('region', 'grading-info-container'), gradingPanel: getDataSelector('region', 'grade'), gradingPanelContainer: getDataSelector('region', 'grading-panel-container'), gradingPanelErrors: getDataSelector('region', 'grade-errors'), searchResultsContainer: getDataSelector('region', 'search-results-container'), statusContainer: getDataSelector('region', 'status-container'), userSearchContainer: getDataSelector('region', 'user-search-container'), userSearchInput: getDataSelector('region', 'user-search-input') }, values: { sendStudentNotifications: '[data-region="notification"] input[type="radio"]:checked', } }; PK-B\xA1amd/src/local/grades/local/grader/gradingpanel.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Grading panel functions. * * @module mod_forum/local/grades/local/grader/gradingpanel * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Get the grade panel setter and getter for the current component. * This function dynamically pulls the relevant gradingpanel JS file defined in the grading method. * We do this because we do not know until execution time what the grading type is and we do not want to import unused files. * * @method * @param {String} component The component being graded * @param {Number} context The contextid of the thing being graded * @param {String} gradingComponent The thing providing the grading type * @param {String} gradingSubtype The subtype fo the grading component * @param {String} itemName The name of the thing being graded * @return {Object} */ export default async(component, context, gradingComponent, gradingSubtype, itemName) => { let gradingMethodHandler = `${gradingComponent}/grades/grader/gradingpanel`; if (gradingSubtype) { gradingMethodHandler += `/${gradingSubtype}`; } const GradingMethod = await import(gradingMethodHandler); return { getter: (userId) => GradingMethod.fetchCurrentGrade(component, context, itemName, userId), setter: (userId, notifyStudent, formData) => GradingMethod.storeCurrentGrade( component, context, itemName, userId, notifyStudent, formData), }; }; PK-B\Q?NNamd/src/local/grades/grader.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * This module will tie together all of the different calls the gradable module will make. * * @module mod_forum/local/grades/grader * @copyright 2019 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Templates from 'core/templates'; import Selectors from './local/grader/selectors'; import getUserPicker from './local/grader/user_picker'; import {createLayout as createFullScreenWindow} from 'mod_forum/local/layout/fullscreen'; import getGradingPanelFunctions from './local/grader/gradingpanel'; import {add as addToast} from 'core/toast'; import {addNotification} from 'core/notification'; import {getString} from 'core/str'; import {failedUpdate} from 'core_grades/grades/grader/gradingpanel/normalise'; import {addIconToContainerWithPromise} from 'core/loadingicon'; import {debounce} from 'core/utils'; import {fillInitialValues} from 'core_grades/grades/grader/gradingpanel/comparison'; import Modal from 'core/modal_cancel'; import {subscribe} from 'core/pubsub'; import DrawerEvents from 'core/drawer_events'; const templateNames = { grader: { app: 'mod_forum/local/grades/grader', gradingPanel: { error: 'mod_forum/local/grades/local/grader/gradingpanel/error', }, searchResults: 'mod_forum/local/grades/local/grader/user_picker/user_search', status: 'mod_forum/local/grades/local/grader/status', }, }; /** * Helper function that replaces the user picker placeholder with what we get back from the user picker class. * * @param {HTMLElement} root * @param {String} html */ const displayUserPicker = (root, html) => { const pickerRegion = root.querySelector(Selectors.regions.pickerRegion); Templates.replaceNodeContents(pickerRegion, html, ''); }; /** * To be removed, this is now done as a part of Templates.renderForPromise() * * @param {String} html * @param {String} js * @returns {array} An array containing the HTML, and JS. */ const fetchContentFromRender = (html, js) => { return [html, js]; }; /** * Here we build the function that is passed to the user picker that'll handle updating the user content area * of the grading interface. * * @param {HTMLElement} root * @param {Function} getContentForUser * @param {Function} getGradeForUser * @param {Function} saveGradeForUser * @return {Function} */ const getUpdateUserContentFunction = (root, getContentForUser, getGradeForUser, saveGradeForUser) => { let firstLoad = true; return async(user) => { const spinner = firstLoad ? null : addIconToContainerWithPromise(root); const [ [html, js], userGrade, ] = await Promise.all([ getContentForUser(user.id).then(fetchContentFromRender), getGradeForUser(user.id), ]); Templates.replaceNodeContents(root.querySelector(Selectors.regions.moduleReplace), html, js); const [ gradingPanelHtml, gradingPanelJS ] = await Templates.render(userGrade.templatename, userGrade.grade).then(fetchContentFromRender); const panelContainer = root.querySelector(Selectors.regions.gradingPanelContainer); const panel = panelContainer.querySelector(Selectors.regions.gradingPanel); Templates.replaceNodeContents(panel, gradingPanelHtml, gradingPanelJS); const form = panel.querySelector('form'); fillInitialValues(form); form.addEventListener('submit', event => { saveGradeForUser(user); event.preventDefault(); }); panelContainer.scrollTop = 0; firstLoad = false; if (spinner) { spinner.resolve(); } return userGrade; }; }; /** * Show the search results container and hide the user picker and body content. * * @param {HTMLElement} bodyContainer The container element for the body content * @param {HTMLElement} userPickerContainer The container element for the user picker * @param {HTMLElement} searchResultsContainer The container element for the search results */ const showSearchResultContainer = (bodyContainer, userPickerContainer, searchResultsContainer) => { bodyContainer.classList.add('hidden'); userPickerContainer.classList.add('hidden'); searchResultsContainer.classList.remove('hidden'); }; /** * Hide the search results container and show the user picker and body content. * * @param {HTMLElement} bodyContainer The container element for the body content * @param {HTMLElement} userPickerContainer The container element for the user picker * @param {HTMLElement} searchResultsContainer The container element for the search results */ const hideSearchResultContainer = (bodyContainer, userPickerContainer, searchResultsContainer) => { bodyContainer.classList.remove('hidden'); userPickerContainer.classList.remove('hidden'); searchResultsContainer.classList.add('hidden'); }; /** * Toggles the visibility of the user search. * * @param {HTMLElement} toggleSearchButton The button that toggles the search * @param {HTMLElement} searchContainer The container element for the user search * @param {HTMLElement} searchInput The input element for searching */ const showUserSearchInput = (toggleSearchButton, searchContainer, searchInput) => { searchContainer.classList.remove('collapsed'); toggleSearchButton.setAttribute('aria-expanded', 'true'); toggleSearchButton.classList.add('expand'); toggleSearchButton.classList.remove('collapse'); // Hide the grading info container from screen reader. const gradingInfoContainer = searchContainer.parentElement.querySelector(Selectors.regions.gradingInfoContainer); gradingInfoContainer.setAttribute('aria-hidden', 'true'); // Hide the collapse grading drawer button from screen reader. const collapseGradingDrawer = searchContainer.parentElement.querySelector(Selectors.buttons.collapseGradingDrawer); collapseGradingDrawer.setAttribute('aria-hidden', 'true'); collapseGradingDrawer.setAttribute('tabindex', '-1'); searchInput.focus(); }; /** * Toggles the visibility of the user search. * * @param {HTMLElement} toggleSearchButton The button that toggles the search * @param {HTMLElement} searchContainer The container element for the user search * @param {HTMLElement} searchInput The input element for searching */ const hideUserSearchInput = (toggleSearchButton, searchContainer, searchInput) => { searchContainer.classList.add('collapsed'); toggleSearchButton.setAttribute('aria-expanded', 'false'); toggleSearchButton.classList.add('collapse'); toggleSearchButton.classList.remove('expand'); toggleSearchButton.focus(); // Show the grading info container to screen reader. const gradingInfoContainer = searchContainer.parentElement.querySelector(Selectors.regions.gradingInfoContainer); gradingInfoContainer.removeAttribute('aria-hidden'); // Show the collapse grading drawer button from screen reader. const collapseGradingDrawer = searchContainer.parentElement.querySelector(Selectors.buttons.collapseGradingDrawer); collapseGradingDrawer.removeAttribute('aria-hidden'); collapseGradingDrawer.setAttribute('tabindex', '0'); searchInput.value = ''; }; /** * Find the list of users who's names include the given search term. * * @param {Array} userList List of users for the grader * @param {String} searchTerm The search term to match * @return {Array} */ const searchForUsers = (userList, searchTerm) => { if (searchTerm === '') { return userList; } searchTerm = searchTerm.toLowerCase(); return userList.filter((user) => { return user.fullname.toLowerCase().includes(searchTerm); }); }; /** * Render the list of users in the search results area. * * @param {HTMLElement} searchResultsContainer The container element for search results * @param {Array} users The list of users to display */ const renderSearchResults = async(searchResultsContainer, users) => { const {html, js} = await Templates.renderForPromise(templateNames.grader.searchResults, {users}); Templates.replaceNodeContents(searchResultsContainer, html, js); }; /** * Add click handlers to the buttons in the header of the grading interface. * * @param {HTMLElement} graderLayout * @param {Object} userPicker * @param {Function} saveGradeFunction * @param {Array} userList List of users for the grader. */ const registerEventListeners = (graderLayout, userPicker, saveGradeFunction, userList) => { const graderContainer = graderLayout.getContainer(); const toggleSearchButton = graderContainer.querySelector(Selectors.buttons.toggleSearch); const searchInputContainer = graderContainer.querySelector(Selectors.regions.userSearchContainer); const searchInput = searchInputContainer.querySelector(Selectors.regions.userSearchInput); const bodyContainer = graderContainer.querySelector(Selectors.regions.bodyContainer); const userPickerContainer = graderContainer.querySelector(Selectors.regions.pickerRegion); const searchResultsContainer = graderContainer.querySelector(Selectors.regions.searchResultsContainer); graderContainer.addEventListener('click', (e) => { if (e.target.closest(Selectors.buttons.toggleFullscreen)) { e.stopImmediatePropagation(); e.preventDefault(); graderLayout.toggleFullscreen(); return; } if (e.target.closest(Selectors.buttons.closeGrader)) { e.stopImmediatePropagation(); e.preventDefault(); graderLayout.close(); return; } if (e.target.closest(Selectors.buttons.saveGrade)) { saveGradeFunction(userPicker.currentUser); } if (e.target.closest(Selectors.buttons.toggleSearch)) { if (toggleSearchButton.getAttribute('aria-expanded') === 'true') { // Search is open so let's close it. hideUserSearchInput(toggleSearchButton, searchInputContainer, searchInput); hideSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer); searchResultsContainer.innerHTML = ''; } else { // Search is closed so let's open it. showUserSearchInput(toggleSearchButton, searchInputContainer, searchInput); showSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer); renderSearchResults(searchResultsContainer, userList); } return; } const selectUserButton = e.target.closest(Selectors.buttons.selectUser); if (selectUserButton) { const userId = selectUserButton.getAttribute('data-userid'); const user = userList.find(user => user.id == userId); userPicker.setUserId(userId); userPicker.showUser(user); hideUserSearchInput(toggleSearchButton, searchInputContainer, searchInput); hideSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer); searchResultsContainer.innerHTML = ''; } }); // Debounce the search input so that it only executes 300 milliseconds after the user has finished typing. searchInput.addEventListener('input', debounce(() => { const users = searchForUsers(userList, searchInput.value); renderSearchResults(searchResultsContainer, users); }, 300)); // Remove the right margin of the content container when the grading panel is hidden so that it expands to full-width. subscribe(DrawerEvents.DRAWER_HIDDEN, (drawerRoot) => { const gradingPanel = drawerRoot[0]; if (gradingPanel.querySelector(Selectors.regions.gradingPanel)) { setContentContainerMargin(graderContainer, 0); } }); // Bring back the right margin of the content container when the grading panel is shown to give space for the grading panel. subscribe(DrawerEvents.DRAWER_SHOWN, (drawerRoot) => { const gradingPanel = drawerRoot[0]; if (gradingPanel.querySelector(Selectors.regions.gradingPanel)) { setContentContainerMargin(graderContainer, gradingPanel.offsetWidth); } }); }; /** * Adjusts the right margin of the content container. * * @param {HTMLElement} graderContainer The container for the grader app. * @param {Number} rightMargin The right margin value. */ const setContentContainerMargin = (graderContainer, rightMargin) => { const contentContainer = graderContainer.querySelector(Selectors.regions.moduleContainer); if (contentContainer) { contentContainer.style.marginRight = `${rightMargin}px`; } }; /** * Get the function used to save a user grade. * * @param {HTMLElement} root The container for the grader * @param {Function} setGradeForUser The function that will be called. * @return {Function} */ const getSaveUserGradeFunction = (root, setGradeForUser) => { return async(user) => { try { root.querySelector(Selectors.regions.gradingPanelErrors).innerHTML = ''; const result = await setGradeForUser( user.id, root.querySelector(Selectors.values.sendStudentNotifications).value, root.querySelector(Selectors.regions.gradingPanel) ); if (result.success) { addToast(await getString('grades:gradesavedfor', 'mod_forum', user)); } if (result.failed) { displayGradingError(root, user, result.error); } return result; } catch (err) { displayGradingError(root, user, err); return failedUpdate(err); } }; }; /** * Display a grading error, typically from a failed save. * * @param {HTMLElement} root The container for the grader * @param {Object} user The user who was errored * @param {Object} err The details of the error */ const displayGradingError = async(root, user, err) => { const [ {html, js}, errorString ] = await Promise.all([ Templates.renderForPromise(templateNames.grader.gradingPanel.error, {error: err}), await getString('grades:gradesavefailed', 'mod_forum', {error: err.message, ...user}), ]); Templates.replaceNodeContents(root.querySelector(Selectors.regions.gradingPanelErrors), html, js); addToast(errorString, {type: 'warning'}); }; /** * Launch the grader interface with the specified parameters. * * @param {Function} getListOfUsers A function to get the list of users * @param {Function} getContentForUser A function to get the content for a specific user * @param {Function} getGradeForUser A function get the grade details for a specific user * @param {Function} setGradeForUser A function to set the grade for a specific user * @param {Object} preferences Preferences for the launch function * @param {Number} preferences.initialUserId * @param {string} preferences.moduleName * @param {string} preferences.courseName * @param {string} preferences.courseUrl * @param {boolean} preferences.sendStudentNotifications * @param {null|HTMLElement} preferences.focusOnClose */ export const launch = async(getListOfUsers, getContentForUser, getGradeForUser, setGradeForUser, { initialUserId = null, moduleName, courseName, courseUrl, sendStudentNotifications, focusOnClose = null, } = {}) => { // We need all of these functions to be executed in series, if one step runs before another the interface // will not work. // We need this promise to resolve separately so that we can avoid loading the whole interface if there are no users. const userList = await getListOfUsers(); if (!userList.length) { addNotification({ message: await getString('nouserstograde', 'core_grades'), type: "error", }); return; } // Now that we have confirmed there are at least some users let's boot up the grader interface. const [ graderLayout, {html, js}, ] = await Promise.all([ createFullScreenWindow({ fullscreen: false, showLoader: false, focusOnClose, }), Templates.renderForPromise(templateNames.grader.app, { moduleName, courseName, courseUrl, drawer: {show: true}, defaultsendnotifications: sendStudentNotifications, }), ]); const graderContainer = graderLayout.getContainer(); const saveGradeFunction = getSaveUserGradeFunction(graderContainer, setGradeForUser); Templates.replaceNodeContents(graderContainer, html, js); const updateUserContent = getUpdateUserContentFunction(graderContainer, getContentForUser, getGradeForUser, saveGradeFunction); const userIds = userList.map(user => user.id); const statusContainer = graderContainer.querySelector(Selectors.regions.statusContainer); // Fetch the userpicker for display. const userPicker = await getUserPicker( userList, async(user) => { const userGrade = await updateUserContent(user); const renderContext = { status: userGrade.hasgrade, index: userIds.indexOf(user.id) + 1, total: userList.length }; Templates.render(templateNames.grader.status, renderContext).then(html => { statusContainer.innerHTML = html; return html; }).catch(); }, saveGradeFunction, { initialUserId, }, ); // Register all event listeners. registerEventListeners(graderLayout, userPicker, saveGradeFunction, userList); // Display the newly created user picker. displayUserPicker(graderContainer, userPicker.rootNode); }; /** * Show the grade for a specific user. * * @param {Function} getGradeForUser A function get the grade details for a specific user * @param {Number} userid The ID of a specific user * @param {String} moduleName the name of the module * @param {object} param * @param {null|HTMLElement} param.focusOnClose */ export const view = async(getGradeForUser, userid, moduleName, { focusOnClose = null, } = {}) => { const userGrade = await getGradeForUser(userid); const [ modal, gradeTemplateData ] = await Promise.all([ Modal.create({ title: moduleName, large: true, removeOnClose: true, returnElement: focusOnClose, show: true, body: Templates.render('mod_forum/local/grades/view_grade', userGrade), }), renderGradeTemplate(userGrade) ]); const bodyPromise = await modal.getBodyPromise(); const gradeReplace = bodyPromise[0].querySelector('[data-region="grade-template"]'); Templates.replaceNodeContents(gradeReplace, gradeTemplateData.html, gradeTemplateData.js); }; const renderGradeTemplate = (userGrade) => Templates.renderForPromise(userGrade.templatename, userGrade.grade); export {getGradingPanelFunctions}; PK-B\xttamd/src/local/layouts.jsnu[import {createLayout as createFullScreenWindow} from './layout/fullscreen'; export { createFullScreenWindow }; PK-B\X֮=="amd/src/local/layout/fullscreen.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Full screen window layout. * * @module mod_forum/local/layout/fullscreen * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import {addIconToContainer} from 'core/loadingicon'; import {addToastRegion} from 'core/toast'; import * as FocusLockManager from 'core/local/aria/focuslock'; /** * Get the composed layout. * * @method * @param {string} templateName * @param {object} context * @returns {LayoutHelper} */ export const createLayout = ({ fullscreen = true, showLoader = false, focusOnClose = null, } = {}) => { const container = document.createElement('div'); document.body.append(container); container.classList.add('layout'); container.classList.add('fullscreen'); container.setAttribute('role', 'application'); addToastRegion(container); // Lock scrolling on the document body. lockBodyScroll(); // Lock tab control. FocusLockManager.trapFocus(container); const helpers = getLayoutHelpers(container, FocusLockManager, focusOnClose); if (showLoader) { helpers.showLoadingIcon(); } if (fullscreen) { helpers.requestFullscreen(); } return helpers; }; /** * LayoutHelper A helper object containing functions for managing the current fullscreen layout * * @typedef {object} * @property {Function} close A function to close the fullscreen layout * @property {Function} toggleFullscreen A function to toggle the fullscreen from active to disabled and back * @property {Function} requestFullscreen Make a request to the browser to make the window full screen. * Note: This must be called in response to a direct user action * @property {Function} exitFullscreen Exit the fullscreen mode * @property {Function} getContainer Get the container of the fullscreen layout * @property {Function} setContent Set the content of the fullscreen layout * @property {Function} showLoadingIcon Display the loading icon * @property {Function} hideLoadingIcon Hide the loading icon */ /** * Get the layout helpers. * * @method * @private * @param {HTMLElement} layoutNode * @param {FocusLockManager} FocusLockManager * @param {Boolean} focusOnClose * @returns {LayoutHelper} */ const getLayoutHelpers = (layoutNode, FocusLockManager, focusOnClose) => { const contentNode = document.createElement('div'); layoutNode.append(contentNode); const loadingNode = document.createElement('div'); layoutNode.append(loadingNode); /** * Close and destroy the window container. */ const close = () => { exitFullscreen(); unlockBodyScroll(); FocusLockManager.untrapFocus(); layoutNode.remove(); if (focusOnClose) { try { focusOnClose.focus(); } catch (e) { // eslint-disable-line } } }; /** * Attempt to make the conatiner full screen. */ const requestFullscreen = () => { if (layoutNode.requestFullscreen) { layoutNode.requestFullscreen(); } else if (layoutNode.msRequestFullscreen) { layoutNode.msRequestFullscreen(); } else if (layoutNode.mozRequestFullscreen) { layoutNode.mozRequestFullscreen(); } else if (layoutNode.webkitRequestFullscreen) { layoutNode.webkitRequestFullscreen(); } else { // Not supported. // Hack to make this act like full-screen as much as possible. layoutNode.setTop(0); } }; /** * Exit full screen but do not close the container fully. */ const exitFullscreen = () => { if (document.exitRequestFullScreen) { if (document.fullScreenElement !== layoutNode) { return; } document.exitRequestFullScreen(); } else if (document.msExitFullscreen) { if (document.msFullscreenElement !== layoutNode) { return; } document.msExitFullscreen(); } else if (document.mozCancelFullScreen) { if (document.mozFullScreenElement !== layoutNode) { return; } document.mozCancelFullScreen(); } else if (document.webkitExitFullscreen) { if (document.webkitFullscreenElement !== layoutNode) { return; } document.webkitExitFullscreen(); } }; const toggleFullscreen = () => { if (document.exitRequestFullScreen) { if (document.fullScreenElement === layoutNode) { exitFullscreen(); } else { requestFullscreen(); } } else if (document.msExitFullscreen) { if (document.msFullscreenElement === layoutNode) { exitFullscreen(); } else { requestFullscreen(); } } else if (document.mozCancelFullScreen) { if (document.mozFullScreenElement === layoutNode) { exitFullscreen(); } else { requestFullscreen(); } } else if (document.webkitExitFullscreen) { if (document.webkitFullscreenElement === layoutNode) { exitFullscreen(); } else { requestFullscreen(); } } }; /** * Get the Node which is fullscreen. * * @return {Element} */ const getContainer = () => { return contentNode; }; const setContent = (content) => { hideLoadingIcon(); // Note: It would be better to use replaceWith, but this is not compatible with IE. let child = contentNode.lastElementChild; while (child) { contentNode.removeChild(child); child = contentNode.lastElementChild; } contentNode.append(content); }; const showLoadingIcon = () => { addIconToContainer(loadingNode); }; const hideLoadingIcon = () => { // Hide the loading container. let child = loadingNode.lastElementChild; while (child) { loadingNode.removeChild(child); child = loadingNode.lastElementChild; } }; /** * @return {Object} */ return { close, toggleFullscreen, requestFullscreen, exitFullscreen, getContainer, setContent, showLoadingIcon, hideLoadingIcon, }; }; const lockBodyScroll = () => { document.querySelector('body').classList.add('overflow-hidden'); }; const unlockBodyScroll = () => { document.querySelector('body').classList.remove('overflow-hidden'); }; PK-B\+amd/src/discussion_list.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Module for the list of discussions on when viewing a forum. * * @module mod_forum/discussion_list * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core/templates', 'core/str', 'core/notification', 'mod_forum/subscription_toggle', 'mod_forum/selectors', 'mod_forum/repository', 'core/pubsub', 'mod_forum/forum_events', 'core_form/changechecker', ], function( $, Templates, Str, Notification, SubscriptionToggle, Selectors, Repository, PubSub, ForumEvents, FormChangeChecker ) { var registerEventListeners = function(root) { PubSub.subscribe(ForumEvents.SUBSCRIPTION_TOGGLED, function(data) { var discussionId = data.discussionId; var subscribed = data.subscriptionState; var discussionListItem = root.find(Selectors.discussion.item + '[data-discussionid= ' + discussionId + ']'); var subscribedLabel = discussionListItem.find(Selectors.discussion.subscribedLabel); if (subscribed) { discussionListItem.addClass('subscribed'); subscribedLabel.removeAttr('hidden'); } else { discussionListItem.removeClass('subscribed'); subscribedLabel.attr('hidden', true); } }); root.on('click', Selectors.post.inpageCancelButton, function(e) { // Tell formchangechecker to reset the form state. FormChangeChecker.resetFormDirtyState(e.currentTarget); }); root.on('click', Selectors.favourite.toggle, function(e) { e.preventDefault(); var toggleElement = $(this); var forumId = toggleElement.data('forumid'); var discussionId = toggleElement.data('discussionid'); var subscriptionState = toggleElement.data('targetstate'); Repository.setFavouriteDiscussionState(forumId, discussionId, subscriptionState) .then(function() { return location.reload(); }) .catch(Notification.exception); }); root.on('click', Selectors.pin.toggle, function(e) { e.preventDefault(); var toggleElement = $(this); var forumId = toggleElement.data('forumid'); var discussionId = toggleElement.data('discussionid'); var state = toggleElement.data('targetstate'); Repository.setPinDiscussionState(forumId, discussionId, state) .then(function() { return location.reload(); }) .catch(Notification.exception); }); root.on('click', Selectors.lock.toggle, function(e) { var toggleElement = $(this); var forumId = toggleElement.data('forumid'); var discussionId = toggleElement.data('discussionid'); var state = toggleElement.data('state'); Repository.setDiscussionLockState(forumId, discussionId, state) .then(function(context) { var icon = toggleElement.parents(Selectors.summary.actions).find(Selectors.lock.icon); var lockedLabel = toggleElement.parents(Selectors.discussion.item).find(Selectors.discussion.lockedLabel); if (context.locked) { icon.removeClass('hidden'); lockedLabel.removeAttr('hidden'); } else { icon.addClass('hidden'); lockedLabel.attr('hidden', true); } return context; }) .then(function(context) { context.forumid = forumId; return Templates.render('mod_forum/discussion_lock_toggle', context); }) .then(function(html, js) { return Templates.replaceNode(toggleElement, html, js); }) .then(function() { return Str.get_string('lockupdated', 'forum') .done(function(s) { return Notification.addNotification({ message: s, type: "info" }); }); }) .catch(Notification.exception); e.preventDefault(); }); }; return { init: function(root) { SubscriptionToggle.init(root, false, function(toggleElement, context) { var toggleId = toggleElement.attr('id'); var newTargetState = context.userstate.subscribed ? 0 : 1; toggleElement.data('targetstate', newTargetState); var stringKey = context.userstate.subscribed ? 'unsubscribediscussion' : 'subscribediscussion'; return Str.get_string(stringKey, 'mod_forum') .then(function(string) { toggleElement.closest('td').find('label[for="' + toggleId + '"]').find('span').text(string); return string; }); }); registerEventListeners(root); } }; }); PK-B\^!_; ; amd/src/subscription_toggle.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Handle discussion subscription toggling on a discussion list in * the forum view. * * @module mod_forum/subscription_toggle * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core/templates', 'core/notification', 'mod_forum/repository', 'mod_forum/selectors', 'core/pubsub', 'mod_forum/forum_events', ], function( $, Templates, Notification, Repository, Selectors, PubSub, ForumEvents ) { /** * Register event listeners for the subscription toggle. * * @param {object} root The discussion list root element * @param {boolean} preventDefault Should the default action of the event be prevented * @param {function} callback Success callback */ var registerEventListeners = function(root, preventDefault, callback) { root.on('click', Selectors.subscription.toggle, function(e) { var toggleElement = $(this); var forumId = toggleElement.data('forumid'); var discussionId = toggleElement.data('discussionid'); var subscriptionState = toggleElement.data('targetstate'); Repository.setDiscussionSubscriptionState(forumId, discussionId, subscriptionState) .then(function(context) { PubSub.publish(ForumEvents.SUBSCRIPTION_TOGGLED, { discussionId: discussionId, subscriptionState: subscriptionState }); return callback(toggleElement, context); }) .catch(Notification.exception); if (preventDefault) { e.preventDefault(); } }); }; return { init: registerEventListeners }; }); PK-B\5F F amd/src/pin_toggle.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * This module is the highest level module for the calendar. It is * responsible for initialising all of the components required for * the calendar to run. It also coordinates the interaction between * components by listening for and responding to different events * triggered within the calendar UI. * * @module mod_forum/pin_toggle * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core/ajax', 'core/str', 'core/templates', 'core/notification', 'mod_forum/repository', 'mod_forum/selectors', 'core/str', ], function( $, Ajax, Str, Templates, Notification, Repository, Selectors, String ) { /** * Registery event listeners for the pin toggle. * * @param {object} root The calendar root element * @param {boolean} preventDefault Should the default action of the event be prevented * @param {function} callback Success callback */ var registerEventListeners = function(root, preventDefault, callback) { root.on('click', Selectors.pin.toggle, function(e) { var toggleElement = $(this); var forumid = toggleElement.data('forumid'); var discussionid = toggleElement.data('discussionid'); var pinstate = toggleElement.data('targetstate'); Repository.setPinDiscussionState(forumid, discussionid, pinstate) .then(function(context) { return callback(toggleElement, context); }) .then(function() { return String.get_string("pinupdated", "forum") .done(function(s) { return Notification.addNotification({ message: s, type: "info" }); }); }) .fail(Notification.exception); if (preventDefault) { e.preventDefault(); } }); }; return { init: registerEventListeners }; });PK-B\6amd/src/repository.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Forum repository class to encapsulate all of the AJAX requests that subscribe or unsubscribe * can be sent for forum. * * @module mod_forum/repository * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['core/ajax'], function(Ajax) { /** * Set the subscription state for a discussion in a forum. * * @param {number} forumId ID of the forum the discussion belongs to * @param {number} discussionId ID of the discussion with the subscription state * @param {boolean} targetState Set the subscribed state. True == subscribed; false == unsubscribed. * @return {object} jQuery promise */ var setDiscussionSubscriptionState = function(forumId, discussionId, targetState) { var request = { methodname: 'mod_forum_set_subscription_state', args: { forumid: forumId, discussionid: discussionId, targetstate: targetState } }; return Ajax.call([request])[0]; }; var addDiscussionPost = function(postid, subject, message, messageformat, isprivatereply, topreferredformat) { var request = { methodname: 'mod_forum_add_discussion_post', args: { postid: postid, message: message, messageformat: messageformat, subject: subject, options: [{ name: "private", value: isprivatereply, }, { name: "topreferredformat", value: topreferredformat, }] } }; return Ajax.call([request])[0]; }; /** * Set the favourite state for a discussion in a forum. * * @param {number} forumId ID of the forum the discussion belongs to * @param {number} discussionId ID of the discussion with the subscription state * @param {null|date} targetState Set the favourite state. True == favourited; false == unfavourited. * @return {object} jQuery promise */ var setFavouriteDiscussionState = function(forumId, discussionId, targetState) { var request = { methodname: 'mod_forum_toggle_favourite_state', args: { discussionid: discussionId, targetstate: targetState } }; return Ajax.call([request])[0]; }; var setDiscussionLockState = function(forumId, discussionId, targetState) { var request = { methodname: 'mod_forum_set_lock_state', args: { forumid: forumId, discussionid: discussionId, targetstate: targetState} }; return Ajax.call([request])[0]; }; /** * Set the pinned state for the discussion provided. * * @param {number} forumid * @param {number} discussionid * @param {boolean} targetstate * @return {*|Promise} */ var setPinDiscussionState = function(forumid, discussionid, targetstate) { var request = { methodname: 'mod_forum_set_pin_state', args: { discussionid: discussionid, targetstate: targetstate } }; return Ajax.call([request])[0]; }; /** * Get the discussions for the user and cmid provided. * * @param {number} userid * @param {number} cmid * @param {string} sortby * @param {string} sortdirection * @return {*|Promise} */ var getDiscussionByUserID = function(userid, cmid, sortby = 'modified', sortdirection = 'DESC') { var request = { methodname: 'mod_forum_get_discussion_posts_by_userid', args: { userid: userid, cmid: cmid, sortby: sortby, sortdirection: sortdirection, }, }; return Ajax.call([request])[0]; }; /** * Get the posts for the discussion ID provided. * * @param {number} discussionId * @param {String} sortby * @param {String} sortdirection * @return {*|Promise} */ var getDiscussionPosts = function(discussionId, sortby = 'created', sortdirection = 'ASC') { var request = { methodname: 'mod_forum_get_discussion_posts', args: { discussionid: discussionId, sortby: sortby, sortdirection: sortdirection, }, }; return Ajax.call([request])[0]; }; return { setDiscussionSubscriptionState: setDiscussionSubscriptionState, addDiscussionPost: addDiscussionPost, setDiscussionLockState: setDiscussionLockState, setFavouriteDiscussionState: setFavouriteDiscussionState, setPinDiscussionState: setPinDiscussionState, getDiscussionByUserID: getDiscussionByUserID, getDiscussionPosts: getDiscussionPosts, }; }); PK-B\b=__amd/src/lock_toggle.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Handle the manual locking of individual discussions * * @module mod_forum/lock_toggle * @copyright 2019 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core/templates', 'core/notification', 'mod_forum/repository', 'mod_forum/selectors', ], function( $, Templates, Notification, Repository, Selectors ) { /** * Register event listeners for the subscription toggle. * * @param {object} root The discussion list root element * @param {boolean} preventDefault Should the default action of the event be prevented */ var registerEventListeners = function(root, preventDefault) { root.on('click', Selectors.lock.toggle, function(e) { var toggleElement = $(this); var forumId = toggleElement.data('forumid'); var discussionId = toggleElement.data('discussionid'); var state = toggleElement.data('state'); Repository.setDiscussionLockState(forumId, discussionId, state) .then(function() { return location.reload(); }) .catch(Notification.exception); if (preventDefault) { e.preventDefault(); } }); }; return { init: registerEventListeners }; }); PK-B\_466amd/src/posts_list.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * This module is the highest level module for the calendar. It is * responsible for initialising all of the components required for * the calendar to run. It also coordinates the interaction between * components by listening for and responding to different events * triggered within the calendar UI. * * @module mod_forum/posts_list * @copyright 2019 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core/templates', 'core/notification', 'core/pending', 'mod_forum/selectors', 'mod_forum/inpage_reply', 'core_form/changechecker', ], function( $, Templates, Notification, Pending, Selectors, InPageReply, FormChangeChecker ) { var registerEventListeners = function(root, throttlingwarningmsg) { root.on('click', Selectors.post.inpageReplyLink, function(e) { e.preventDefault(); // After adding a reply a url hash is being generated that scrolls (points) to the newly added reply. // The hash being present causes this scrolling behavior to the particular reply to persists even when // another, non-related in-page replay link is being clicked which ultimately causes a bad user experience. // A particular solution for this problem would be changing the browser's history state when a url hash is // present. if (window.location.hash) { // Remove the fragment identifier from the url. var url = window.location.href.split('#')[0]; history.pushState({}, document.title, url); } var pending = new Pending('inpage-reply'); var currentTarget = $(e.currentTarget).parents(Selectors.post.forumCoreContent); var currentSubject = currentTarget.find(Selectors.post.forumSubject); var currentRoot = $(e.currentTarget).parents(Selectors.post.forumContent); var context = { postid: $(currentRoot).data('post-id'), "reply_url": $(e.currentTarget).attr('href'), sesskey: M.cfg.sesskey, parentsubject: currentSubject.data('replySubject'), canreplyprivately: $(e.currentTarget).data('can-reply-privately'), postformat: InPageReply.CONTENT_FORMATS.MOODLE, throttlingwarningmsg: throttlingwarningmsg }; if (!currentRoot.find(Selectors.post.inpageReplyContent).length) { Templates.render('mod_forum/inpage_reply', context) .then(function(html, js) { return Templates.appendNodeContents(currentTarget, html, js); }) .then(function() { return currentRoot.find(Selectors.post.inpageReplyContent) .slideToggle(300, pending.resolve).find('textarea').focus(); }) .then(function() { FormChangeChecker.watchFormById(`inpage-reply-${context.postid}`); return; }) .catch(Notification.exception); } else { var form = currentRoot.find(Selectors.post.inpageReplyContent); form.slideToggle(300, pending.resolve); if (form.is(':visible')) { form.find('textarea').focus(); } } }); }; return { init: function(root, throttlingwarningmsg) { registerEventListeners(root, throttlingwarningmsg); InPageReply.init(root); } }; }); PK-B\6[ [ amd/src/selectors.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Common CSS selectors for the forum UI. * * @module mod_forum/selectors * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([], function() { return { subscription: { toggle: "[data-type='subscription-toggle'][data-action='toggle']", }, summary: { actions: "[data-container='discussion-summary-actions']" }, post: { post: '[data-region="post"]', action: '[data-region="post-action"]', actionsContainer: '[data-region="post-actions-container"]', authorName: '[data-region="author-name"]', forumCoreContent: "[data-region-content='forum-post-core']", forumContent: "[data-content='forum-post']", forumSubject: "[data-region-content='forum-post-core-subject']", inpageCancelButton: "button[name='cancelbtn']", inpageReplyButton: "button", inpageReplyLink: "[data-action='collapsible-link']", inpageReplyCancelButton: "[data-action='cancel-inpage-reply']", inpageReplyCreateButton: "[data-action='create-inpage-reply']", inpageReplyContainer: '[data-region="inpage-reply-container"]', inpageReplyContent: "[data-content='inpage-reply-content']", inpageReplyForm: "form[data-content='inpage-reply-form']", inpageSubmitBtn: "[data-action='forum-inpage-submit']", inpageSubmitBtnText: "[data-region='submit-text']", loadingIconContainer: "[data-region='loading-icon-container']", repliesContainer: "[data-region='replies-container']", replyCount: '[data-region="reply-count"]', modeSelect: "select[name='mode']", showReplies: '[data-action="show-replies"]', hideReplies: '[data-action="hide-replies"]', repliesVisibilityToggleContainer: '[data-region="replies-visibility-toggle-container"]' }, lock: { toggle: "[data-action='toggle'][data-type='lock-toggle']", icon: "[data-region='locked-icon']" }, favourite: { toggle: "[data-type='favorite-toggle'][data-action='toggle']", }, pin: { toggle: "[data-type='pin-toggle'][data-action='toggle']", }, discussion: { tools: '[data-container="discussion-tools"]', item: '[data-region="discussion-list-item"]', lockedLabel: "[data-region='locked-label']", subscribedLabel: "[data-region='subscribed-label']", timedLabel: "[data-region='timed-label']", }, }; }); PK-B\གྷ>>amd/src/discussion_nested_v2.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Module for viewing a discussion in nested v2 view. * * @module mod_forum/discussion_nested_v2 * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import $ from 'jquery'; import AutoRows from 'core/auto_rows'; import CustomEvents from 'core/custom_interaction_events'; import * as FormChangeChecker from 'core_form/changechecker'; import Notification from 'core/notification'; import Templates from 'core/templates'; import Discussion from 'mod_forum/discussion'; import InPageReply from 'mod_forum/inpage_reply'; import LockToggle from 'mod_forum/lock_toggle'; import FavouriteToggle from 'mod_forum/favourite_toggle'; import Pin from 'mod_forum/pin_toggle'; import Selectors from 'mod_forum/selectors'; import Subscribe from 'mod_forum/subscription_toggle'; const ANIMATION_DURATION = 150; /** * Get the closest post container element from the given element. * * @param {Object} element jQuery element to search from * @return {Object} jQuery element */ const getPostContainer = (element) => { return element.closest(Selectors.post.post); }; /** * Get the closest post container element from the given element. * * @param {Object} element jQuery element to search from * @param {Number} id Id of the post to find. * @return {Object} jQuery element */ const getPostContainerById = (element, id) => { return element.find(`${Selectors.post.post}[data-post-id=${id}]`); }; /** * Get the parent post container elements from the given element. * * @param {Object} element jQuery element to search from * @return {Object} jQuery element */ const getParentPostContainers = (element) => { return element.parents(Selectors.post.post); }; /** * Get the post content container element from the post container element. * * @param {Object} postContainer jQuery element for the post container * @return {Object} jQuery element */ const getPostContentContainer = (postContainer) => { return postContainer.children().not(Selectors.post.repliesContainer).find(Selectors.post.forumCoreContent); }; /** * Get the in page reply container element from the post container element. * * @param {Object} postContainer jQuery element for the post container * @return {Object} jQuery element */ const getInPageReplyContainer = (postContainer) => { return postContainer.children().filter(Selectors.post.inpageReplyContainer); }; /** * Get the in page reply form element from the post container element. * * @param {Object} postContainer jQuery element for the post container * @return {Object} jQuery element */ const getInPageReplyForm = (postContainer) => { return getInPageReplyContainer(postContainer).find(Selectors.post.inpageReplyContent); }; /** * Get the in page reply create (reply) button element from the post container element. * * @param {Object} postContainer jQuery element for the post container * @return {Object} jQuery element */ const getInPageReplyCreateButton = (postContainer) => { return getPostContentContainer(postContainer).find(Selectors.post.inpageReplyCreateButton); }; /** * Get the replies visibility toggle container (show/hide replies button container) element * from the post container element. * * @param {Object} postContainer jQuery element for the post container * @return {Object} jQuery element */ const getRepliesVisibilityToggleContainer = (postContainer) => { return postContainer.children(Selectors.post.repliesVisibilityToggleContainer); }; /** * Get the replies container element from the post container element. * * @param {Object} postContainer jQuery element for the post container * @return {Object} jQuery element */ const getRepliesContainer = (postContainer) => { return postContainer.children(Selectors.post.repliesContainer); }; /** * Check if the post has any replies. * * @param {Object} postContainer jQuery element for the post container * @return {Bool} */ const hasReplies = (postContainer) => { return getRepliesContainer(postContainer).children().length > 0; }; /** * Get the show replies button element from the replies visibility toggle container element. * * @param {Object} replyVisibilityToggleContainer jQuery element for the toggle container * @return {Object} jQuery element */ const getShowRepliesButton = (replyVisibilityToggleContainer) => { return replyVisibilityToggleContainer.find(Selectors.post.showReplies); }; /** * Get the hide replies button element from the replies visibility toggle container element. * * @param {Object} replyVisibilityToggleContainer jQuery element for the toggle container * @return {Object} jQuery element */ const getHideRepliesButton = (replyVisibilityToggleContainer) => { return replyVisibilityToggleContainer.find(Selectors.post.hideReplies); }; /** * Check if the replies are visible. * * @param {Object} postContainer jQuery element for the post container * @return {Bool} */ const repliesVisible = (postContainer) => { const repliesContainer = getRepliesContainer(postContainer); return repliesContainer.is(':visible'); }; /** * Show the post replies. * * @param {Object} postContainer jQuery element for the post container * @param {Number|null} postIdToSee Id of the post to scroll into view (if any) */ const showReplies = (postContainer, postIdToSee = null) => { const repliesContainer = getRepliesContainer(postContainer); const replyVisibilityToggleContainer = getRepliesVisibilityToggleContainer(postContainer); const showButton = getShowRepliesButton(replyVisibilityToggleContainer); const hideButton = getHideRepliesButton(replyVisibilityToggleContainer); showButton.addClass('hidden'); hideButton.removeClass('hidden'); repliesContainer.slideDown({ duration: ANIMATION_DURATION, queue: false, complete: () => { if (postIdToSee) { const postContainerToSee = getPostContainerById(repliesContainer, postIdToSee); if (postContainerToSee.length) { postContainerToSee[0].scrollIntoView(); } } } }).css('display', 'none').fadeIn(ANIMATION_DURATION); }; /** * Hide the post replies. * * @param {Object} postContainer jQuery element for the post container */ const hideReplies = (postContainer) => { const repliesContainer = getRepliesContainer(postContainer); const replyVisibilityToggleContainer = getRepliesVisibilityToggleContainer(postContainer); const showButton = getShowRepliesButton(replyVisibilityToggleContainer); const hideButton = getHideRepliesButton(replyVisibilityToggleContainer); showButton.removeClass('hidden'); hideButton.addClass('hidden'); repliesContainer.slideUp({ duration: ANIMATION_DURATION, queue: false }).fadeOut(ANIMATION_DURATION); }; /** Variable to hold the showInPageReplyForm function after it's built. */ let showInPageReplyForm = null; /** * Build the showInPageReplyForm function with the given additional template context. * * @param {Object} additionalTemplateContext Additional render context for the in page reply template. * @return {Function} */ const buildShowInPageReplyFormFunction = (additionalTemplateContext) => { /** * Show the in page reply form in the given in page reply container. The form * display will be animated. * * @param {Object} postContainer jQuery element for the post container */ return async(postContainer) => { const inPageReplyContainer = getInPageReplyContainer(postContainer); const repliesVisibilityToggleContainer = getRepliesVisibilityToggleContainer(postContainer); const inPageReplyCreateButton = getInPageReplyCreateButton(postContainer); if (!hasInPageReplyForm(inPageReplyContainer)) { try { const html = await renderInPageReplyTemplate(additionalTemplateContext, inPageReplyCreateButton, postContainer); Templates.appendNodeContents(inPageReplyContainer, html, ''); } catch (e) { Notification.exception(e); } FormChangeChecker.watchForm(postContainer[0].querySelector('form')); } inPageReplyCreateButton.fadeOut(ANIMATION_DURATION, () => { const inPageReplyForm = getInPageReplyForm(postContainer); inPageReplyForm.slideDown({ duration: ANIMATION_DURATION, queue: false, complete: () => { inPageReplyForm.find('textarea').focus(); } }).css('display', 'none').fadeIn(ANIMATION_DURATION); if (repliesVisibilityToggleContainer.length && hasReplies(postContainer)) { repliesVisibilityToggleContainer.fadeIn(ANIMATION_DURATION); hideReplies(postContainer); } }); }; }; /** * Hide the in page reply form in the given in page reply container. The form * display will be animated. * * @param {Object} postContainer jQuery element for the post container * @param {Number|null} postIdToSee Id of the post to scroll into view (if any) */ const hideInPageReplyForm = (postContainer, postIdToSee = null) => { const inPageReplyForm = getInPageReplyForm(postContainer); const inPageReplyCreateButton = getInPageReplyCreateButton(postContainer); const repliesVisibilityToggleContainer = getRepliesVisibilityToggleContainer(postContainer); if (repliesVisibilityToggleContainer.length && hasReplies(postContainer)) { repliesVisibilityToggleContainer.fadeOut(ANIMATION_DURATION); if (!repliesVisible(postContainer)) { showReplies(postContainer, postIdToSee); } } inPageReplyForm.slideUp({ duration: ANIMATION_DURATION, queue: false, complete: () => { inPageReplyCreateButton.fadeIn(ANIMATION_DURATION); } }).fadeOut(200); }; /** * Check if the in page reply container contains the in page reply form. * * @param {Object} inPageReplyContainer jQuery element for the in page reply container * @return {Bool} */ const hasInPageReplyForm = (inPageReplyContainer) => { return inPageReplyContainer.find(Selectors.post.inpageReplyContent).length > 0; }; /** * Render the template to generate the in page reply form HTML. * * @param {Object} additionalTemplateContext Additional render context for the in page reply template * @param {Object} button jQuery element for the reply button that was clicked * @param {Object} postContainer jQuery element for the post container * @return {Object} jQuery promise */ const renderInPageReplyTemplate = (additionalTemplateContext, button, postContainer) => { const postContentContainer = getPostContentContainer(postContainer); const currentSubject = postContentContainer.find(Selectors.post.forumSubject).text(); const currentAuthorName = postContentContainer.find(Selectors.post.authorName).text(); const context = { postid: postContainer.data('post-id'), "reply_url": button.attr('data-href'), sesskey: M.cfg.sesskey, parentsubject: currentSubject, parentauthorname: currentAuthorName, canreplyprivately: button.data('can-reply-privately'), postformat: InPageReply.CONTENT_FORMATS.MOODLE, ...additionalTemplateContext }; return Templates.render('mod_forum/inpage_reply_v2', context); }; /** * Increment the total reply count in the show/hide replies buttons for the post. * * @param {Object} postContainer jQuery element for the post container */ const incrementTotalReplyCount = (postContainer) => { getRepliesVisibilityToggleContainer(postContainer).find(Selectors.post.replyCount).each((index, element) => { const currentCount = parseInt(element.innerText, 10); element.innerText = currentCount + 1; }); }; /** * Create all of the event listeners for the discussion. * * @param {Object} root jQuery element for the discussion container */ const registerEventListeners = (root) => { CustomEvents.define(root, [CustomEvents.events.activate]); // Auto expanding text area for in page reply. AutoRows.init(root); // Reply button is clicked. root.on(CustomEvents.events.activate, Selectors.post.inpageReplyCreateButton, (e, data) => { data.originalEvent.preventDefault(); const postContainer = getPostContainer($(e.currentTarget)); showInPageReplyForm(postContainer); }); // Cancel in page reply button. root.on(CustomEvents.events.activate, Selectors.post.inpageReplyCancelButton, (e, data) => { data.originalEvent.preventDefault(); const postContainer = getPostContainer($(e.currentTarget)); hideInPageReplyForm(postContainer); }); // Show replies button clicked. root.on(CustomEvents.events.activate, Selectors.post.showReplies, (e, data) => { data.originalEvent.preventDefault(); const postContainer = getPostContainer($(e.target)); showReplies(postContainer); }); // Hide replies button clicked. root.on(CustomEvents.events.activate, Selectors.post.hideReplies, (e, data) => { data.originalEvent.preventDefault(); const postContainer = getPostContainer($(e.target)); hideReplies(postContainer); }); // Post created with in page reply. root.on(InPageReply.EVENTS.POST_CREATED, Selectors.post.inpageSubmitBtn, (e, newPostId) => { const currentTarget = $(e.currentTarget); const postContainer = getPostContainer(currentTarget); const postContainers = getParentPostContainers(currentTarget); hideInPageReplyForm(postContainer, newPostId); postContainers.each((index, container) => { incrementTotalReplyCount($(container)); }); }); }; /** * Initialise the javascript for the discussion in nested v2 display mode. * * @param {Object} root jQuery element for the discussion container * @param {Object} context Additional render context for the in page reply template */ export const init = (root, context) => { // Build the showInPageReplyForm function with the additional render context. showInPageReplyForm = buildShowInPageReplyFormFunction(context); // Add discussion event listeners. registerEventListeners(root); // Initialise default discussion javascript (keyboard nav etc). Discussion.init(root); // Add in page reply javascript. InPageReply.init(root); // Initialise the settings menu javascript. const discussionToolsContainer = root.find(Selectors.discussion.tools); LockToggle.init(discussionToolsContainer, false); FavouriteToggle.init(discussionToolsContainer, false, (toggleElement, response) => { const newTargetState = response.userstate.favourited ? 0 : 1; return toggleElement.data('targetstate', newTargetState); }); Pin.init(discussionToolsContainer, false, (toggleElement, response) => { const newTargetState = response.pinned ? 0 : 1; return toggleElement.data('targetstate', newTargetState); }); Subscribe.init(discussionToolsContainer, false, (toggleElement, response) => { const newTargetState = response.userstate.subscribed ? 0 : 1; toggleElement.data('targetstate', newTargetState); }); }; PK-B\iV amd/src/form-user-selector.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Enrolled user selector module. * * @module mod_forum/form-user-selector * @copyright 2019 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['jquery', 'core/ajax', 'core/templates'], function($, Ajax, Templates) { return /** @alias module:mod_forum/form-user-selector */ { processResults: function(selector, results) { var users = []; $.each(results, function(index, user) { users.push({ value: user.id, label: user._label }); }); return users; }, transport: function(selector, query, success, failure) { var promise; var courseid = $(selector).attr('courseid'); var contextid = $(selector).attr('data-contextid'); promise = Ajax.call([{ methodname: 'core_enrol_search_users', args: { courseid: courseid, search: query, searchanywhere: true, page: 0, perpage: 30, contextid: contextid, } }]); promise[0].then(function(results) { var promises = [], i = 0; // Render the label. $.each(results, function(index, user) { promises.push(Templates.render('mod_forum/form-user-selector-suggestion', user)); }); // Apply the label to the results. return $.when.apply($.when, promises).then(function() { var args = arguments; $.each(results, function(index, user) { user._label = args[i]; i++; }); success(results); return; }); }).fail(failure); } }; }); PK-B\mJamd/src/forum_events.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Events for the forum activity. * * @module mod_forum/forum_events * @copyright 2019 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([], function() { return { SUBSCRIPTION_TOGGLED: 'mod_forum/subscription_toggle:subscriptionToggled', }; }); PK-B\N>? amd/src/favourite_toggle.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Handle discussion subscription toggling on a discussion list in * the forum view. * * @module mod_forum/favourite_toggle * @copyright 2019 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core/templates', 'core/notification', 'mod_forum/repository', 'mod_forum/selectors', 'core/str', ], function( $, Templates, Notification, Repository, Selectors, String ) { /** * Register event listeners for the subscription toggle. * * @param {object} root The discussion list root element * @param {boolean} preventDefault Should the default action of the event be prevented * @param {function} callback Success callback */ var registerEventListeners = function(root, preventDefault, callback) { root.on('click', Selectors.favourite.toggle, function(e) { var toggleElement = $(this); var forumId = toggleElement.data('forumid'); var discussionId = toggleElement.data('discussionid'); var subscriptionState = toggleElement.data('targetstate'); Repository.setFavouriteDiscussionState(forumId, discussionId, subscriptionState) .then(function(context) { return callback(toggleElement, context); }) .then(function() { return String.get_string("favouriteupdated", "forum") .done(function(s) { return Notification.addNotification({ message: s, type: "info" }); }); }) .catch(Notification.exception); if (preventDefault) { e.preventDefault(); } }); }; return { init: registerEventListeners }; }); PK-B\'%kkamd/build/pin_toggle.min.js.mapnu[{"version":3,"file":"pin_toggle.min.js","sources":["../src/pin_toggle.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * This module is the highest level module for the calendar. It is\n * responsible for initialising all of the components required for\n * the calendar to run. It also coordinates the interaction between\n * components by listening for and responding to different events\n * triggered within the calendar UI.\n *\n * @module mod_forum/pin_toggle\n * @copyright 2018 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/ajax',\n 'core/str',\n 'core/templates',\n 'core/notification',\n 'mod_forum/repository',\n 'mod_forum/selectors',\n 'core/str',\n], function(\n $,\n Ajax,\n Str,\n Templates,\n Notification,\n Repository,\n Selectors,\n String\n) {\n\n /**\n * Registery event listeners for the pin toggle.\n *\n * @param {object} root The calendar root element\n * @param {boolean} preventDefault Should the default action of the event be prevented\n * @param {function} callback Success callback\n */\n var registerEventListeners = function(root, preventDefault, callback) {\n root.on('click', Selectors.pin.toggle, function(e) {\n var toggleElement = $(this);\n var forumid = toggleElement.data('forumid');\n var discussionid = toggleElement.data('discussionid');\n var pinstate = toggleElement.data('targetstate');\n Repository.setPinDiscussionState(forumid, discussionid, pinstate)\n .then(function(context) {\n return callback(toggleElement, context);\n })\n .then(function() {\n return String.get_string(\"pinupdated\", \"forum\")\n .done(function(s) {\n return Notification.addNotification({\n message: s,\n type: \"info\"\n });\n });\n })\n .fail(Notification.exception);\n\n if (preventDefault) {\n e.preventDefault();\n }\n });\n };\n\n return {\n init: registerEventListeners\n };\n});"],"names":["define","$","Ajax","Str","Templates","Notification","Repository","Selectors","String","init","root","preventDefault","callback","on","pin","toggle","e","toggleElement","this","forumid","data","discussionid","pinstate","setPinDiscussionState","then","context","get_string","done","s","addNotification","message","type","fail","exception"],"mappings":";;;;;;;;;;;AA0BAA,8BAAO,CACH,SACA,YACA,WACA,iBACA,oBACA,uBACA,sBACA,aACD,SACCC,EACAC,KACAC,IACAC,UACAC,aACAC,WACAC,UACAC,cAqCO,CACHC,KA5ByB,SAASC,KAAMC,eAAgBC,UACxDF,KAAKG,GAAG,QAASN,UAAUO,IAAIC,QAAQ,SAASC,OACxCC,cAAgBhB,EAAEiB,MAClBC,QAAUF,cAAcG,KAAK,WAC7BC,aAAeJ,cAAcG,KAAK,gBAClCE,SAAWL,cAAcG,KAAK,eAClCd,WAAWiB,sBAAsBJ,QAASE,aAAcC,UACnDE,MAAK,SAASC,gBACJb,SAASK,cAAeQ,YAElCD,MAAK,kBACKhB,OAAOkB,WAAW,aAAc,SAClCC,MAAK,SAASC,UACJvB,aAAawB,gBAAgB,CAChCC,QAASF,EACTG,KAAM,eAIrBC,KAAK3B,aAAa4B,WAEnBtB,gBACAK,EAAEL"}PK-B\EJ$($(%amd/build/discussion_nested_v2.min.jsnu[define("mod_forum/discussion_nested_v2",["exports","jquery","core/auto_rows","core/custom_interaction_events","core_form/changechecker","core/notification","core/templates","mod_forum/discussion","mod_forum/inpage_reply","mod_forum/lock_toggle","mod_forum/favourite_toggle","mod_forum/pin_toggle","mod_forum/selectors","mod_forum/subscription_toggle"],(function(_exports,_jquery,_auto_rows,_custom_interaction_events,FormChangeChecker,_notification,_templates,_discussion,_inpage_reply,_lock_toggle,_favourite_toggle,_pin_toggle,_selectors,_subscription_toggle){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Module for viewing a discussion in nested v2 view. * * @module mod_forum/discussion_nested_v2 * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=_interopRequireDefault(_jquery),_auto_rows=_interopRequireDefault(_auto_rows),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),FormChangeChecker=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(FormChangeChecker),_notification=_interopRequireDefault(_notification),_templates=_interopRequireDefault(_templates),_discussion=_interopRequireDefault(_discussion),_inpage_reply=_interopRequireDefault(_inpage_reply),_lock_toggle=_interopRequireDefault(_lock_toggle),_favourite_toggle=_interopRequireDefault(_favourite_toggle),_pin_toggle=_interopRequireDefault(_pin_toggle),_selectors=_interopRequireDefault(_selectors),_subscription_toggle=_interopRequireDefault(_subscription_toggle);const getPostContainer=element=>element.closest(_selectors.default.post.post),getPostContainerById=(element,id)=>element.find("".concat(_selectors.default.post.post,"[data-post-id=").concat(id,"]")),getPostContentContainer=postContainer=>postContainer.children().not(_selectors.default.post.repliesContainer).find(_selectors.default.post.forumCoreContent),getInPageReplyContainer=postContainer=>postContainer.children().filter(_selectors.default.post.inpageReplyContainer),getInPageReplyForm=postContainer=>getInPageReplyContainer(postContainer).find(_selectors.default.post.inpageReplyContent),getInPageReplyCreateButton=postContainer=>getPostContentContainer(postContainer).find(_selectors.default.post.inpageReplyCreateButton),getRepliesVisibilityToggleContainer=postContainer=>postContainer.children(_selectors.default.post.repliesVisibilityToggleContainer),getRepliesContainer=postContainer=>postContainer.children(_selectors.default.post.repliesContainer),hasReplies=postContainer=>getRepliesContainer(postContainer).children().length>0,getShowRepliesButton=replyVisibilityToggleContainer=>replyVisibilityToggleContainer.find(_selectors.default.post.showReplies),getHideRepliesButton=replyVisibilityToggleContainer=>replyVisibilityToggleContainer.find(_selectors.default.post.hideReplies),repliesVisible=postContainer=>getRepliesContainer(postContainer).is(":visible"),showReplies=function(postContainer){let postIdToSee=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;const repliesContainer=getRepliesContainer(postContainer),replyVisibilityToggleContainer=getRepliesVisibilityToggleContainer(postContainer),showButton=getShowRepliesButton(replyVisibilityToggleContainer),hideButton=getHideRepliesButton(replyVisibilityToggleContainer);showButton.addClass("hidden"),hideButton.removeClass("hidden"),repliesContainer.slideDown({duration:150,queue:!1,complete:()=>{if(postIdToSee){const postContainerToSee=getPostContainerById(repliesContainer,postIdToSee);postContainerToSee.length&&postContainerToSee[0].scrollIntoView()}}}).css("display","none").fadeIn(150)},hideReplies=postContainer=>{const repliesContainer=getRepliesContainer(postContainer),replyVisibilityToggleContainer=getRepliesVisibilityToggleContainer(postContainer),showButton=getShowRepliesButton(replyVisibilityToggleContainer),hideButton=getHideRepliesButton(replyVisibilityToggleContainer);showButton.removeClass("hidden"),hideButton.addClass("hidden"),repliesContainer.slideUp({duration:150,queue:!1}).fadeOut(150)};let showInPageReplyForm=null;const hideInPageReplyForm=function(postContainer){let postIdToSee=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;const inPageReplyForm=getInPageReplyForm(postContainer),inPageReplyCreateButton=getInPageReplyCreateButton(postContainer),repliesVisibilityToggleContainer=getRepliesVisibilityToggleContainer(postContainer);repliesVisibilityToggleContainer.length&&hasReplies(postContainer)&&(repliesVisibilityToggleContainer.fadeOut(150),repliesVisible(postContainer)||showReplies(postContainer,postIdToSee)),inPageReplyForm.slideUp({duration:150,queue:!1,complete:()=>{inPageReplyCreateButton.fadeIn(150)}}).fadeOut(200)},hasInPageReplyForm=inPageReplyContainer=>inPageReplyContainer.find(_selectors.default.post.inpageReplyContent).length>0,renderInPageReplyTemplate=(additionalTemplateContext,button,postContainer)=>{const postContentContainer=getPostContentContainer(postContainer),currentSubject=postContentContainer.find(_selectors.default.post.forumSubject).text(),currentAuthorName=postContentContainer.find(_selectors.default.post.authorName).text(),context={postid:postContainer.data("post-id"),reply_url:button.attr("data-href"),sesskey:M.cfg.sesskey,parentsubject:currentSubject,parentauthorname:currentAuthorName,canreplyprivately:button.data("can-reply-privately"),postformat:_inpage_reply.default.CONTENT_FORMATS.MOODLE,...additionalTemplateContext};return _templates.default.render("mod_forum/inpage_reply_v2",context)},registerEventListeners=root=>{_custom_interaction_events.default.define(root,[_custom_interaction_events.default.events.activate]),_auto_rows.default.init(root),root.on(_custom_interaction_events.default.events.activate,_selectors.default.post.inpageReplyCreateButton,((e,data)=>{data.originalEvent.preventDefault();const postContainer=getPostContainer((0,_jquery.default)(e.currentTarget));showInPageReplyForm(postContainer)})),root.on(_custom_interaction_events.default.events.activate,_selectors.default.post.inpageReplyCancelButton,((e,data)=>{data.originalEvent.preventDefault();const postContainer=getPostContainer((0,_jquery.default)(e.currentTarget));hideInPageReplyForm(postContainer)})),root.on(_custom_interaction_events.default.events.activate,_selectors.default.post.showReplies,((e,data)=>{data.originalEvent.preventDefault();const postContainer=getPostContainer((0,_jquery.default)(e.target));showReplies(postContainer)})),root.on(_custom_interaction_events.default.events.activate,_selectors.default.post.hideReplies,((e,data)=>{data.originalEvent.preventDefault();const postContainer=getPostContainer((0,_jquery.default)(e.target));hideReplies(postContainer)})),root.on(_inpage_reply.default.EVENTS.POST_CREATED,_selectors.default.post.inpageSubmitBtn,((e,newPostId)=>{const currentTarget=(0,_jquery.default)(e.currentTarget),postContainer=getPostContainer(currentTarget),postContainers=currentTarget.parents(_selectors.default.post.post);hideInPageReplyForm(postContainer,newPostId),postContainers.each(((index,container)=>{(postContainer=>{getRepliesVisibilityToggleContainer(postContainer).find(_selectors.default.post.replyCount).each(((index,element)=>{const currentCount=parseInt(element.innerText,10);element.innerText=currentCount+1}))})((0,_jquery.default)(container))}))}))};_exports.init=(root,context)=>{var additionalTemplateContext;additionalTemplateContext=context,showInPageReplyForm=async postContainer=>{const inPageReplyContainer=getInPageReplyContainer(postContainer),repliesVisibilityToggleContainer=getRepliesVisibilityToggleContainer(postContainer),inPageReplyCreateButton=getInPageReplyCreateButton(postContainer);if(!hasInPageReplyForm(inPageReplyContainer)){try{const html=await renderInPageReplyTemplate(additionalTemplateContext,inPageReplyCreateButton,postContainer);_templates.default.appendNodeContents(inPageReplyContainer,html,"")}catch(e){_notification.default.exception(e)}FormChangeChecker.watchForm(postContainer[0].querySelector("form"))}inPageReplyCreateButton.fadeOut(150,(()=>{const inPageReplyForm=getInPageReplyForm(postContainer);inPageReplyForm.slideDown({duration:150,queue:!1,complete:()=>{inPageReplyForm.find("textarea").focus()}}).css("display","none").fadeIn(150),repliesVisibilityToggleContainer.length&&hasReplies(postContainer)&&(repliesVisibilityToggleContainer.fadeIn(150),hideReplies(postContainer))}))},registerEventListeners(root),_discussion.default.init(root),_inpage_reply.default.init(root);const discussionToolsContainer=root.find(_selectors.default.discussion.tools);_lock_toggle.default.init(discussionToolsContainer,!1),_favourite_toggle.default.init(discussionToolsContainer,!1,((toggleElement,response)=>{const newTargetState=response.userstate.favourited?0:1;return toggleElement.data("targetstate",newTargetState)})),_pin_toggle.default.init(discussionToolsContainer,!1,((toggleElement,response)=>{const newTargetState=response.pinned?0:1;return toggleElement.data("targetstate",newTargetState)})),_subscription_toggle.default.init(discussionToolsContainer,!1,((toggleElement,response)=>{const newTargetState=response.userstate.subscribed?0:1;toggleElement.data("targetstate",newTargetState)}))}})); //# sourceMappingURL=discussion_nested_v2.min.js.mapPK-B\p¯.amd/build/grades/expandconversation.min.js.mapnu[{"version":3,"file":"expandconversation.min.js","sources":["../../src/grades/expandconversation.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * This module handles the creation of a Modal that shows the user's post in context of the entire discussion.\n *\n * @module mod_forum/grades/expandconversation\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport * as ForumSelectors from './grader/selectors';\nimport Repository from 'mod_forum/repository';\nimport {exception as showException} from \"core/notification\";\nimport Templates from 'core/templates';\nimport Modal from 'core/modal_cancel';\nimport * as ModalEvents from 'core/modal_events';\n\n/**\n * Find the Node containing the gradable details from the provided node by searching up the tree.\n *\n * @param {HTMLElement} node\n * @returns {HTMLElement}\n */\nconst findGradableNode = node => node.closest(ForumSelectors.expandConversation);\n\n/**\n * Show the post in context in a modal.\n *\n * @param {HTMLElement} rootNode The button that has been clicked\n * @param {object} param\n * @param {bool} [param.focusOnClose=null]\n */\nconst showPostInContext = async(rootNode, {\n focusOnClose = null,\n} = {}) => {\n const postId = rootNode.dataset.postid;\n const discussionId = rootNode.dataset.discussionid;\n const discussionName = rootNode.dataset.name;\n const experimentalDisplayMode = rootNode.dataset.experimentalDisplayMode == \"1\";\n\n const [\n allPosts,\n modal,\n ] = await Promise.all([\n Repository.getDiscussionPosts(parseInt(discussionId)),\n Modal.create({\n title: discussionName,\n large: true,\n removeOnClose: true,\n returnElement: focusOnClose,\n }),\n ]);\n\n const postsById = new Map(allPosts.posts.map(post => {\n post.readonly = true;\n post.hasreplies = false;\n post.replies = [];\n return [post.id, post];\n }));\n\n let posts = [];\n allPosts.posts.forEach(post => {\n if (post.parentid) {\n const parent = postsById.get(post.parentid);\n if (parent) {\n post.parentauthorname = parent.author.fullname;\n parent.hasreplies = true;\n parent.replies.push(post);\n } else {\n posts.push(post);\n }\n } else {\n posts.push(post);\n }\n });\n\n modal.getRoot().on(ModalEvents.bodyRendered, () => {\n const relevantPost = modal.getRoot()[0].querySelector(`#p${postId}`);\n if (relevantPost) {\n relevantPost.scrollIntoView({behavior: \"smooth\"});\n }\n });\n\n modal.show();\n\n // Note: We do not use await here because it messes with the Modal transitions.\n const templatePromise = Templates.render('mod_forum/grades/grader/discussion/post_modal', {\n posts,\n experimentaldisplaymode: experimentalDisplayMode\n });\n modal.setBody(templatePromise);\n};\n\n/**\n * Register event listeners for the expand conversations button.\n *\n * @param {HTMLElement} rootNode The root to listen to.\n */\nexport const registerEventListeners = (rootNode) => {\n rootNode.addEventListener('click', (e) => {\n const rootNode = findGradableNode(e.target);\n\n if (rootNode) {\n e.preventDefault();\n\n try {\n showPostInContext(rootNode, {\n focusOnClose: e.target,\n });\n } catch (err) {\n showException(err);\n }\n }\n });\n};\n"],"names":["rootNode","addEventListener","e","target","closest","ForumSelectors","expandConversation","preventDefault","async","focusOnClose","postId","dataset","postid","discussionId","discussionid","discussionName","name","experimentalDisplayMode","allPosts","modal","Promise","all","Repository","getDiscussionPosts","parseInt","Modal","create","title","large","removeOnClose","returnElement","postsById","Map","posts","map","post","readonly","hasreplies","replies","id","forEach","parentid","parent","get","parentauthorname","author","fullname","push","getRoot","on","ModalEvents","bodyRendered","relevantPost","querySelector","scrollIntoView","behavior","show","templatePromise","Templates","render","experimentaldisplaymode","setBody","showPostInContext","err"],"mappings":";;;;;;;8XA8GuCA,WACnCA,SAASC,iBAAiB,SAAUC,UAC1BF,SAA4BE,EAAEC,OA7ENC,QAAQC,eAAeC,uBA+EjDN,SAAU,CACVE,EAAEK,sBAvEYC,eAAMR,cAAUS,aACtCA,aAAe,6DACf,SACMC,OAASV,SAASW,QAAQC,OAC1BC,aAAeb,SAASW,QAAQG,aAChCC,eAAiBf,SAASW,QAAQK,KAClCC,wBAAsE,KAA5CjB,SAASW,QAAQM,yBAG7CC,SACAC,aACMC,QAAQC,IAAI,CAClBC,oBAAWC,mBAAmBC,SAASX,eACvCY,sBAAMC,OAAO,CACTC,MAAOZ,eACPa,OAAO,EACPC,eAAe,EACfC,cAAerB,iBAIjBsB,UAAY,IAAIC,IAAId,SAASe,MAAMC,KAAIC,OACzCA,KAAKC,UAAW,EAChBD,KAAKE,YAAa,EAClBF,KAAKG,QAAU,GACR,CAACH,KAAKI,GAAIJ,cAGjBF,MAAQ,GACZf,SAASe,MAAMO,SAAQL,UACfA,KAAKM,SAAU,OACTC,OAASX,UAAUY,IAAIR,KAAKM,UAC9BC,QACAP,KAAKS,iBAAmBF,OAAOG,OAAOC,SACtCJ,OAAOL,YAAa,EACpBK,OAAOJ,QAAQS,KAAKZ,OAEpBF,MAAMc,KAAKZ,WAGfF,MAAMc,KAAKZ,SAInBhB,MAAM6B,UAAUC,GAAGC,YAAYC,cAAc,WACnCC,aAAejC,MAAM6B,UAAU,GAAGK,0BAAmB3C,SACvD0C,cACAA,aAAaE,eAAe,CAACC,SAAU,cAI/CpC,MAAMqC,aAGAC,gBAAkBC,mBAAUC,OAAO,gDAAiD,CACtF1B,MAAAA,MACA2B,wBAAyB3C,0BAE7BE,MAAM0C,QAAQJ,iBAgBFK,CAAkB9D,SAAU,CACxBS,aAAcP,EAAEC,SAEtB,MAAO4D,iCACSA"}PK-B\E| *amd/build/grades/expandconversation.min.jsnu[define("mod_forum/grades/expandconversation",["exports","./grader/selectors","mod_forum/repository","core/notification","core/templates","core/modal_cancel","core/modal_events"],(function(_exports,ForumSelectors,_repository,_notification,_templates,_modal_cancel,ModalEvents){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} /** * This module handles the creation of a Modal that shows the user's post in context of the entire discussion. * * @module mod_forum/grades/expandconversation * @copyright 2019 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.registerEventListeners=void 0,ForumSelectors=_interopRequireWildcard(ForumSelectors),_repository=_interopRequireDefault(_repository),_templates=_interopRequireDefault(_templates),_modal_cancel=_interopRequireDefault(_modal_cancel),ModalEvents=_interopRequireWildcard(ModalEvents);_exports.registerEventListeners=rootNode=>{rootNode.addEventListener("click",(e=>{const rootNode=e.target.closest(ForumSelectors.expandConversation);if(rootNode){e.preventDefault();try{!async function(rootNode){let{focusOnClose:focusOnClose=null}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const postId=rootNode.dataset.postid,discussionId=rootNode.dataset.discussionid,discussionName=rootNode.dataset.name,experimentalDisplayMode="1"==rootNode.dataset.experimentalDisplayMode,[allPosts,modal]=await Promise.all([_repository.default.getDiscussionPosts(parseInt(discussionId)),_modal_cancel.default.create({title:discussionName,large:!0,removeOnClose:!0,returnElement:focusOnClose})]),postsById=new Map(allPosts.posts.map((post=>(post.readonly=!0,post.hasreplies=!1,post.replies=[],[post.id,post]))));let posts=[];allPosts.posts.forEach((post=>{if(post.parentid){const parent=postsById.get(post.parentid);parent?(post.parentauthorname=parent.author.fullname,parent.hasreplies=!0,parent.replies.push(post)):posts.push(post)}else posts.push(post)})),modal.getRoot().on(ModalEvents.bodyRendered,(()=>{const relevantPost=modal.getRoot()[0].querySelector("#p".concat(postId));relevantPost&&relevantPost.scrollIntoView({behavior:"smooth"})})),modal.show();const templatePromise=_templates.default.render("mod_forum/grades/grader/discussion/post_modal",{posts:posts,experimentaldisplaymode:experimentalDisplayMode});modal.setBody(templatePromise)}(rootNode,{focusOnClose:e.target})}catch(err){(0,_notification.exception)(err)}}}))}})); //# sourceMappingURL=expandconversation.min.js.mapPK-B\<ꚧ}},amd/build/grades/grader/selectors.min.js.mapnu[{"version":3,"file":"selectors.min.js","sources":["../../../src/grades/grader/selectors.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * This module will tie together all of the different calls the gradable module will make.\n *\n * @module mod_forum/grades/grader/selectors\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default {\n launch: '[data-grade-action=\"launch\"]',\n gradableItem: '[data-gradable-itemtype]',\n gradableItems: {\n wholeForum: '[data-gradable-itemtype=\"forum\"]',\n },\n expandConversation: '[data-action=\"view-context\"]',\n posts: '[data-region=\"posts\"]',\n viewGrade: '[data-grade-action=\"view\"]',\n};\n"],"names":["launch","gradableItem","gradableItems","wholeForum","expandConversation","posts","viewGrade"],"mappings":"mLAsBe,CACXA,OAAQ,+BACRC,aAAc,2BACdC,cAAe,CACXC,WAAY,oCAEhBC,mBAAoB,+BACpBC,MAAO,wBACPC,UAAW"}PK-B\-M(amd/build/grades/grader/selectors.min.jsnu[define("mod_forum/grades/grader/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={launch:'[data-grade-action="launch"]',gradableItem:"[data-gradable-itemtype]",gradableItems:{wholeForum:'[data-gradable-itemtype="forum"]'},expandConversation:'[data-action="view-context"]',posts:'[data-region="posts"]',viewGrade:'[data-grade-action="view"]'},_exports.default})); //# sourceMappingURL=selectors.min.js.mapPK-B\B#amd/build/grades/grader.min.jsnu[define("mod_forum/grades/grader",["exports","./grader/selectors","mod_forum/repository","core/templates","../local/grades/grader","core/notification","core_course/repository","core/url"],(function(_exports,Selectors,_repository,_templates,Grader,_notification,_repository2,_url){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} /** * This module will tie together all of the different calls the gradable module will make. * * @module mod_forum/grades/grader * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.registerLaunchListeners=void 0,Selectors=_interopRequireWildcard(Selectors),_repository=_interopRequireDefault(_repository),_templates=_interopRequireDefault(_templates),Grader=_interopRequireWildcard(Grader),_notification=_interopRequireDefault(_notification),_repository2=_interopRequireDefault(_repository2);const templateNames_contentRegion="mod_forum/grades/grader/discussion/posts",getContentForUserIdFunction=(cmid,experimentalDisplayMode)=>userid=>_repository.default.getDiscussionByUserID(userid,cmid).then((context=>(context.discussions=context.discussions.map(discussionPostMapper),context.experimentaldisplaymode=!!experimentalDisplayMode,_templates.default.render(templateNames_contentRegion,context)))).catch(_notification.default.exception),getGradableUsersForCourseidFunction=(courseID,groupID,onlyActive)=>async()=>(await _repository2.default.getGradableUsersFromCourseID(courseID,groupID,onlyActive)).users,findGradableNode=node=>node.closest(Selectors.gradableItem),discussionPostMapper=discussion=>{const parentMap=new Map;discussion.posts.parentposts.forEach((post=>parentMap.set(post.id,post)));const userPosts=discussion.posts.userposts.map((post=>{post.readonly=!0,post.hasreplies=!1,post.replies=[];const parent=post.parentid?parentMap.get(post.parentid):null;return parent&&(parent.hasreplies=!1,parent.replies=[],parent.readonly=!0,post.parentauthorname=parent.author.fullname),{parent:parent,post:post}}));return{...discussion,posts:userPosts}};_exports.registerLaunchListeners=()=>{document.addEventListener("click",(async e=>{if(e.target.matches(Selectors.launch)){const rootNode=findGradableNode(e.target);if(!rootNode)throw Error("Unable to find a gradable item");if(!rootNode.matches(Selectors.gradableItems.wholeForum))throw Error("Unable to find a valid gradable item");e.preventDefault();try{await async function(rootNode){let{focusOnClose:focusOnClose=null}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const data=rootNode.dataset,gradingPanelFunctions=await Grader.getGradingPanelFunctions("mod_forum",data.contextid,data.gradingComponent,data.gradingComponentSubtype,data.gradableItemtype),groupID=data.group?data.group:0,onlyActive=data.gradeOnlyActiveUsers;await Grader.launch(getGradableUsersForCourseidFunction(data.courseId,groupID,onlyActive),getContentForUserIdFunction(data.cmid,"1"==data.experimentalDisplayMode),gradingPanelFunctions.getter,gradingPanelFunctions.setter,{groupid:data.groupid,initialUserId:data.initialuserid,moduleName:data.name,courseName:data.courseName,courseUrl:(0,_url.relativeUrl)("/course/view.php",{id:data.courseId}),sendStudentNotifications:data.sendStudentNotifications,focusOnClose:focusOnClose})}(rootNode,{focusOnClose:e.target})}catch(error){_notification.default.exception(error)}}if(e.target.matches(Selectors.viewGrade)){e.preventDefault();const rootNode=findGradableNode(e.target);if(!rootNode)throw Error("Unable to find a gradable item");if(!rootNode.matches(Selectors.gradableItems.wholeForum))throw Error("Unable to find a valid gradable item");e.preventDefault();try{await async function(rootNode){let{focusOnClose:focusOnClose=null}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const data=rootNode.dataset,gradingPanelFunctions=await Grader.getGradingPanelFunctions("mod_forum",data.contextid,data.gradingComponent,data.gradingComponentSubtype,data.gradableItemtype);await Grader.view(gradingPanelFunctions.getter,data.userid,data.name,{focusOnClose:focusOnClose})}(rootNode,{focusOnClose:e.target})}catch(error){_notification.default.exception(error)}}}))}})); //# sourceMappingURL=grader.min.js.mapPK-B\fX@++"amd/build/grades/grader.min.js.mapnu[{"version":3,"file":"grader.min.js","sources":["../../src/grades/grader.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * This module will tie together all of the different calls the gradable module will make.\n *\n * @module mod_forum/grades/grader\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport * as Selectors from './grader/selectors';\nimport Repository from 'mod_forum/repository';\nimport Templates from 'core/templates';\nimport * as Grader from '../local/grades/grader';\nimport Notification from 'core/notification';\nimport CourseRepository from 'core_course/repository';\nimport {relativeUrl} from 'core/url';\n\nconst templateNames = {\n contentRegion: 'mod_forum/grades/grader/discussion/posts',\n};\n\n/**\n * Curried function with CMID set, this is then used in unified grader as a fetch a users content.\n *\n * @param {Number} cmid\n * @param {Bool} experimentalDisplayMode\n * @return {Function}\n */\nconst getContentForUserIdFunction = (cmid, experimentalDisplayMode) => (userid) => {\n /**\n * Given the parent function is called with the second param set execute the partially executed function.\n *\n * @param {Number} userid\n */\n return Repository.getDiscussionByUserID(userid, cmid)\n .then(context => {\n // Rebuild the returned data for the template.\n context.discussions = context.discussions.map(discussionPostMapper);\n context.experimentaldisplaymode = experimentalDisplayMode ? true : false;\n\n return Templates.render(templateNames.contentRegion, context);\n })\n .catch(Notification.exception);\n};\n\n/**\n * Curried function with CMID set, this is then used in unified grader as a fetch users call.\n * The function curried fetches all users in a course for a given CMID.\n *\n * @param {Number} courseID\n * @param {Number} groupID\n * @param {Boolean} onlyActive Whether to fetch only the active enrolled users or all enrolled users in the course.\n * @return {Array} Array of users for a given context.\n */\nconst getGradableUsersForCourseidFunction = (courseID, groupID, onlyActive) => async() => {\n const context = await CourseRepository.getGradableUsersFromCourseID(courseID, groupID, onlyActive);\n\n return context.users;\n};\n\n\nconst findGradableNode = node => node.closest(Selectors.gradableItem);\n\n/**\n * For a discussion we need to manipulate it's posts to hide certain UI elements.\n *\n * @param {Object} discussion\n * @return {Array} name, id, posts\n */\nconst discussionPostMapper = (discussion) => {\n // Map postid => post.\n const parentMap = new Map();\n discussion.posts.parentposts.forEach(post => parentMap.set(post.id, post));\n const userPosts = discussion.posts.userposts.map(post => {\n post.readonly = true;\n post.hasreplies = false;\n post.replies = [];\n\n const parent = post.parentid ? parentMap.get(post.parentid) : null;\n if (parent) {\n parent.hasreplies = false;\n parent.replies = [];\n parent.readonly = true;\n post.parentauthorname = parent.author.fullname;\n }\n\n return {\n parent,\n post\n };\n });\n\n return {\n ...discussion,\n posts: userPosts,\n };\n};\n\n/**\n * Launch the Grader.\n *\n * @param {HTMLElement} rootNode the root HTML element describing what is to be graded\n * @param {object} param\n * @param {bool} [param.focusOnClose=null]\n */\nconst launchWholeForumGrading = async(rootNode, {\n focusOnClose = null,\n} = {}) => {\n const data = rootNode.dataset;\n const gradingPanelFunctions = await Grader.getGradingPanelFunctions(\n 'mod_forum',\n data.contextid,\n data.gradingComponent,\n data.gradingComponentSubtype,\n data.gradableItemtype\n );\n\n const groupID = data.group ? data.group : 0;\n const onlyActive = data.gradeOnlyActiveUsers;\n\n await Grader.launch(\n getGradableUsersForCourseidFunction(data.courseId, groupID, onlyActive),\n getContentForUserIdFunction(data.cmid, data.experimentalDisplayMode == \"1\"),\n gradingPanelFunctions.getter,\n gradingPanelFunctions.setter,\n {\n groupid: data.groupid,\n initialUserId: data.initialuserid,\n moduleName: data.name,\n courseName: data.courseName,\n courseUrl: relativeUrl('/course/view.php', {id: data.courseId}),\n sendStudentNotifications: data.sendStudentNotifications,\n focusOnClose,\n }\n );\n};\n\n/**\n * Launch the Grader.\n *\n * @param {HTMLElement} rootNode the root HTML element describing what is to be graded\n * @param {object} param\n * @param {bool} [param.focusOnClose=null]\n */\nconst launchViewGrading = async(rootNode, {\n focusOnClose = null,\n} = {}) => {\n const data = rootNode.dataset;\n const gradingPanelFunctions = await Grader.getGradingPanelFunctions(\n 'mod_forum',\n data.contextid,\n data.gradingComponent,\n data.gradingComponentSubtype,\n data.gradableItemtype\n );\n\n await Grader.view(\n gradingPanelFunctions.getter,\n data.userid,\n data.name,\n {\n focusOnClose,\n }\n );\n};\n\n/**\n * Register listeners to launch the grading panel.\n */\nexport const registerLaunchListeners = () => {\n document.addEventListener('click', async(e) => {\n if (e.target.matches(Selectors.launch)) {\n const rootNode = findGradableNode(e.target);\n\n if (!rootNode) {\n throw Error('Unable to find a gradable item');\n }\n\n if (rootNode.matches(Selectors.gradableItems.wholeForum)) {\n // Note: The preventDefault must be before any async function calls because the function becomes async\n // at that point and the default action is implemented.\n e.preventDefault();\n try {\n await launchWholeForumGrading(rootNode, {\n focusOnClose: e.target,\n });\n } catch (error) {\n Notification.exception(error);\n }\n } else {\n throw Error('Unable to find a valid gradable item');\n }\n }\n if (e.target.matches(Selectors.viewGrade)) {\n e.preventDefault();\n const rootNode = findGradableNode(e.target);\n\n if (!rootNode) {\n throw Error('Unable to find a gradable item');\n }\n\n if (rootNode.matches(Selectors.gradableItems.wholeForum)) {\n // Note: The preventDefault must be before any async function calls because the function becomes async\n // at that point and the default action is implemented.\n e.preventDefault();\n try {\n await launchViewGrading(rootNode, {\n focusOnClose: e.target,\n });\n } catch (error) {\n Notification.exception(error);\n }\n } else {\n throw Error('Unable to find a valid gradable item');\n }\n }\n });\n};\n"],"names":["templateNames","getContentForUserIdFunction","cmid","experimentalDisplayMode","userid","Repository","getDiscussionByUserID","then","context","discussions","map","discussionPostMapper","experimentaldisplaymode","Templates","render","catch","Notification","exception","getGradableUsersForCourseidFunction","courseID","groupID","onlyActive","async","CourseRepository","getGradableUsersFromCourseID","users","findGradableNode","node","closest","Selectors","gradableItem","discussion","parentMap","Map","posts","parentposts","forEach","post","set","id","userPosts","userposts","readonly","hasreplies","replies","parent","parentid","get","parentauthorname","author","fullname","document","addEventListener","e","target","matches","launch","rootNode","Error","gradableItems","wholeForum","preventDefault","focusOnClose","data","dataset","gradingPanelFunctions","Grader","getGradingPanelFunctions","contextid","gradingComponent","gradingComponentSubtype","gradableItemtype","group","gradeOnlyActiveUsers","courseId","getter","setter","groupid","initialUserId","initialuserid","moduleName","name","courseName","courseUrl","sendStudentNotifications","launchWholeForumGrading","error","viewGrade","view","launchViewGrading"],"mappings":";;;;;;;mYA8BMA,4BACa,2CAUbC,4BAA8B,CAACC,KAAMC,0BAA6BC,QAM7DC,oBAAWC,sBAAsBF,OAAQF,MAC3CK,MAAKC,UAEFA,QAAQC,YAAcD,QAAQC,YAAYC,IAAIC,sBAC9CH,QAAQI,0BAA0BT,wBAE3BU,mBAAUC,OAAOd,4BAA6BQ,YAExDO,MAAMC,sBAAaC,WAYtBC,oCAAsC,CAACC,SAAUC,QAASC,aAAeC,gBACrDC,qBAAiBC,6BAA6BL,SAAUC,QAASC,aAExEI,MAIbC,iBAAmBC,MAAQA,KAAKC,QAAQC,UAAUC,cAQlDnB,qBAAwBoB,mBAEpBC,UAAY,IAAIC,IACtBF,WAAWG,MAAMC,YAAYC,SAAQC,MAAQL,UAAUM,IAAID,KAAKE,GAAIF,cAC9DG,UAAYT,WAAWG,MAAMO,UAAU/B,KAAI2B,OAC7CA,KAAKK,UAAW,EAChBL,KAAKM,YAAa,EAClBN,KAAKO,QAAU,SAETC,OAASR,KAAKS,SAAWd,UAAUe,IAAIV,KAAKS,UAAY,YAC1DD,SACAA,OAAOF,YAAa,EACpBE,OAAOD,QAAU,GACjBC,OAAOH,UAAW,EAClBL,KAAKW,iBAAmBH,OAAOI,OAAOC,UAGnC,CACHL,OAAAA,OACAR,KAAAA,eAID,IACAN,WACHG,MAAOM,6CA2EwB,KACnCW,SAASC,iBAAiB,SAAS9B,MAAAA,OAC3B+B,EAAEC,OAAOC,QAAQ1B,UAAU2B,QAAS,OAC9BC,SAAW/B,iBAAiB2B,EAAEC,YAE/BG,eACKC,MAAM,sCAGZD,SAASF,QAAQ1B,UAAU8B,cAAcC,kBAYnCF,MAAM,wCATZL,EAAEQ,2BA5EcvC,eAAMmC,cAAUK,aAC5CA,aAAe,6DACf,SACMC,KAAON,SAASO,QAChBC,4BAA8BC,OAAOC,yBACvC,YACAJ,KAAKK,UACLL,KAAKM,iBACLN,KAAKO,wBACLP,KAAKQ,kBAGHnD,QAAU2C,KAAKS,MAAQT,KAAKS,MAAQ,EACpCnD,WAAa0C,KAAKU,2BAElBP,OAAOV,OACTtC,oCAAoC6C,KAAKW,SAAUtD,QAASC,YAC5DpB,4BAA4B8D,KAAK7D,KAAsC,KAAhC6D,KAAK5D,yBAC5C8D,sBAAsBU,OACtBV,sBAAsBW,OACtB,CACIC,QAASd,KAAKc,QACdC,cAAef,KAAKgB,cACpBC,WAAYjB,KAAKkB,KACjBC,WAAYnB,KAAKmB,WACjBC,WAAW,oBAAY,mBAAoB,CAAC5C,GAAIwB,KAAKW,WACrDU,yBAA0BrB,KAAKqB,yBAC/BtB,aAAAA,eAmDcuB,CAAwB5B,SAAU,CACpCK,aAAcT,EAAEC,SAEtB,MAAOgC,6BACQrE,UAAUqE,WAM/BjC,EAAEC,OAAOC,QAAQ1B,UAAU0D,WAAY,CACvClC,EAAEQ,uBACIJ,SAAW/B,iBAAiB2B,EAAEC,YAE/BG,eACKC,MAAM,sCAGZD,SAASF,QAAQ1B,UAAU8B,cAAcC,kBAYnCF,MAAM,wCATZL,EAAEQ,2BA5DQvC,eAAMmC,cAAUK,aACtCA,aAAe,6DACf,SACMC,KAAON,SAASO,QAChBC,4BAA8BC,OAAOC,yBACvC,YACAJ,KAAKK,UACLL,KAAKM,iBACLN,KAAKO,wBACLP,KAAKQ,wBAGHL,OAAOsB,KACTvB,sBAAsBU,OACtBZ,KAAK3D,OACL2D,KAAKkB,KACL,CACInB,aAAAA,eA6Cc2B,CAAkBhC,SAAU,CAC9BK,aAAcT,EAAEC,SAEtB,MAAOgC,6BACQrE,UAAUqE"}PK-B\dnZ Z amd/build/repository.min.jsnu[/** * Forum repository class to encapsulate all of the AJAX requests that subscribe or unsubscribe * can be sent for forum. * * @module mod_forum/repository * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("mod_forum/repository",["core/ajax"],(function(Ajax){return{setDiscussionSubscriptionState:function(forumId,discussionId,targetState){var request={methodname:"mod_forum_set_subscription_state",args:{forumid:forumId,discussionid:discussionId,targetstate:targetState}};return Ajax.call([request])[0]},addDiscussionPost:function(postid,subject,message,messageformat,isprivatereply,topreferredformat){var request={methodname:"mod_forum_add_discussion_post",args:{postid:postid,message:message,messageformat:messageformat,subject:subject,options:[{name:"private",value:isprivatereply},{name:"topreferredformat",value:topreferredformat}]}};return Ajax.call([request])[0]},setDiscussionLockState:function(forumId,discussionId,targetState){var request={methodname:"mod_forum_set_lock_state",args:{forumid:forumId,discussionid:discussionId,targetstate:targetState}};return Ajax.call([request])[0]},setFavouriteDiscussionState:function(forumId,discussionId,targetState){var request={methodname:"mod_forum_toggle_favourite_state",args:{discussionid:discussionId,targetstate:targetState}};return Ajax.call([request])[0]},setPinDiscussionState:function(forumid,discussionid,targetstate){var request={methodname:"mod_forum_set_pin_state",args:{discussionid:discussionid,targetstate:targetstate}};return Ajax.call([request])[0]},getDiscussionByUserID:function(userid,cmid){let sortby=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"modified",sortdirection=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"DESC";var request={methodname:"mod_forum_get_discussion_posts_by_userid",args:{userid:userid,cmid:cmid,sortby:sortby,sortdirection:sortdirection}};return Ajax.call([request])[0]},getDiscussionPosts:function(discussionId){let sortby=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"created",sortdirection=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"ASC";var request={methodname:"mod_forum_get_discussion_posts",args:{discussionid:discussionId,sortby:sortby,sortdirection:sortdirection}};return Ajax.call([request])[0]}}})); //# sourceMappingURL=repository.min.js.mapPK-B\$amd/build/subscription_toggle.min.jsnu[/** * Handle discussion subscription toggling on a discussion list in * the forum view. * * @module mod_forum/subscription_toggle * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("mod_forum/subscription_toggle",["jquery","core/templates","core/notification","mod_forum/repository","mod_forum/selectors","core/pubsub","mod_forum/forum_events"],(function($,Templates,Notification,Repository,Selectors,PubSub,ForumEvents){return{init:function(root,preventDefault,callback){root.on("click",Selectors.subscription.toggle,(function(e){var toggleElement=$(this),forumId=toggleElement.data("forumid"),discussionId=toggleElement.data("discussionid"),subscriptionState=toggleElement.data("targetstate");Repository.setDiscussionSubscriptionState(forumId,discussionId,subscriptionState).then((function(context){return PubSub.publish(ForumEvents.SUBSCRIPTION_TOGGLED,{discussionId:discussionId,subscriptionState:subscriptionState}),callback(toggleElement,context)})).catch(Notification.exception),preventDefault&&e.preventDefault()}))}}})); //# sourceMappingURL=subscription_toggle.min.js.mapPK-B\( 33!amd/build/inpage_reply.min.js.mapnu[{"version":3,"file":"inpage_reply.min.js","sources":["../src/inpage_reply.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * This module handles the in page replying to forum posts.\n *\n * @module mod_forum/inpage_reply\n * @copyright 2019 Peter Dias\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/templates',\n 'core/notification',\n 'mod_forum/repository',\n 'mod_forum/selectors',\n 'core_form/changechecker',\n], function(\n $,\n Templates,\n Notification,\n Repository,\n Selectors,\n FormChangeChecker\n) {\n\n var DISPLAYCONSTANTS = {\n NESTED_V2: 4,\n THREADED: 2,\n NESTED: 3,\n FLAT_OLDEST_FIRST: 1,\n FLAT_NEWEST_FIRST: -1\n };\n\n var EVENTS = {\n POST_CREATED: 'mod_forum-post-created'\n };\n\n /**\n * Moodle formats taken from the FORMAT_* constants declared in lib/weblib.php.\n * @type {Object}\n */\n var CONTENT_FORMATS = {\n MOODLE: 0\n };\n /**\n * Show the loading icon for the submit button.\n *\n * @param {Object} button The submit button element\n */\n var showSubmitButtonLoadingIcon = function(button) {\n var textContainer = button.find(Selectors.post.inpageSubmitBtnText);\n var loadingIconContainer = button.find(Selectors.post.loadingIconContainer);\n var width = button.outerWidth();\n // Fix the width so that the button size doesn't change when we show the loading icon.\n button.css('width', width);\n textContainer.addClass('hidden');\n loadingIconContainer.removeClass('hidden');\n };\n\n /**\n * Hide the loading icon for the submit button.\n *\n * @param {Object} button The submit button element\n */\n var hideSubmitButtonLoadingIcon = function(button) {\n var textContainer = button.find(Selectors.post.inpageSubmitBtnText);\n var loadingIconContainer = button.find(Selectors.post.loadingIconContainer);\n // Reset the width back to it's default.\n button.css('width', '');\n textContainer.removeClass('hidden');\n loadingIconContainer.addClass('hidden');\n };\n\n /**\n * Register the event listeners for the submit/cancel buttons of the in page reply.\n *\n * @param {Object} root The discussion container element.\n */\n var registerEventListeners = function(root) {\n root.on('click', Selectors.post.inpageSubmitBtn, function(e) {\n e.preventDefault();\n var submitButton = $(e.currentTarget);\n var allButtons = submitButton.parent().find(Selectors.post.inpageReplyButton);\n var form = submitButton.parents(Selectors.post.inpageReplyForm).get(0);\n var message = form.elements.post.value.trim();\n // For now, we consider the inline reply post written using the FORMAT_MOODLE (because a textarea is displayed).\n // In the future, other formats should be supported, letting users to use their preferred editor and format.\n var messageformat = CONTENT_FORMATS.MOODLE;\n // The message post will be converted from messageformat to FORMAT_HTML.\n var topreferredformat = true;\n var postid = form.elements.reply.value;\n var subject = form.elements.subject.value;\n var currentRoot = submitButton.closest(Selectors.post.post);\n var isprivatereply = form.elements.privatereply != undefined ? form.elements.privatereply.checked : false;\n var modeSelector = root.find(Selectors.post.modeSelect);\n var mode = modeSelector.length ? parseInt(modeSelector.get(0).value) : null;\n var newid;\n\n if (message.length) {\n showSubmitButtonLoadingIcon(submitButton);\n allButtons.prop('disabled', true);\n\n Repository.addDiscussionPost(postid, subject, message, messageformat, isprivatereply, topreferredformat)\n .then(function(context) {\n var message = context.messages.reduce(function(carry, message) {\n if (message.type == 'success') {\n carry += '

' + message.message + '

';\n }\n return carry;\n }, '');\n Notification.addNotification({\n message: message,\n type: \"success\"\n });\n\n return context;\n })\n .then(function(context) {\n form.reset();\n var post = context.post;\n newid = post.id;\n\n switch (mode) {\n case DISPLAYCONSTANTS.NESTED_V2:\n var capabilities = post.capabilities;\n var currentAuthorName = currentRoot.children()\n .not(Selectors.post.repliesContainer)\n .find(Selectors.post.authorName)\n .text();\n post.parentauthorname = currentAuthorName;\n post.showactionmenu = capabilities.view ||\n capabilities.controlreadstatus ||\n capabilities.edit ||\n capabilities.split ||\n capabilities.delete ||\n capabilities.export ||\n post.urls.viewparent;\n return Templates.render('mod_forum/forum_discussion_nested_v2_post_reply', post);\n case DISPLAYCONSTANTS.THREADED:\n return Templates.render('mod_forum/forum_discussion_threaded_post', post);\n case DISPLAYCONSTANTS.NESTED:\n return Templates.render('mod_forum/forum_discussion_nested_post', post);\n default:\n return Templates.render('mod_forum/forum_discussion_post', post);\n }\n })\n .then(function(html, js) {\n var repliesnode = currentRoot.find(Selectors.post.repliesContainer).first();\n\n if (mode == DISPLAYCONSTANTS.FLAT_NEWEST_FIRST) {\n return Templates.prependNodeContents(repliesnode, html, js);\n } else {\n return Templates.appendNodeContents(repliesnode, html, js);\n }\n })\n .then(function() {\n submitButton.trigger(EVENTS.POST_CREATED, newid);\n hideSubmitButtonLoadingIcon(submitButton);\n allButtons.prop('disabled', false);\n\n // Tell formchangechecker we submitted the form.\n FormChangeChecker.resetFormDirtyState(submitButton[0]);\n\n return currentRoot.find(Selectors.post.inpageReplyContent).hide();\n })\n .then(function() {\n location.href = \"#p\" + newid;\n\n // Reload the page, say if threshold is being set by user those would get reflected through the templates.\n location.reload();\n })\n .catch(function(error) {\n hideSubmitButtonLoadingIcon(submitButton);\n allButtons.prop('disabled', false);\n return Notification.exception(error);\n });\n }\n });\n\n root.on('click', Selectors.post.inpageCancelButton, function(e) {\n // Tell formchangechecker to reset the form state.\n FormChangeChecker.resetFormDirtyState(e.currentTarget);\n });\n };\n\n return {\n init: function(root) {\n registerEventListeners(root);\n },\n CONTENT_FORMATS: CONTENT_FORMATS,\n EVENTS: EVENTS\n };\n});\n"],"names":["define","$","Templates","Notification","Repository","Selectors","FormChangeChecker","DISPLAYCONSTANTS","EVENTS","POST_CREATED","CONTENT_FORMATS","MOODLE","hideSubmitButtonLoadingIcon","button","textContainer","find","post","inpageSubmitBtnText","loadingIconContainer","css","removeClass","addClass","registerEventListeners","root","on","inpageSubmitBtn","e","preventDefault","newid","width","submitButton","currentTarget","allButtons","parent","inpageReplyButton","form","parents","inpageReplyForm","get","message","elements","value","trim","messageformat","postid","reply","subject","currentRoot","closest","isprivatereply","undefined","privatereply","checked","modeSelector","modeSelect","mode","length","parseInt","outerWidth","prop","addDiscussionPost","then","context","messages","reduce","carry","type","addNotification","reset","id","capabilities","currentAuthorName","children","not","repliesContainer","authorName","text","parentauthorname","showactionmenu","view","controlreadstatus","edit","split","delete","export","urls","viewparent","render","html","js","repliesnode","first","prependNodeContents","appendNodeContents","trigger","resetFormDirtyState","inpageReplyContent","hide","location","href","reload","catch","error","exception","inpageCancelButton","init"],"mappings":";;;;;;;AAsBAA,gCAAO,CACH,SACA,iBACA,oBACA,uBACA,sBACA,4BACD,SACCC,EACAC,UACAC,aACAC,WACAC,UACAC,uBAGIC,2BACW,EADXA,0BAEU,EAFVA,wBAGQ,EAHRA,oCAKoB,EAGpBC,OAAS,CACTC,aAAc,0BAOdC,gBAAkB,CAClBC,OAAQ,GAsBRC,4BAA8B,SAASC,YACnCC,cAAgBD,OAAOE,KAAKV,UAAUW,KAAKC,qBAC3CC,qBAAuBL,OAAOE,KAAKV,UAAUW,KAAKE,sBAEtDL,OAAOM,IAAI,QAAS,IACpBL,cAAcM,YAAY,UAC1BF,qBAAqBG,SAAS,WAQ9BC,uBAAyB,SAASC,MAClCA,KAAKC,GAAG,QAASnB,UAAUW,KAAKS,iBAAiB,SAASC,GACtDA,EAAEC,qBAgBEC,MA/C+Bf,OACnCC,cACAI,qBACAW,MA6BIC,aAAe7B,EAAEyB,EAAEK,eACnBC,WAAaF,aAAaG,SAASlB,KAAKV,UAAUW,KAAKkB,mBACvDC,KAAOL,aAAaM,QAAQ/B,UAAUW,KAAKqB,iBAAiBC,IAAI,GAChEC,QAAUJ,KAAKK,SAASxB,KAAKyB,MAAMC,OAGnCC,cAAgBjC,gBAAgBC,OAGhCiC,OAAST,KAAKK,SAASK,MAAMJ,MAC7BK,QAAUX,KAAKK,SAASM,QAAQL,MAChCM,YAAcjB,aAAakB,QAAQ3C,UAAUW,KAAKA,MAClDiC,eAA+CC,MAA9Bf,KAAKK,SAASW,cAA4BhB,KAAKK,SAASW,aAAaC,QACtFC,aAAe9B,KAAKR,KAAKV,UAAUW,KAAKsC,YACxCC,KAAOF,aAAaG,OAASC,SAASJ,aAAaf,IAAI,GAAGG,OAAS,KAGnEF,QAAQiB,SAhDZ1C,eADmCD,OAkDHiB,cAjDTf,KAAKV,UAAUW,KAAKC,qBAC3CC,qBAAuBL,OAAOE,KAAKV,UAAUW,KAAKE,sBAClDW,MAAQhB,OAAO6C,aAEnB7C,OAAOM,IAAI,QAASU,OACpBf,cAAcO,SAAS,UACvBH,qBAAqBE,YAAY,UA4CzBY,WAAW2B,KAAK,YAAY,GAE5BvD,WAAWwD,kBAAkBhB,OAAQE,QAASP,QAASI,cAAeM,gBAblD,GAcfY,MAAK,SAASC,aACPvB,QAAUuB,QAAQC,SAASC,QAAO,SAASC,MAAO1B,eAC9B,WAAhBA,QAAQ2B,OACRD,OAAS,MAAQ1B,QAAQA,QAAU,QAEhC0B,QACR,WACH9D,aAAagE,gBAAgB,CACzB5B,QAASA,QACT2B,KAAM,YAGHJ,WAEVD,MAAK,SAASC,SACX3B,KAAKiC,YACDpD,KAAO8C,QAAQ9C,YACnBY,MAAQZ,KAAKqD,GAELd,WACChD,+BACG+D,aAAetD,KAAKsD,aACpBC,kBAAoBxB,YAAYyB,WACAC,IAAIpE,UAAUW,KAAK0D,kBACnB3D,KAAKV,UAAUW,KAAK2D,YACpBC,cACpC5D,KAAK6D,iBAAmBN,kBACxBvD,KAAK8D,eAAiBR,aAAaS,MACbT,aAAaU,mBACbV,aAAaW,MACbX,aAAaY,OACbZ,aAAaa,QACbb,aAAac,QACbpE,KAAKqE,KAAKC,WACzBpF,UAAUqF,OAAO,kDAAmDvE,WAC1ET,iCACML,UAAUqF,OAAO,2CAA4CvE,WACnET,+BACML,UAAUqF,OAAO,yCAA0CvE,qBAE3Dd,UAAUqF,OAAO,kCAAmCvE,UAGtE6C,MAAK,SAAS2B,KAAMC,QACbC,YAAc3C,YAAYhC,KAAKV,UAAUW,KAAK0D,kBAAkBiB,eAEhEpC,MAAQhD,mCACDL,UAAU0F,oBAAoBF,YAAaF,KAAMC,IAEjDvF,UAAU2F,mBAAmBH,YAAaF,KAAMC,OAG9D5B,MAAK,kBACF/B,aAAagE,QAAQtF,OAAOC,aAAcmB,OAC1ChB,4BAA4BkB,cAC5BE,WAAW2B,KAAK,YAAY,GAG5BrD,kBAAkByF,oBAAoBjE,aAAa,IAE5CiB,YAAYhC,KAAKV,UAAUW,KAAKgF,oBAAoBC,UAE9DpC,MAAK,WACFqC,SAASC,KAAO,KAAOvE,MAGvBsE,SAASE,YAEZC,OAAM,SAASC,cACZ1F,4BAA4BkB,cAC5BE,WAAW2B,KAAK,YAAY,GACrBxD,aAAaoG,UAAUD,cAK9C/E,KAAKC,GAAG,QAASnB,UAAUW,KAAKwF,oBAAoB,SAAS9E,GAEzDpB,kBAAkByF,oBAAoBrE,EAAEK,yBAIzC,CACH0E,KAAM,SAASlF,MACXD,uBAAuBC,OAE3Bb,gBAAiBA,gBACjBF,OAAQA"}PK-B\ȴ66amd/build/inpage_reply.min.jsnu[/** * This module handles the in page replying to forum posts. * * @module mod_forum/inpage_reply * @copyright 2019 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("mod_forum/inpage_reply",["jquery","core/templates","core/notification","mod_forum/repository","mod_forum/selectors","core_form/changechecker"],(function($,Templates,Notification,Repository,Selectors,FormChangeChecker){var DISPLAYCONSTANTS_NESTED_V2=4,DISPLAYCONSTANTS_THREADED=2,DISPLAYCONSTANTS_NESTED=3,DISPLAYCONSTANTS_FLAT_NEWEST_FIRST=-1,EVENTS={POST_CREATED:"mod_forum-post-created"},CONTENT_FORMATS={MOODLE:0},hideSubmitButtonLoadingIcon=function(button){var textContainer=button.find(Selectors.post.inpageSubmitBtnText),loadingIconContainer=button.find(Selectors.post.loadingIconContainer);button.css("width",""),textContainer.removeClass("hidden"),loadingIconContainer.addClass("hidden")},registerEventListeners=function(root){root.on("click",Selectors.post.inpageSubmitBtn,(function(e){e.preventDefault();var newid,button,textContainer,loadingIconContainer,width,submitButton=$(e.currentTarget),allButtons=submitButton.parent().find(Selectors.post.inpageReplyButton),form=submitButton.parents(Selectors.post.inpageReplyForm).get(0),message=form.elements.post.value.trim(),messageformat=CONTENT_FORMATS.MOODLE,postid=form.elements.reply.value,subject=form.elements.subject.value,currentRoot=submitButton.closest(Selectors.post.post),isprivatereply=null!=form.elements.privatereply&&form.elements.privatereply.checked,modeSelector=root.find(Selectors.post.modeSelect),mode=modeSelector.length?parseInt(modeSelector.get(0).value):null;message.length&&(textContainer=(button=submitButton).find(Selectors.post.inpageSubmitBtnText),loadingIconContainer=button.find(Selectors.post.loadingIconContainer),width=button.outerWidth(),button.css("width",width),textContainer.addClass("hidden"),loadingIconContainer.removeClass("hidden"),allButtons.prop("disabled",!0),Repository.addDiscussionPost(postid,subject,message,messageformat,isprivatereply,!0).then((function(context){var message=context.messages.reduce((function(carry,message){return"success"==message.type&&(carry+="

"+message.message+"

"),carry}),"");return Notification.addNotification({message:message,type:"success"}),context})).then((function(context){form.reset();var post=context.post;switch(newid=post.id,mode){case DISPLAYCONSTANTS_NESTED_V2:var capabilities=post.capabilities,currentAuthorName=currentRoot.children().not(Selectors.post.repliesContainer).find(Selectors.post.authorName).text();return post.parentauthorname=currentAuthorName,post.showactionmenu=capabilities.view||capabilities.controlreadstatus||capabilities.edit||capabilities.split||capabilities.delete||capabilities.export||post.urls.viewparent,Templates.render("mod_forum/forum_discussion_nested_v2_post_reply",post);case DISPLAYCONSTANTS_THREADED:return Templates.render("mod_forum/forum_discussion_threaded_post",post);case DISPLAYCONSTANTS_NESTED:return Templates.render("mod_forum/forum_discussion_nested_post",post);default:return Templates.render("mod_forum/forum_discussion_post",post)}})).then((function(html,js){var repliesnode=currentRoot.find(Selectors.post.repliesContainer).first();return mode==DISPLAYCONSTANTS_FLAT_NEWEST_FIRST?Templates.prependNodeContents(repliesnode,html,js):Templates.appendNodeContents(repliesnode,html,js)})).then((function(){return submitButton.trigger(EVENTS.POST_CREATED,newid),hideSubmitButtonLoadingIcon(submitButton),allButtons.prop("disabled",!1),FormChangeChecker.resetFormDirtyState(submitButton[0]),currentRoot.find(Selectors.post.inpageReplyContent).hide()})).then((function(){location.href="#p"+newid,location.reload()})).catch((function(error){return hideSubmitButtonLoadingIcon(submitButton),allButtons.prop("disabled",!1),Notification.exception(error)})))})),root.on("click",Selectors.post.inpageCancelButton,(function(e){FormChangeChecker.resetFormDirtyState(e.currentTarget)}))};return{init:function(root){registerEventListeners(root)},CONTENT_FORMATS:CONTENT_FORMATS,EVENTS:EVENTS}})); //# sourceMappingURL=inpage_reply.min.js.mapPK-B\np2  amd/build/lock_toggle.min.js.mapnu[{"version":3,"file":"lock_toggle.min.js","sources":["../src/lock_toggle.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Handle the manual locking of individual discussions\n *\n * @module mod_forum/lock_toggle\n * @copyright 2019 Peter Dias \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/templates',\n 'core/notification',\n 'mod_forum/repository',\n 'mod_forum/selectors',\n ], function(\n $,\n Templates,\n Notification,\n Repository,\n Selectors\n ) {\n\n /**\n * Register event listeners for the subscription toggle.\n *\n * @param {object} root The discussion list root element\n * @param {boolean} preventDefault Should the default action of the event be prevented\n */\n var registerEventListeners = function(root, preventDefault) {\n root.on('click', Selectors.lock.toggle, function(e) {\n var toggleElement = $(this);\n var forumId = toggleElement.data('forumid');\n var discussionId = toggleElement.data('discussionid');\n var state = toggleElement.data('state');\n\n Repository.setDiscussionLockState(forumId, discussionId, state)\n .then(function() {\n return location.reload();\n })\n .catch(Notification.exception);\n\n if (preventDefault) {\n e.preventDefault();\n }\n });\n };\n\n return {\n init: registerEventListeners\n };\n});\n"],"names":["define","$","Templates","Notification","Repository","Selectors","init","root","preventDefault","on","lock","toggle","e","toggleElement","this","forumId","data","discussionId","state","setDiscussionLockState","then","location","reload","catch","exception"],"mappings":";;;;;;;AAsBAA,+BAAO,CACC,SACA,iBACA,oBACA,uBACA,wBACD,SACCC,EACAC,UACAC,aACAC,WACAC,iBA4BG,CACHC,KApByB,SAASC,KAAMC,gBACxCD,KAAKE,GAAG,QAASJ,UAAUK,KAAKC,QAAQ,SAASC,OACzCC,cAAgBZ,EAAEa,MAClBC,QAAUF,cAAcG,KAAK,WAC7BC,aAAeJ,cAAcG,KAAK,gBAClCE,MAAQL,cAAcG,KAAK,SAE/BZ,WAAWe,uBAAuBJ,QAASE,aAAcC,OACpDE,MAAK,kBACKC,SAASC,YAEnBC,MAAMpB,aAAaqB,WAEpBhB,gBACAI,EAAEJ"}PK-B\k448amd/build/local/grades/local/grader/selectors.min.js.mapnu[{"version":3,"file":"selectors.min.js","sources":["../../../../../src/local/grades/local/grader/selectors.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Define all of the selectors we will be using on the grading interface.\n *\n * @module mod_forum/local/grades/local/grader/selectors\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * A small helper function to build queryable data selectors.\n * @param {String} name\n * @param {String} value\n * @return {string}\n */\nconst getDataSelector = (name, value) => {\n return `[data-${name}=\"${value}\"]`;\n};\n\nexport default {\n buttons: {\n toggleFullscreen: getDataSelector('action', 'togglefullscreen'),\n closeGrader: getDataSelector('action', 'closegrader'),\n collapseGradingDrawer: getDataSelector('action', 'collapse-grading-drawer'),\n saveGrade: getDataSelector('action', 'savegrade'),\n selectUser: getDataSelector('action', 'select-user'),\n toggleSearch: getDataSelector('action', 'toggle-search')\n },\n regions: {\n bodyContainer: getDataSelector('region', 'body-container'),\n moduleContainer: getDataSelector('region', 'module_content_container'),\n moduleReplace: getDataSelector('region', 'module_content'),\n pickerRegion: getDataSelector('region', 'user_picker'),\n gradingInfoContainer: getDataSelector('region', 'grading-info-container'),\n gradingPanel: getDataSelector('region', 'grade'),\n gradingPanelContainer: getDataSelector('region', 'grading-panel-container'),\n gradingPanelErrors: getDataSelector('region', 'grade-errors'),\n searchResultsContainer: getDataSelector('region', 'search-results-container'),\n statusContainer: getDataSelector('region', 'status-container'),\n userSearchContainer: getDataSelector('region', 'user-search-container'),\n userSearchInput: getDataSelector('region', 'user-search-input')\n },\n values: {\n sendStudentNotifications: '[data-region=\"notification\"] input[type=\"radio\"]:checked',\n }\n};\n\n"],"names":["getDataSelector","name","value","buttons","toggleFullscreen","closeGrader","collapseGradingDrawer","saveGrade","selectUser","toggleSearch","regions","bodyContainer","moduleContainer","moduleReplace","pickerRegion","gradingInfoContainer","gradingPanel","gradingPanelContainer","gradingPanelErrors","searchResultsContainer","statusContainer","userSearchContainer","userSearchInput","values","sendStudentNotifications"],"mappings":";;;;;;;;MA6BMA,gBAAkB,CAACC,KAAMC,wBACXD,kBAASC,yBAGd,CACXC,QAAS,CACLC,iBAAkBJ,gBAAgB,SAAU,oBAC5CK,YAAaL,gBAAgB,SAAU,eACvCM,sBAAuBN,gBAAgB,SAAU,2BACjDO,UAAWP,gBAAgB,SAAU,aACrCQ,WAAYR,gBAAgB,SAAU,eACtCS,aAAcT,gBAAgB,SAAU,kBAE5CU,QAAS,CACLC,cAAeX,gBAAgB,SAAU,kBACzCY,gBAAiBZ,gBAAgB,SAAU,4BAC3Ca,cAAeb,gBAAgB,SAAU,kBACzCc,aAAcd,gBAAgB,SAAU,eACxCe,qBAAsBf,gBAAgB,SAAU,0BAChDgB,aAAchB,gBAAgB,SAAU,SACxCiB,sBAAuBjB,gBAAgB,SAAU,2BACjDkB,mBAAoBlB,gBAAgB,SAAU,gBAC9CmB,uBAAwBnB,gBAAgB,SAAU,4BAClDoB,gBAAiBpB,gBAAgB,SAAU,oBAC3CqB,oBAAqBrB,gBAAgB,SAAU,yBAC/CsB,gBAAiBtB,gBAAgB,SAAU,sBAE/CuB,OAAQ,CACJC,yBAA0B"}PK-B\\7amd/build/local/grades/local/grader/gradingpanel.min.jsnu[define("mod_forum/local/grades/local/grader/gradingpanel",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{}; /** * Grading panel functions. * * @module mod_forum/local/grades/local/grader/gradingpanel * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */return _exports.default=async(component,context,gradingComponent,gradingSubtype,itemName)=>{let gradingMethodHandler="".concat(gradingComponent,"/grades/grader/gradingpanel");gradingSubtype&&(gradingMethodHandler+="/".concat(gradingSubtype));const GradingMethod=await("function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require([gradingMethodHandler],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require(gradingMethodHandler)):Promise.resolve(_systemImportTransformerGlobalIdentifier[gradingMethodHandler]));return{getter:userId=>GradingMethod.fetchCurrentGrade(component,context,itemName,userId),setter:(userId,notifyStudent,formData)=>GradingMethod.storeCurrentGrade(component,context,itemName,userId,notifyStudent,formData)}},_exports.default})); //# sourceMappingURL=gradingpanel.min.js.mapPK-B\n * * ;amd/build/local/grades/local/grader/gradingpanel.min.js.mapnu[{"version":3,"file":"gradingpanel.min.js","sources":["../../../../../src/local/grades/local/grader/gradingpanel.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Grading panel functions.\n *\n * @module mod_forum/local/grades/local/grader/gradingpanel\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * Get the grade panel setter and getter for the current component.\n * This function dynamically pulls the relevant gradingpanel JS file defined in the grading method.\n * We do this because we do not know until execution time what the grading type is and we do not want to import unused files.\n *\n * @method\n * @param {String} component The component being graded\n * @param {Number} context The contextid of the thing being graded\n * @param {String} gradingComponent The thing providing the grading type\n * @param {String} gradingSubtype The subtype fo the grading component\n * @param {String} itemName The name of the thing being graded\n * @return {Object}\n */\nexport default async(component, context, gradingComponent, gradingSubtype, itemName) => {\n let gradingMethodHandler = `${gradingComponent}/grades/grader/gradingpanel`;\n if (gradingSubtype) {\n gradingMethodHandler += `/${gradingSubtype}`;\n }\n\n const GradingMethod = await import(gradingMethodHandler);\n\n return {\n getter: (userId) => GradingMethod.fetchCurrentGrade(component, context, itemName, userId),\n setter: (userId, notifyStudent, formData) => GradingMethod.storeCurrentGrade(\n component, context, itemName, userId, notifyStudent, formData),\n };\n};\n\n"],"names":["async","component","context","gradingComponent","gradingSubtype","itemName","gradingMethodHandler","GradingMethod","getter","userId","fetchCurrentGrade","setter","notifyStudent","formData","storeCurrentGrade"],"mappings":";;;;;;;6BAoCeA,MAAMC,UAAWC,QAASC,iBAAkBC,eAAgBC,gBACnEC,+BAA0BH,gDAC1BC,iBACAE,iCAA4BF,uBAG1BG,oOAA6BD,gYAAAA,8BAE5B,CACHE,OAASC,QAAWF,cAAcG,kBAAkBT,UAAWC,QAASG,SAAUI,QAClFE,OAAQ,CAACF,OAAQG,cAAeC,WAAaN,cAAcO,kBACvDb,UAAWC,QAASG,SAAUI,OAAQG,cAAeC"}PK-B\#Damd/build/local/grades/local/grader/user_picker/selectors.min.js.mapnu[{"version":3,"file":"selectors.min.js","sources":["../../../../../../src/local/grades/local/grader/user_picker/selectors.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Define all of the selectors we will be using on the grading interface.\n *\n * @module mod_forum/local/grades/local/grader/user_picker/selectors\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n regions: {\n currentUser: '[data-region=\"user_picker/current_user\"]',\n userRegion: '[data-region=\"user_picker/user\"]',\n },\n actions: {\n changeUser: '[data-action=\"change-user\"]',\n }\n};\n\n"],"names":["regions","currentUser","userRegion","actions","changeUser"],"mappings":"2MAuBe,CACXA,QAAS,CACLC,YAAa,2CACbC,WAAY,oCAEhBC,QAAS,CACLC,WAAY"}PK-B\wS8@amd/build/local/grades/local/grader/user_picker/selectors.min.jsnu[define("mod_forum/local/grades/local/grader/user_picker/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={regions:{currentUser:'[data-region="user_picker/current_user"]',userRegion:'[data-region="user_picker/user"]'},actions:{changeUser:'[data-action="change-user"]'}},_exports.default})); //# sourceMappingURL=selectors.min.js.mapPK-B\Ҹ8}##:amd/build/local/grades/local/grader/user_picker.min.js.mapnu[{"version":3,"file":"user_picker.min.js","sources":["../../../../../src/local/grades/local/grader/user_picker.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * This module will tie together all of the different calls the gradable module will make.\n *\n * @module mod_forum/local/grades/local/grader/user_picker\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport Selectors from './user_picker/selectors';\nimport {getString} from 'core/str';\n\nconst templatePath = 'mod_forum/local/grades/local/grader';\n\n/**\n * The Grader User Picker.\n *\n * @class mod_forum/local/grades/local/grader/user_picker\n */\nclass UserPicker {\n\n /**\n * Constructor for the User Picker.\n *\n * @constructor mod_forum/local/grades/local/grader/user_picker\n * @param {Array} userList List of users\n * @param {Function} showUserCallback The callback used to display the user\n * @param {Function} preChangeUserCallback The callback to use before changing user\n */\n constructor(userList, showUserCallback, preChangeUserCallback) {\n this.userList = userList;\n this.showUserCallback = showUserCallback;\n this.preChangeUserCallback = preChangeUserCallback;\n this.currentUserIndex = 0;\n\n // Ensure that render is bound correctly.\n this.render = this.render.bind(this);\n this.setUserId = this.setUserId.bind(this);\n }\n\n /**\n * Set the current userid without rendering the change.\n * To show the user, call showUser too.\n *\n * @param {Number} userId\n */\n setUserId(userId) {\n // Determine the current index based on the user ID.\n const userIndex = this.userList.findIndex(user => {\n return user.id === parseInt(userId);\n });\n\n if (userIndex === -1) {\n throw Error(`User with id ${userId} not found`);\n }\n\n this.currentUserIndex = userIndex;\n }\n\n /**\n * Render the user picker.\n */\n async render() {\n // Create the root node.\n this.root = document.createElement('div');\n\n const {html, js} = await this.renderNavigator();\n Templates.replaceNodeContents(this.root, html, js);\n\n // Call the showUser function to show the first user immediately.\n await this.showUser(this.currentUser);\n\n // Ensure that the event listeners are all bound.\n this.registerEventListeners();\n }\n\n /**\n * Render the navigator itself.\n *\n * @returns {Promise}\n */\n renderNavigator() {\n return Templates.renderForPromise(`${templatePath}/user_picker`, {});\n }\n\n /**\n * Render the current user details for the picker.\n *\n * @param {Object} context The data used to render the user picker.\n * @returns {Promise}\n */\n renderUserChange(context) {\n return Templates.renderForPromise(`${templatePath}/user_picker/user`, context);\n }\n\n /**\n * Show the specified user in the picker.\n *\n * @param {Object} user\n */\n async showUser(user) {\n const [{html, js}] = await Promise.all([this.renderUserChange(user), this.showUserCallback(user)]);\n const userRegion = this.root.querySelector(Selectors.regions.userRegion);\n Templates.replaceNodeContents(userRegion, html, js);\n\n // Update the hidden now-grading region so screen readers can announce the user that's currently being graded.\n const currentUserRegion = this.root.querySelector(Selectors.regions.currentUser);\n currentUserRegion.textContent = await getString('nowgradinguser', 'mod_forum', user.fullname);\n }\n\n /**\n * Register the event listeners for the user picker.\n */\n registerEventListeners() {\n this.root.addEventListener('click', async(e) => {\n const button = e.target.closest(Selectors.actions.changeUser);\n\n if (button) {\n const result = await this.preChangeUserCallback(this.currentUser);\n\n if (!result.failed) {\n this.updateIndex(parseInt(button.dataset.direction));\n await this.showUser(this.currentUser);\n }\n }\n });\n }\n\n /**\n * Update the current user index.\n *\n * @param {Number} direction\n * @returns {Number}}\n */\n updateIndex(direction) {\n this.currentUserIndex += direction;\n\n // Loop around the edges.\n if (this.currentUserIndex < 0) {\n this.currentUserIndex = this.userList.length - 1;\n } else if (this.currentUserIndex > this.userList.length - 1) {\n this.currentUserIndex = 0;\n }\n\n return this.currentUserIndex;\n }\n\n /**\n * Get the details of the user currently shown with the total number of users, and the 1-indexed count of the\n * current user.\n *\n * @returns {Object}\n */\n get currentUser() {\n return {\n ...this.userList[this.currentUserIndex],\n total: this.userList.length,\n displayIndex: this.currentUserIndex + 1,\n };\n }\n\n /**\n * Get the root node for the User Picker.\n *\n * @returns {HTMLElement}\n */\n get rootNode() {\n return this.root;\n }\n}\n\n/**\n * Create a new user picker.\n *\n * @param {Array} users The list of users\n * @param {Function} showUserCallback The function to call to show a specific user\n * @param {Function} preChangeUserCallback The fucntion to call to save the grade for the current user\n * @param {Number} [currentUserID] The userid of the current user\n * @returns {UserPicker}\n */\nexport default async(\n users,\n showUserCallback,\n preChangeUserCallback,\n {\n initialUserId = null,\n } = {}\n) => {\n const userPicker = new UserPicker(users, showUserCallback, preChangeUserCallback);\n if (initialUserId) {\n userPicker.setUserId(initialUserId);\n }\n await userPicker.render();\n\n return userPicker;\n};\n"],"names":["UserPicker","constructor","userList","showUserCallback","preChangeUserCallback","currentUserIndex","render","this","bind","setUserId","userId","userIndex","findIndex","user","id","parseInt","Error","root","document","createElement","html","js","renderNavigator","replaceNodeContents","showUser","currentUser","registerEventListeners","Templates","renderForPromise","renderUserChange","context","Promise","all","userRegion","querySelector","Selectors","regions","textContent","fullname","addEventListener","async","button","e","target","closest","actions","changeUser","failed","updateIndex","dataset","direction","length","total","displayIndex","rootNode","users","initialUserId","userPicker"],"mappings":";;;;;;;uLAkCMA,WAUFC,YAAYC,SAAUC,iBAAkBC,4BAC/BF,SAAWA,cACXC,iBAAmBA,sBACnBC,sBAAwBA,2BACxBC,iBAAmB,OAGnBC,OAASC,KAAKD,OAAOE,KAAKD,WAC1BE,UAAYF,KAAKE,UAAUD,KAAKD,MASzCE,UAAUC,cAEAC,UAAYJ,KAAKL,SAASU,WAAUC,MAC/BA,KAAKC,KAAOC,SAASL,cAGb,IAAfC,gBACMK,6BAAsBN,2BAG3BL,iBAAmBM,8BAQnBM,KAAOC,SAASC,cAAc,aAE7BC,KAACA,KAADC,GAAOA,UAAYd,KAAKe,qCACpBC,oBAAoBhB,KAAKU,KAAMG,KAAMC,UAGzCd,KAAKiB,SAASjB,KAAKkB,kBAGpBC,yBAQTJ,yBACWK,mBAAUC,2BAtEJ,sDAsEoD,IASrEC,iBAAiBC,gBACNH,mBAAUC,2BAhFJ,2DAgFyDE,wBAQ3DjB,aACJO,KAACA,KAADC,GAAOA,WAAaU,QAAQC,IAAI,CAACzB,KAAKsB,iBAAiBhB,MAAON,KAAKJ,iBAAiBU,QACrFoB,WAAa1B,KAAKU,KAAKiB,cAAcC,mBAAUC,QAAQH,+BACnDV,oBAAoBU,WAAYb,KAAMC,IAGtBd,KAAKU,KAAKiB,cAAcC,mBAAUC,QAAQX,aAClDY,kBAAoB,kBAAU,iBAAkB,YAAaxB,KAAKyB,UAMxFZ,8BACST,KAAKsB,iBAAiB,SAASC,MAAAA,UAC1BC,OAASC,EAAEC,OAAOC,QAAQT,mBAAUU,QAAQC,eAE9CL,OAAQ,QACalC,KAAKH,sBAAsBG,KAAKkB,cAEzCsB,cACHC,YAAYjC,SAAS0B,OAAOQ,QAAQC,kBACnC3C,KAAKiB,SAASjB,KAAKkB,kBAYzCuB,YAAYE,uBACH7C,kBAAoB6C,UAGrB3C,KAAKF,iBAAmB,OACnBA,iBAAmBE,KAAKL,SAASiD,OAAS,EACxC5C,KAAKF,iBAAmBE,KAAKL,SAASiD,OAAS,SACjD9C,iBAAmB,GAGrBE,KAAKF,iBASZoB,wBACO,IACAlB,KAAKL,SAASK,KAAKF,kBACtB+C,MAAO7C,KAAKL,SAASiD,OACrBE,aAAc9C,KAAKF,iBAAmB,GAS1CiD,sBACO/C,KAAKU,8BAaLuB,eACXe,MACApD,iBACAC,2BACAoD,cACIA,cAAgB,6DAChB,SAEEC,WAAa,IAAIzD,WAAWuD,MAAOpD,iBAAkBC,8BACvDoD,eACAC,WAAWhD,UAAU+C,qBAEnBC,WAAWnD,SAEVmD"}PK-B\qX>>4amd/build/local/grades/local/grader/selectors.min.jsnu[define("mod_forum/local/grades/local/grader/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0; /** * Define all of the selectors we will be using on the grading interface. * * @module mod_forum/local/grades/local/grader/selectors * @copyright 2019 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ const getDataSelector=(name,value)=>"[data-".concat(name,'="').concat(value,'"]');var _default={buttons:{toggleFullscreen:getDataSelector("action","togglefullscreen"),closeGrader:getDataSelector("action","closegrader"),collapseGradingDrawer:getDataSelector("action","collapse-grading-drawer"),saveGrade:getDataSelector("action","savegrade"),selectUser:getDataSelector("action","select-user"),toggleSearch:getDataSelector("action","toggle-search")},regions:{bodyContainer:getDataSelector("region","body-container"),moduleContainer:getDataSelector("region","module_content_container"),moduleReplace:getDataSelector("region","module_content"),pickerRegion:getDataSelector("region","user_picker"),gradingInfoContainer:getDataSelector("region","grading-info-container"),gradingPanel:getDataSelector("region","grade"),gradingPanelContainer:getDataSelector("region","grading-panel-container"),gradingPanelErrors:getDataSelector("region","grade-errors"),searchResultsContainer:getDataSelector("region","search-results-container"),statusContainer:getDataSelector("region","status-container"),userSearchContainer:getDataSelector("region","user-search-container"),userSearchInput:getDataSelector("region","user-search-input")},values:{sendStudentNotifications:'[data-region="notification"] input[type="radio"]:checked'}};return _exports.default=_default,_exports.default})); //# sourceMappingURL=selectors.min.js.mapPK-B\S- 6amd/build/local/grades/local/grader/user_picker.min.jsnu[define("mod_forum/local/grades/local/grader/user_picker",["exports","core/templates","./user_picker/selectors","core/str"],(function(_exports,_templates,_selectors,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * This module will tie together all of the different calls the gradable module will make. * * @module mod_forum/local/grades/local/grader/user_picker * @copyright 2019 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=_interopRequireDefault(_templates),_selectors=_interopRequireDefault(_selectors);class UserPicker{constructor(userList,showUserCallback,preChangeUserCallback){this.userList=userList,this.showUserCallback=showUserCallback,this.preChangeUserCallback=preChangeUserCallback,this.currentUserIndex=0,this.render=this.render.bind(this),this.setUserId=this.setUserId.bind(this)}setUserId(userId){const userIndex=this.userList.findIndex((user=>user.id===parseInt(userId)));if(-1===userIndex)throw Error("User with id ".concat(userId," not found"));this.currentUserIndex=userIndex}async render(){this.root=document.createElement("div");const{html:html,js:js}=await this.renderNavigator();_templates.default.replaceNodeContents(this.root,html,js),await this.showUser(this.currentUser),this.registerEventListeners()}renderNavigator(){return _templates.default.renderForPromise("".concat("mod_forum/local/grades/local/grader","/user_picker"),{})}renderUserChange(context){return _templates.default.renderForPromise("".concat("mod_forum/local/grades/local/grader","/user_picker/user"),context)}async showUser(user){const[{html:html,js:js}]=await Promise.all([this.renderUserChange(user),this.showUserCallback(user)]),userRegion=this.root.querySelector(_selectors.default.regions.userRegion);_templates.default.replaceNodeContents(userRegion,html,js);this.root.querySelector(_selectors.default.regions.currentUser).textContent=await(0,_str.getString)("nowgradinguser","mod_forum",user.fullname)}registerEventListeners(){this.root.addEventListener("click",(async e=>{const button=e.target.closest(_selectors.default.actions.changeUser);if(button){(await this.preChangeUserCallback(this.currentUser)).failed||(this.updateIndex(parseInt(button.dataset.direction)),await this.showUser(this.currentUser))}}))}updateIndex(direction){return this.currentUserIndex+=direction,this.currentUserIndex<0?this.currentUserIndex=this.userList.length-1:this.currentUserIndex>this.userList.length-1&&(this.currentUserIndex=0),this.currentUserIndex}get currentUser(){return{...this.userList[this.currentUserIndex],total:this.userList.length,displayIndex:this.currentUserIndex+1}}get rootNode(){return this.root}}return _exports.default=async function(users,showUserCallback,preChangeUserCallback){let{initialUserId:initialUserId=null}=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};const userPicker=new UserPicker(users,showUserCallback,preChangeUserCallback);return initialUserId&&userPicker.setUserId(initialUserId),await userPicker.render(),userPicker},_exports.default})); //# sourceMappingURL=user_picker.min.js.mapPK-B\--$amd/build/local/grades/grader.min.jsnu[define("mod_forum/local/grades/grader",["exports","core/templates","./local/grader/selectors","./local/grader/user_picker","mod_forum/local/layout/fullscreen","./local/grader/gradingpanel","core/toast","core/notification","core/str","core_grades/grades/grader/gradingpanel/normalise","core/loadingicon","core/utils","core_grades/grades/grader/gradingpanel/comparison","core/modal_cancel","core/pubsub","core/drawer_events"],(function(_exports,_templates,_selectors,_user_picker,_fullscreen,_gradingpanel,_toast,_notification,_str,_normalise,_loadingicon,_utils,_comparison,_modal_cancel,_pubsub,_drawer_events){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * This module will tie together all of the different calls the gradable module will make. * * @module mod_forum/local/grades/grader * @copyright 2019 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),Object.defineProperty(_exports,"getGradingPanelFunctions",{enumerable:!0,get:function(){return _gradingpanel.default}}),_exports.view=_exports.launch=void 0,_templates=_interopRequireDefault(_templates),_selectors=_interopRequireDefault(_selectors),_user_picker=_interopRequireDefault(_user_picker),_gradingpanel=_interopRequireDefault(_gradingpanel),_modal_cancel=_interopRequireDefault(_modal_cancel),_drawer_events=_interopRequireDefault(_drawer_events);const templateNames_grader={app:"mod_forum/local/grades/grader",gradingPanel:{error:"mod_forum/local/grades/local/grader/gradingpanel/error"},searchResults:"mod_forum/local/grades/local/grader/user_picker/user_search",status:"mod_forum/local/grades/local/grader/status"},displayUserPicker=(root,html)=>{const pickerRegion=root.querySelector(_selectors.default.regions.pickerRegion);_templates.default.replaceNodeContents(pickerRegion,html,"")},fetchContentFromRender=(html,js)=>[html,js],getUpdateUserContentFunction=(root,getContentForUser,getGradeForUser,saveGradeForUser)=>{let firstLoad=!0;return async user=>{const spinner=firstLoad?null:(0,_loadingicon.addIconToContainerWithPromise)(root),[[html,js],userGrade]=await Promise.all([getContentForUser(user.id).then(fetchContentFromRender),getGradeForUser(user.id)]);_templates.default.replaceNodeContents(root.querySelector(_selectors.default.regions.moduleReplace),html,js);const[gradingPanelHtml,gradingPanelJS]=await _templates.default.render(userGrade.templatename,userGrade.grade).then(fetchContentFromRender),panelContainer=root.querySelector(_selectors.default.regions.gradingPanelContainer),panel=panelContainer.querySelector(_selectors.default.regions.gradingPanel);_templates.default.replaceNodeContents(panel,gradingPanelHtml,gradingPanelJS);const form=panel.querySelector("form");return(0,_comparison.fillInitialValues)(form),form.addEventListener("submit",(event=>{saveGradeForUser(user),event.preventDefault()})),panelContainer.scrollTop=0,firstLoad=!1,spinner&&spinner.resolve(),userGrade}},hideSearchResultContainer=(bodyContainer,userPickerContainer,searchResultsContainer)=>{bodyContainer.classList.remove("hidden"),userPickerContainer.classList.remove("hidden"),searchResultsContainer.classList.add("hidden")},hideUserSearchInput=(toggleSearchButton,searchContainer,searchInput)=>{searchContainer.classList.add("collapsed"),toggleSearchButton.setAttribute("aria-expanded","false"),toggleSearchButton.classList.add("collapse"),toggleSearchButton.classList.remove("expand"),toggleSearchButton.focus();searchContainer.parentElement.querySelector(_selectors.default.regions.gradingInfoContainer).removeAttribute("aria-hidden");const collapseGradingDrawer=searchContainer.parentElement.querySelector(_selectors.default.buttons.collapseGradingDrawer);collapseGradingDrawer.removeAttribute("aria-hidden"),collapseGradingDrawer.setAttribute("tabindex","0"),searchInput.value=""},renderSearchResults=async(searchResultsContainer,users)=>{const{html:html,js:js}=await _templates.default.renderForPromise(templateNames_grader.searchResults,{users:users});_templates.default.replaceNodeContents(searchResultsContainer,html,js)},registerEventListeners=(graderLayout,userPicker,saveGradeFunction,userList)=>{const graderContainer=graderLayout.getContainer(),toggleSearchButton=graderContainer.querySelector(_selectors.default.buttons.toggleSearch),searchInputContainer=graderContainer.querySelector(_selectors.default.regions.userSearchContainer),searchInput=searchInputContainer.querySelector(_selectors.default.regions.userSearchInput),bodyContainer=graderContainer.querySelector(_selectors.default.regions.bodyContainer),userPickerContainer=graderContainer.querySelector(_selectors.default.regions.pickerRegion),searchResultsContainer=graderContainer.querySelector(_selectors.default.regions.searchResultsContainer);graderContainer.addEventListener("click",(e=>{if(e.target.closest(_selectors.default.buttons.toggleFullscreen))return e.stopImmediatePropagation(),e.preventDefault(),void graderLayout.toggleFullscreen();if(e.target.closest(_selectors.default.buttons.closeGrader))return e.stopImmediatePropagation(),e.preventDefault(),void graderLayout.close();if(e.target.closest(_selectors.default.buttons.saveGrade)&&saveGradeFunction(userPicker.currentUser),e.target.closest(_selectors.default.buttons.toggleSearch))return void("true"===toggleSearchButton.getAttribute("aria-expanded")?(hideUserSearchInput(toggleSearchButton,searchInputContainer,searchInput),hideSearchResultContainer(bodyContainer,userPickerContainer,searchResultsContainer),searchResultsContainer.innerHTML=""):(((toggleSearchButton,searchContainer,searchInput)=>{searchContainer.classList.remove("collapsed"),toggleSearchButton.setAttribute("aria-expanded","true"),toggleSearchButton.classList.add("expand"),toggleSearchButton.classList.remove("collapse"),searchContainer.parentElement.querySelector(_selectors.default.regions.gradingInfoContainer).setAttribute("aria-hidden","true");const collapseGradingDrawer=searchContainer.parentElement.querySelector(_selectors.default.buttons.collapseGradingDrawer);collapseGradingDrawer.setAttribute("aria-hidden","true"),collapseGradingDrawer.setAttribute("tabindex","-1"),searchInput.focus()})(toggleSearchButton,searchInputContainer,searchInput),((bodyContainer,userPickerContainer,searchResultsContainer)=>{bodyContainer.classList.add("hidden"),userPickerContainer.classList.add("hidden"),searchResultsContainer.classList.remove("hidden")})(bodyContainer,userPickerContainer,searchResultsContainer),renderSearchResults(searchResultsContainer,userList)));const selectUserButton=e.target.closest(_selectors.default.buttons.selectUser);if(selectUserButton){const userId=selectUserButton.getAttribute("data-userid"),user=userList.find((user=>user.id==userId));userPicker.setUserId(userId),userPicker.showUser(user),hideUserSearchInput(toggleSearchButton,searchInputContainer,searchInput),hideSearchResultContainer(bodyContainer,userPickerContainer,searchResultsContainer),searchResultsContainer.innerHTML=""}})),searchInput.addEventListener("input",(0,_utils.debounce)((()=>{const users=((userList,searchTerm)=>""===searchTerm?userList:(searchTerm=searchTerm.toLowerCase(),userList.filter((user=>user.fullname.toLowerCase().includes(searchTerm)))))(userList,searchInput.value);renderSearchResults(searchResultsContainer,users)}),300)),(0,_pubsub.subscribe)(_drawer_events.default.DRAWER_HIDDEN,(drawerRoot=>{drawerRoot[0].querySelector(_selectors.default.regions.gradingPanel)&&setContentContainerMargin(graderContainer,0)})),(0,_pubsub.subscribe)(_drawer_events.default.DRAWER_SHOWN,(drawerRoot=>{const gradingPanel=drawerRoot[0];gradingPanel.querySelector(_selectors.default.regions.gradingPanel)&&setContentContainerMargin(graderContainer,gradingPanel.offsetWidth)}))},setContentContainerMargin=(graderContainer,rightMargin)=>{const contentContainer=graderContainer.querySelector(_selectors.default.regions.moduleContainer);contentContainer&&(contentContainer.style.marginRight="".concat(rightMargin,"px"))},getSaveUserGradeFunction=(root,setGradeForUser)=>async user=>{try{root.querySelector(_selectors.default.regions.gradingPanelErrors).innerHTML="";const result=await setGradeForUser(user.id,root.querySelector(_selectors.default.values.sendStudentNotifications).value,root.querySelector(_selectors.default.regions.gradingPanel));return result.success&&(0,_toast.add)(await(0,_str.getString)("grades:gradesavedfor","mod_forum",user)),result.failed&&displayGradingError(root,user,result.error),result}catch(err){return displayGradingError(root,user,err),(0,_normalise.failedUpdate)(err)}},displayGradingError=async(root,user,err)=>{const[{html:html,js:js},errorString]=await Promise.all([_templates.default.renderForPromise(templateNames_grader.gradingPanel.error,{error:err}),await(0,_str.getString)("grades:gradesavefailed","mod_forum",{error:err.message,...user})]);_templates.default.replaceNodeContents(root.querySelector(_selectors.default.regions.gradingPanelErrors),html,js),(0,_toast.add)(errorString,{type:"warning"})};_exports.launch=async function(getListOfUsers,getContentForUser,getGradeForUser,setGradeForUser){let{initialUserId:initialUserId=null,moduleName:moduleName,courseName:courseName,courseUrl:courseUrl,sendStudentNotifications:sendStudentNotifications,focusOnClose:focusOnClose=null}=arguments.length>4&&void 0!==arguments[4]?arguments[4]:{};const userList=await getListOfUsers();if(!userList.length)return void(0,_notification.addNotification)({message:await(0,_str.getString)("nouserstograde","core_grades"),type:"error"});const[graderLayout,{html:html,js:js}]=await Promise.all([(0,_fullscreen.createLayout)({fullscreen:!1,showLoader:!1,focusOnClose:focusOnClose}),_templates.default.renderForPromise(templateNames_grader.app,{moduleName:moduleName,courseName:courseName,courseUrl:courseUrl,drawer:{show:!0},defaultsendnotifications:sendStudentNotifications})]),graderContainer=graderLayout.getContainer(),saveGradeFunction=getSaveUserGradeFunction(graderContainer,setGradeForUser);_templates.default.replaceNodeContents(graderContainer,html,js);const updateUserContent=getUpdateUserContentFunction(graderContainer,getContentForUser,getGradeForUser,saveGradeFunction),userIds=userList.map((user=>user.id)),statusContainer=graderContainer.querySelector(_selectors.default.regions.statusContainer),userPicker=await(0,_user_picker.default)(userList,(async user=>{const renderContext={status:(await updateUserContent(user)).hasgrade,index:userIds.indexOf(user.id)+1,total:userList.length};_templates.default.render(templateNames_grader.status,renderContext).then((html=>(statusContainer.innerHTML=html,html))).catch()}),saveGradeFunction,{initialUserId:initialUserId});registerEventListeners(graderLayout,userPicker,saveGradeFunction,userList),displayUserPicker(graderContainer,userPicker.rootNode)};_exports.view=async function(getGradeForUser,userid,moduleName){let{focusOnClose:focusOnClose=null}=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};const userGrade=await getGradeForUser(userid),[modal,gradeTemplateData]=await Promise.all([_modal_cancel.default.create({title:moduleName,large:!0,removeOnClose:!0,returnElement:focusOnClose,show:!0,body:_templates.default.render("mod_forum/local/grades/view_grade",userGrade)}),renderGradeTemplate(userGrade)]),bodyPromise=await modal.getBodyPromise(),gradeReplace=bodyPromise[0].querySelector('[data-region="grade-template"]');_templates.default.replaceNodeContents(gradeReplace,gradeTemplateData.html,gradeTemplateData.js)};const renderGradeTemplate=userGrade=>_templates.default.renderForPromise(userGrade.templatename,userGrade.grade)})); //# sourceMappingURL=grader.min.js.mapPK-B\Bnn(amd/build/local/grades/grader.min.js.mapnu[{"version":3,"file":"grader.min.js","sources":["../../../src/local/grades/grader.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * This module will tie together all of the different calls the gradable module will make.\n *\n * @module mod_forum/local/grades/grader\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Templates from 'core/templates';\nimport Selectors from './local/grader/selectors';\nimport getUserPicker from './local/grader/user_picker';\nimport {createLayout as createFullScreenWindow} from 'mod_forum/local/layout/fullscreen';\nimport getGradingPanelFunctions from './local/grader/gradingpanel';\nimport {add as addToast} from 'core/toast';\nimport {addNotification} from 'core/notification';\nimport {getString} from 'core/str';\nimport {failedUpdate} from 'core_grades/grades/grader/gradingpanel/normalise';\nimport {addIconToContainerWithPromise} from 'core/loadingicon';\nimport {debounce} from 'core/utils';\nimport {fillInitialValues} from 'core_grades/grades/grader/gradingpanel/comparison';\nimport Modal from 'core/modal_cancel';\nimport {subscribe} from 'core/pubsub';\nimport DrawerEvents from 'core/drawer_events';\n\nconst templateNames = {\n grader: {\n app: 'mod_forum/local/grades/grader',\n gradingPanel: {\n error: 'mod_forum/local/grades/local/grader/gradingpanel/error',\n },\n searchResults: 'mod_forum/local/grades/local/grader/user_picker/user_search',\n status: 'mod_forum/local/grades/local/grader/status',\n },\n};\n\n/**\n * Helper function that replaces the user picker placeholder with what we get back from the user picker class.\n *\n * @param {HTMLElement} root\n * @param {String} html\n */\nconst displayUserPicker = (root, html) => {\n const pickerRegion = root.querySelector(Selectors.regions.pickerRegion);\n Templates.replaceNodeContents(pickerRegion, html, '');\n};\n\n/**\n * To be removed, this is now done as a part of Templates.renderForPromise()\n *\n * @param {String} html\n * @param {String} js\n * @returns {array} An array containing the HTML, and JS.\n */\nconst fetchContentFromRender = (html, js) => {\n return [html, js];\n};\n\n/**\n * Here we build the function that is passed to the user picker that'll handle updating the user content area\n * of the grading interface.\n *\n * @param {HTMLElement} root\n * @param {Function} getContentForUser\n * @param {Function} getGradeForUser\n * @param {Function} saveGradeForUser\n * @return {Function}\n */\nconst getUpdateUserContentFunction = (root, getContentForUser, getGradeForUser, saveGradeForUser) => {\n let firstLoad = true;\n\n return async(user) => {\n const spinner = firstLoad ? null : addIconToContainerWithPromise(root);\n const [\n [html, js],\n userGrade,\n ] = await Promise.all([\n getContentForUser(user.id).then(fetchContentFromRender),\n getGradeForUser(user.id),\n ]);\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.moduleReplace), html, js);\n\n const [\n gradingPanelHtml,\n gradingPanelJS\n ] = await Templates.render(userGrade.templatename, userGrade.grade).then(fetchContentFromRender);\n const panelContainer = root.querySelector(Selectors.regions.gradingPanelContainer);\n const panel = panelContainer.querySelector(Selectors.regions.gradingPanel);\n Templates.replaceNodeContents(panel, gradingPanelHtml, gradingPanelJS);\n\n const form = panel.querySelector('form');\n fillInitialValues(form);\n\n form.addEventListener('submit', event => {\n saveGradeForUser(user);\n event.preventDefault();\n });\n\n panelContainer.scrollTop = 0;\n firstLoad = false;\n\n if (spinner) {\n spinner.resolve();\n }\n return userGrade;\n };\n};\n\n/**\n * Show the search results container and hide the user picker and body content.\n *\n * @param {HTMLElement} bodyContainer The container element for the body content\n * @param {HTMLElement} userPickerContainer The container element for the user picker\n * @param {HTMLElement} searchResultsContainer The container element for the search results\n */\nconst showSearchResultContainer = (bodyContainer, userPickerContainer, searchResultsContainer) => {\n bodyContainer.classList.add('hidden');\n userPickerContainer.classList.add('hidden');\n searchResultsContainer.classList.remove('hidden');\n};\n\n/**\n * Hide the search results container and show the user picker and body content.\n *\n * @param {HTMLElement} bodyContainer The container element for the body content\n * @param {HTMLElement} userPickerContainer The container element for the user picker\n * @param {HTMLElement} searchResultsContainer The container element for the search results\n */\nconst hideSearchResultContainer = (bodyContainer, userPickerContainer, searchResultsContainer) => {\n bodyContainer.classList.remove('hidden');\n userPickerContainer.classList.remove('hidden');\n searchResultsContainer.classList.add('hidden');\n};\n\n/**\n * Toggles the visibility of the user search.\n *\n * @param {HTMLElement} toggleSearchButton The button that toggles the search\n * @param {HTMLElement} searchContainer The container element for the user search\n * @param {HTMLElement} searchInput The input element for searching\n */\nconst showUserSearchInput = (toggleSearchButton, searchContainer, searchInput) => {\n searchContainer.classList.remove('collapsed');\n toggleSearchButton.setAttribute('aria-expanded', 'true');\n toggleSearchButton.classList.add('expand');\n toggleSearchButton.classList.remove('collapse');\n\n // Hide the grading info container from screen reader.\n const gradingInfoContainer = searchContainer.parentElement.querySelector(Selectors.regions.gradingInfoContainer);\n gradingInfoContainer.setAttribute('aria-hidden', 'true');\n\n // Hide the collapse grading drawer button from screen reader.\n const collapseGradingDrawer = searchContainer.parentElement.querySelector(Selectors.buttons.collapseGradingDrawer);\n collapseGradingDrawer.setAttribute('aria-hidden', 'true');\n collapseGradingDrawer.setAttribute('tabindex', '-1');\n\n searchInput.focus();\n};\n\n/**\n * Toggles the visibility of the user search.\n *\n * @param {HTMLElement} toggleSearchButton The button that toggles the search\n * @param {HTMLElement} searchContainer The container element for the user search\n * @param {HTMLElement} searchInput The input element for searching\n */\nconst hideUserSearchInput = (toggleSearchButton, searchContainer, searchInput) => {\n searchContainer.classList.add('collapsed');\n toggleSearchButton.setAttribute('aria-expanded', 'false');\n toggleSearchButton.classList.add('collapse');\n toggleSearchButton.classList.remove('expand');\n toggleSearchButton.focus();\n\n // Show the grading info container to screen reader.\n const gradingInfoContainer = searchContainer.parentElement.querySelector(Selectors.regions.gradingInfoContainer);\n gradingInfoContainer.removeAttribute('aria-hidden');\n\n // Show the collapse grading drawer button from screen reader.\n const collapseGradingDrawer = searchContainer.parentElement.querySelector(Selectors.buttons.collapseGradingDrawer);\n collapseGradingDrawer.removeAttribute('aria-hidden');\n collapseGradingDrawer.setAttribute('tabindex', '0');\n\n searchInput.value = '';\n};\n\n/**\n * Find the list of users who's names include the given search term.\n *\n * @param {Array} userList List of users for the grader\n * @param {String} searchTerm The search term to match\n * @return {Array}\n */\nconst searchForUsers = (userList, searchTerm) => {\n if (searchTerm === '') {\n return userList;\n }\n\n searchTerm = searchTerm.toLowerCase();\n\n return userList.filter((user) => {\n return user.fullname.toLowerCase().includes(searchTerm);\n });\n};\n\n/**\n * Render the list of users in the search results area.\n *\n * @param {HTMLElement} searchResultsContainer The container element for search results\n * @param {Array} users The list of users to display\n */\nconst renderSearchResults = async(searchResultsContainer, users) => {\n const {html, js} = await Templates.renderForPromise(templateNames.grader.searchResults, {users});\n Templates.replaceNodeContents(searchResultsContainer, html, js);\n};\n\n/**\n * Add click handlers to the buttons in the header of the grading interface.\n *\n * @param {HTMLElement} graderLayout\n * @param {Object} userPicker\n * @param {Function} saveGradeFunction\n * @param {Array} userList List of users for the grader.\n */\nconst registerEventListeners = (graderLayout, userPicker, saveGradeFunction, userList) => {\n const graderContainer = graderLayout.getContainer();\n const toggleSearchButton = graderContainer.querySelector(Selectors.buttons.toggleSearch);\n const searchInputContainer = graderContainer.querySelector(Selectors.regions.userSearchContainer);\n const searchInput = searchInputContainer.querySelector(Selectors.regions.userSearchInput);\n const bodyContainer = graderContainer.querySelector(Selectors.regions.bodyContainer);\n const userPickerContainer = graderContainer.querySelector(Selectors.regions.pickerRegion);\n const searchResultsContainer = graderContainer.querySelector(Selectors.regions.searchResultsContainer);\n\n graderContainer.addEventListener('click', (e) => {\n if (e.target.closest(Selectors.buttons.toggleFullscreen)) {\n e.stopImmediatePropagation();\n e.preventDefault();\n graderLayout.toggleFullscreen();\n\n return;\n }\n\n if (e.target.closest(Selectors.buttons.closeGrader)) {\n e.stopImmediatePropagation();\n e.preventDefault();\n\n graderLayout.close();\n\n return;\n }\n\n if (e.target.closest(Selectors.buttons.saveGrade)) {\n saveGradeFunction(userPicker.currentUser);\n }\n\n if (e.target.closest(Selectors.buttons.toggleSearch)) {\n if (toggleSearchButton.getAttribute('aria-expanded') === 'true') {\n // Search is open so let's close it.\n hideUserSearchInput(toggleSearchButton, searchInputContainer, searchInput);\n hideSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer);\n searchResultsContainer.innerHTML = '';\n } else {\n // Search is closed so let's open it.\n showUserSearchInput(toggleSearchButton, searchInputContainer, searchInput);\n showSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer);\n renderSearchResults(searchResultsContainer, userList);\n }\n\n return;\n }\n\n const selectUserButton = e.target.closest(Selectors.buttons.selectUser);\n if (selectUserButton) {\n const userId = selectUserButton.getAttribute('data-userid');\n const user = userList.find(user => user.id == userId);\n userPicker.setUserId(userId);\n userPicker.showUser(user);\n hideUserSearchInput(toggleSearchButton, searchInputContainer, searchInput);\n hideSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer);\n searchResultsContainer.innerHTML = '';\n }\n });\n\n // Debounce the search input so that it only executes 300 milliseconds after the user has finished typing.\n searchInput.addEventListener('input', debounce(() => {\n const users = searchForUsers(userList, searchInput.value);\n renderSearchResults(searchResultsContainer, users);\n }, 300));\n\n // Remove the right margin of the content container when the grading panel is hidden so that it expands to full-width.\n subscribe(DrawerEvents.DRAWER_HIDDEN, (drawerRoot) => {\n const gradingPanel = drawerRoot[0];\n if (gradingPanel.querySelector(Selectors.regions.gradingPanel)) {\n setContentContainerMargin(graderContainer, 0);\n }\n });\n\n // Bring back the right margin of the content container when the grading panel is shown to give space for the grading panel.\n subscribe(DrawerEvents.DRAWER_SHOWN, (drawerRoot) => {\n const gradingPanel = drawerRoot[0];\n if (gradingPanel.querySelector(Selectors.regions.gradingPanel)) {\n setContentContainerMargin(graderContainer, gradingPanel.offsetWidth);\n }\n });\n};\n\n/**\n * Adjusts the right margin of the content container.\n *\n * @param {HTMLElement} graderContainer The container for the grader app.\n * @param {Number} rightMargin The right margin value.\n */\nconst setContentContainerMargin = (graderContainer, rightMargin) => {\n const contentContainer = graderContainer.querySelector(Selectors.regions.moduleContainer);\n if (contentContainer) {\n contentContainer.style.marginRight = `${rightMargin}px`;\n }\n};\n\n/**\n * Get the function used to save a user grade.\n *\n * @param {HTMLElement} root The container for the grader\n * @param {Function} setGradeForUser The function that will be called.\n * @return {Function}\n */\nconst getSaveUserGradeFunction = (root, setGradeForUser) => {\n return async(user) => {\n try {\n root.querySelector(Selectors.regions.gradingPanelErrors).innerHTML = '';\n const result = await setGradeForUser(\n user.id,\n root.querySelector(Selectors.values.sendStudentNotifications).value,\n root.querySelector(Selectors.regions.gradingPanel)\n );\n if (result.success) {\n addToast(await getString('grades:gradesavedfor', 'mod_forum', user));\n }\n if (result.failed) {\n displayGradingError(root, user, result.error);\n }\n\n return result;\n } catch (err) {\n displayGradingError(root, user, err);\n\n return failedUpdate(err);\n }\n };\n};\n\n/**\n * Display a grading error, typically from a failed save.\n *\n * @param {HTMLElement} root The container for the grader\n * @param {Object} user The user who was errored\n * @param {Object} err The details of the error\n */\nconst displayGradingError = async(root, user, err) => {\n const [\n {html, js},\n errorString\n ] = await Promise.all([\n Templates.renderForPromise(templateNames.grader.gradingPanel.error, {error: err}),\n await getString('grades:gradesavefailed', 'mod_forum', {error: err.message, ...user}),\n ]);\n\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.gradingPanelErrors), html, js);\n addToast(errorString, {type: 'warning'});\n};\n\n/**\n * Launch the grader interface with the specified parameters.\n *\n * @param {Function} getListOfUsers A function to get the list of users\n * @param {Function} getContentForUser A function to get the content for a specific user\n * @param {Function} getGradeForUser A function get the grade details for a specific user\n * @param {Function} setGradeForUser A function to set the grade for a specific user\n * @param {Object} preferences Preferences for the launch function\n * @param {Number} preferences.initialUserId\n * @param {string} preferences.moduleName\n * @param {string} preferences.courseName\n * @param {string} preferences.courseUrl\n * @param {boolean} preferences.sendStudentNotifications\n * @param {null|HTMLElement} preferences.focusOnClose\n */\nexport const launch = async(getListOfUsers, getContentForUser, getGradeForUser, setGradeForUser, {\n initialUserId = null,\n moduleName,\n courseName,\n courseUrl,\n sendStudentNotifications,\n focusOnClose = null,\n} = {}) => {\n\n // We need all of these functions to be executed in series, if one step runs before another the interface\n // will not work.\n\n // We need this promise to resolve separately so that we can avoid loading the whole interface if there are no users.\n const userList = await getListOfUsers();\n if (!userList.length) {\n addNotification({\n message: await getString('nouserstograde', 'core_grades'),\n type: \"error\",\n });\n return;\n }\n\n // Now that we have confirmed there are at least some users let's boot up the grader interface.\n const [\n graderLayout,\n {html, js},\n ] = await Promise.all([\n createFullScreenWindow({\n fullscreen: false,\n showLoader: false,\n focusOnClose,\n }),\n Templates.renderForPromise(templateNames.grader.app, {\n moduleName,\n courseName,\n courseUrl,\n drawer: {show: true},\n defaultsendnotifications: sendStudentNotifications,\n }),\n ]);\n\n const graderContainer = graderLayout.getContainer();\n\n const saveGradeFunction = getSaveUserGradeFunction(graderContainer, setGradeForUser);\n\n Templates.replaceNodeContents(graderContainer, html, js);\n const updateUserContent = getUpdateUserContentFunction(graderContainer, getContentForUser, getGradeForUser, saveGradeFunction);\n\n const userIds = userList.map(user => user.id);\n const statusContainer = graderContainer.querySelector(Selectors.regions.statusContainer);\n // Fetch the userpicker for display.\n const userPicker = await getUserPicker(\n userList,\n async(user) => {\n const userGrade = await updateUserContent(user);\n const renderContext = {\n status: userGrade.hasgrade,\n index: userIds.indexOf(user.id) + 1,\n total: userList.length\n };\n Templates.render(templateNames.grader.status, renderContext).then(html => {\n statusContainer.innerHTML = html;\n return html;\n }).catch();\n },\n saveGradeFunction,\n {\n initialUserId,\n },\n );\n\n // Register all event listeners.\n registerEventListeners(graderLayout, userPicker, saveGradeFunction, userList);\n\n // Display the newly created user picker.\n displayUserPicker(graderContainer, userPicker.rootNode);\n};\n\n/**\n * Show the grade for a specific user.\n *\n * @param {Function} getGradeForUser A function get the grade details for a specific user\n * @param {Number} userid The ID of a specific user\n * @param {String} moduleName the name of the module\n * @param {object} param\n * @param {null|HTMLElement} param.focusOnClose\n */\nexport const view = async(getGradeForUser, userid, moduleName, {\n focusOnClose = null,\n} = {}) => {\n\n const userGrade = await getGradeForUser(userid);\n\n const [\n modal,\n gradeTemplateData\n ] = await Promise.all([\n Modal.create({\n title: moduleName,\n large: true,\n removeOnClose: true,\n returnElement: focusOnClose,\n show: true,\n body: Templates.render('mod_forum/local/grades/view_grade', userGrade),\n }),\n renderGradeTemplate(userGrade)\n ]);\n\n const bodyPromise = await modal.getBodyPromise();\n const gradeReplace = bodyPromise[0].querySelector('[data-region=\"grade-template\"]');\n Templates.replaceNodeContents(gradeReplace, gradeTemplateData.html, gradeTemplateData.js);\n};\n\nconst renderGradeTemplate = (userGrade) => Templates.renderForPromise(userGrade.templatename, userGrade.grade);\n\nexport {getGradingPanelFunctions};\n"],"names":["templateNames","app","gradingPanel","error","searchResults","status","displayUserPicker","root","html","pickerRegion","querySelector","Selectors","regions","replaceNodeContents","fetchContentFromRender","js","getUpdateUserContentFunction","getContentForUser","getGradeForUser","saveGradeForUser","firstLoad","async","spinner","userGrade","Promise","all","user","id","then","moduleReplace","gradingPanelHtml","gradingPanelJS","Templates","render","templatename","grade","panelContainer","gradingPanelContainer","panel","form","addEventListener","event","preventDefault","scrollTop","resolve","hideSearchResultContainer","bodyContainer","userPickerContainer","searchResultsContainer","classList","remove","add","hideUserSearchInput","toggleSearchButton","searchContainer","searchInput","setAttribute","focus","parentElement","gradingInfoContainer","removeAttribute","collapseGradingDrawer","buttons","value","renderSearchResults","users","renderForPromise","registerEventListeners","graderLayout","userPicker","saveGradeFunction","userList","graderContainer","getContainer","toggleSearch","searchInputContainer","userSearchContainer","userSearchInput","e","target","closest","toggleFullscreen","stopImmediatePropagation","closeGrader","close","saveGrade","currentUser","getAttribute","innerHTML","showUserSearchInput","showSearchResultContainer","selectUserButton","selectUser","userId","find","setUserId","showUser","searchTerm","toLowerCase","filter","fullname","includes","searchForUsers","DrawerEvents","DRAWER_HIDDEN","drawerRoot","setContentContainerMargin","DRAWER_SHOWN","offsetWidth","rightMargin","contentContainer","moduleContainer","style","marginRight","getSaveUserGradeFunction","setGradeForUser","gradingPanelErrors","result","values","sendStudentNotifications","success","failed","displayGradingError","err","errorString","message","type","getListOfUsers","initialUserId","moduleName","courseName","courseUrl","focusOnClose","length","fullscreen","showLoader","drawer","show","defaultsendnotifications","updateUserContent","userIds","map","statusContainer","renderContext","hasgrade","index","indexOf","total","catch","rootNode","userid","modal","gradeTemplateData","Modal","create","title","large","removeOnClose","returnElement","body","renderGradeTemplate","bodyPromise","getBodyPromise","gradeReplace"],"mappings":";;;;;;;4gBAsCMA,qBACM,CACJC,IAAK,gCACLC,aAAc,CACVC,MAAO,0DAEXC,cAAe,8DACfC,OAAQ,8CAUVC,kBAAoB,CAACC,KAAMC,cACvBC,aAAeF,KAAKG,cAAcC,mBAAUC,QAAQH,iCAChDI,oBAAoBJ,aAAcD,KAAM,KAUhDM,uBAAyB,CAACN,KAAMO,KAC3B,CAACP,KAAMO,IAaZC,6BAA+B,CAACT,KAAMU,kBAAmBC,gBAAiBC,wBACxEC,WAAY,SAETC,MAAAA,aACGC,QAAUF,UAAY,MAAO,8CAA8Bb,QAE5DC,KAAMO,IACPQ,iBACMC,QAAQC,IAAI,CAClBR,kBAAkBS,KAAKC,IAAIC,KAAKd,wBAChCI,gBAAgBQ,KAAKC,yBAEfd,oBAAoBN,KAAKG,cAAcC,mBAAUC,QAAQiB,eAAgBrB,KAAMO,UAGrFe,iBACAC,sBACMC,mBAAUC,OAAOV,UAAUW,aAAcX,UAAUY,OAAOP,KAAKd,wBACnEsB,eAAiB7B,KAAKG,cAAcC,mBAAUC,QAAQyB,uBACtDC,MAAQF,eAAe1B,cAAcC,mBAAUC,QAAQV,iCACnDW,oBAAoByB,MAAOR,iBAAkBC,sBAEjDQ,KAAOD,MAAM5B,cAAc,gDACf6B,MAElBA,KAAKC,iBAAiB,UAAUC,QAC5BtB,iBAAiBO,MACjBe,MAAMC,oBAGVN,eAAeO,UAAY,EAC3BvB,WAAY,EAERE,SACAA,QAAQsB,UAELrB,YAwBTsB,0BAA4B,CAACC,cAAeC,oBAAqBC,0BACnEF,cAAcG,UAAUC,OAAO,UAC/BH,oBAAoBE,UAAUC,OAAO,UACrCF,uBAAuBC,UAAUE,IAAI,WAmCnCC,oBAAsB,CAACC,mBAAoBC,gBAAiBC,eAC9DD,gBAAgBL,UAAUE,IAAI,aAC9BE,mBAAmBG,aAAa,gBAAiB,SACjDH,mBAAmBJ,UAAUE,IAAI,YACjCE,mBAAmBJ,UAAUC,OAAO,UACpCG,mBAAmBI,QAGUH,gBAAgBI,cAAchD,cAAcC,mBAAUC,QAAQ+C,sBACtEC,gBAAgB,qBAG/BC,sBAAwBP,gBAAgBI,cAAchD,cAAcC,mBAAUmD,QAAQD,uBAC5FA,sBAAsBD,gBAAgB,eACtCC,sBAAsBL,aAAa,WAAY,KAE/CD,YAAYQ,MAAQ,IA4BlBC,oBAAsB3C,MAAM2B,uBAAwBiB,eAChDzD,KAACA,KAADO,GAAOA,UAAYiB,mBAAUkC,iBAAiBlE,qBAAqBI,cAAe,CAAC6D,MAAAA,2BAC/EpD,oBAAoBmC,uBAAwBxC,KAAMO,KAW1DoD,uBAAyB,CAACC,aAAcC,WAAYC,kBAAmBC,kBACnEC,gBAAkBJ,aAAaK,eAC/BpB,mBAAqBmB,gBAAgB9D,cAAcC,mBAAUmD,QAAQY,cACrEC,qBAAuBH,gBAAgB9D,cAAcC,mBAAUC,QAAQgE,qBACvErB,YAAcoB,qBAAqBjE,cAAcC,mBAAUC,QAAQiE,iBACnE/B,cAAgB0B,gBAAgB9D,cAAcC,mBAAUC,QAAQkC,eAChEC,oBAAsByB,gBAAgB9D,cAAcC,mBAAUC,QAAQH,cACtEuC,uBAAyBwB,gBAAgB9D,cAAcC,mBAAUC,QAAQoC,wBAE/EwB,gBAAgBhC,iBAAiB,SAAUsC,OACnCA,EAAEC,OAAOC,QAAQrE,mBAAUmD,QAAQmB,yBACnCH,EAAEI,2BACFJ,EAAEpC,sBACF0B,aAAaa,sBAKbH,EAAEC,OAAOC,QAAQrE,mBAAUmD,QAAQqB,oBACnCL,EAAEI,2BACFJ,EAAEpC,sBAEF0B,aAAagB,WAKbN,EAAEC,OAAOC,QAAQrE,mBAAUmD,QAAQuB,YACnCf,kBAAkBD,WAAWiB,aAG7BR,EAAEC,OAAOC,QAAQrE,mBAAUmD,QAAQY,0BACsB,SAArDrB,mBAAmBkC,aAAa,kBAEhCnC,oBAAoBC,mBAAoBsB,qBAAsBpB,aAC9DV,0BAA0BC,cAAeC,oBAAqBC,wBAC9DA,uBAAuBwC,UAAY,KAtHvB,EAACnC,mBAAoBC,gBAAiBC,eAC9DD,gBAAgBL,UAAUC,OAAO,aACjCG,mBAAmBG,aAAa,gBAAiB,QACjDH,mBAAmBJ,UAAUE,IAAI,UACjCE,mBAAmBJ,UAAUC,OAAO,YAGPI,gBAAgBI,cAAchD,cAAcC,mBAAUC,QAAQ+C,sBACtEH,aAAa,cAAe,cAG3CK,sBAAwBP,gBAAgBI,cAAchD,cAAcC,mBAAUmD,QAAQD,uBAC5FA,sBAAsBL,aAAa,cAAe,QAClDK,sBAAsBL,aAAa,WAAY,MAE/CD,YAAYE,SA0GAgC,CAAoBpC,mBAAoBsB,qBAAsBpB,aAnJ5C,EAACT,cAAeC,oBAAqBC,0BACnEF,cAAcG,UAAUE,IAAI,UAC5BJ,oBAAoBE,UAAUE,IAAI,UAClCH,uBAAuBC,UAAUC,OAAO,WAiJ5BwC,CAA0B5C,cAAeC,oBAAqBC,wBAC9DgB,oBAAoBhB,uBAAwBuB,kBAM9CoB,iBAAmBb,EAAEC,OAAOC,QAAQrE,mBAAUmD,QAAQ8B,eACxDD,iBAAkB,OACZE,OAASF,iBAAiBJ,aAAa,eACvC7D,KAAO6C,SAASuB,MAAKpE,MAAQA,KAAKC,IAAMkE,SAC9CxB,WAAW0B,UAAUF,QACrBxB,WAAW2B,SAAStE,MACpB0B,oBAAoBC,mBAAoBsB,qBAAsBpB,aAC9DV,0BAA0BC,cAAeC,oBAAqBC,wBAC9DA,uBAAuBwC,UAAY,OAK3CjC,YAAYf,iBAAiB,SAAS,oBAAS,WACrCyB,MA5FS,EAACM,SAAU0B,aACX,KAAfA,WACO1B,UAGX0B,WAAaA,WAAWC,cAEjB3B,SAAS4B,QAAQzE,MACbA,KAAK0E,SAASF,cAAcG,SAASJ,eAoF9BK,CAAe/B,SAAUhB,YAAYQ,OACnDC,oBAAoBhB,uBAAwBiB,SAC7C,4BAGOsC,uBAAaC,eAAgBC,aACdA,WAAW,GACf/F,cAAcC,mBAAUC,QAAQV,eAC7CwG,0BAA0BlC,gBAAiB,4BAKzC+B,uBAAaI,cAAeF,mBAC5BvG,aAAeuG,WAAW,GAC5BvG,aAAaQ,cAAcC,mBAAUC,QAAQV,eAC7CwG,0BAA0BlC,gBAAiBtE,aAAa0G,iBAW9DF,0BAA4B,CAAClC,gBAAiBqC,qBAC1CC,iBAAmBtC,gBAAgB9D,cAAcC,mBAAUC,QAAQmG,iBACrED,mBACAA,iBAAiBE,MAAMC,sBAAiBJ,oBAW1CK,yBAA2B,CAAC3G,KAAM4G,kBAC7B9F,MAAAA,WAECd,KAAKG,cAAcC,mBAAUC,QAAQwG,oBAAoB5B,UAAY,SAC/D6B,aAAeF,gBACjBzF,KAAKC,GACLpB,KAAKG,cAAcC,mBAAU2G,OAAOC,0BAA0BxD,MAC9DxD,KAAKG,cAAcC,mBAAUC,QAAQV,sBAErCmH,OAAOG,8BACQ,kBAAU,uBAAwB,YAAa9F,OAE9D2F,OAAOI,QACPC,oBAAoBnH,KAAMmB,KAAM2F,OAAOlH,OAGpCkH,OACT,MAAOM,YACLD,oBAAoBnH,KAAMmB,KAAMiG,MAEzB,2BAAaA,OAY1BD,oBAAsBrG,MAAMd,KAAMmB,KAAMiG,cAEtCnH,KAACA,KAADO,GAAOA,IACP6G,mBACMpG,QAAQC,IAAI,CAClBO,mBAAUkC,iBAAiBlE,qBAAqBE,aAAaC,MAAO,CAACA,MAAOwH,YACtE,kBAAU,yBAA0B,YAAa,CAACxH,MAAOwH,IAAIE,WAAYnG,4BAGzEb,oBAAoBN,KAAKG,cAAcC,mBAAUC,QAAQwG,oBAAqB5G,KAAMO,mBACrF6G,YAAa,CAACE,KAAM,6BAkBXzG,eAAM0G,eAAgB9G,kBAAmBC,gBAAiBiG,qBAAiBa,cAC7FA,cAAgB,KAD6EC,WAE7FA,WAF6FC,WAG7FA,WAH6FC,UAI7FA,UAJ6FZ,yBAK7FA,yBAL6Fa,aAM7FA,aAAe,6DACf,SAMM7D,eAAiBwD,qBAClBxD,SAAS8D,oDACM,CACZR,cAAe,kBAAU,iBAAkB,eAC3CC,KAAM,gBAOV1D,cACA5D,KAACA,KAADO,GAAOA,WACDS,QAAQC,IAAI,EAClB,4BAAuB,CACnB6G,YAAY,EACZC,YAAY,EACZH,aAAAA,eAEJpG,mBAAUkC,iBAAiBlE,qBAAqBC,IAAK,CACjDgI,WAAAA,WACAC,WAAAA,WACAC,UAAAA,UACAK,OAAQ,CAACC,MAAM,GACfC,yBAA0BnB,6BAI5B/C,gBAAkBJ,aAAaK,eAE/BH,kBAAoB4C,yBAAyB1C,gBAAiB2C,oCAE1DtG,oBAAoB2D,gBAAiBhE,KAAMO,UAC/C4H,kBAAoB3H,6BAA6BwD,gBAAiBvD,kBAAmBC,gBAAiBoD,mBAEtGsE,QAAUrE,SAASsE,KAAInH,MAAQA,KAAKC,KACpCmH,gBAAkBtE,gBAAgB9D,cAAcC,mBAAUC,QAAQkI,iBAElEzE,iBAAmB,wBACrBE,UACAlD,MAAAA,aAEU0H,cAAgB,CAClB1I,cAFoBsI,kBAAkBjH,OAEpBsH,SAClBC,MAAOL,QAAQM,QAAQxH,KAAKC,IAAM,EAClCwH,MAAO5E,SAAS8D,2BAEVpG,OAAOjC,qBAAqBK,OAAQ0I,eAAenH,MAAKpB,OAC9DsI,gBAAgBtD,UAAYhF,KACrBA,QACR4I,UAEP9E,kBACA,CACI0D,cAAAA,gBAKR7D,uBAAuBC,aAAcC,WAAYC,kBAAmBC,UAGpEjE,kBAAkBkE,gBAAiBH,WAAWgF,yBAY9BhI,eAAMH,gBAAiBoI,OAAQrB,gBAAYG,aAC3DA,aAAe,6DACf,SAEM7G,gBAAkBL,gBAAgBoI,SAGpCC,MACAC,yBACMhI,QAAQC,IAAI,CAClBgI,sBAAMC,OAAO,CACTC,MAAO1B,WACP2B,OAAO,EACPC,eAAe,EACfC,cAAe1B,aACfK,MAAM,EACNsB,KAAM/H,mBAAUC,OAAO,oCAAqCV,aAEhEyI,oBAAoBzI,aAGlB0I,kBAAoBV,MAAMW,iBAC1BC,aAAeF,YAAY,GAAGvJ,cAAc,qDACxCG,oBAAoBsJ,aAAcX,kBAAkBhJ,KAAMgJ,kBAAkBzI,WAGpFiJ,oBAAuBzI,WAAcS,mBAAUkC,iBAAiB3C,UAAUW,aAAcX,UAAUY"}PK-B\zw-(amd/build/local/layout/fullscreen.min.jsnu[define("mod_forum/local/layout/fullscreen",["exports","core/loadingicon","core/toast","core/local/aria/focuslock"],(function(_exports,_loadingicon,_toast,FocusLockManager){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.createLayout=void 0,FocusLockManager=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj} /** * Full screen window layout. * * @module mod_forum/local/layout/fullscreen * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */(FocusLockManager);_exports.createLayout=function(){let{fullscreen:fullscreen=!0,showLoader:showLoader=!1,focusOnClose:focusOnClose=null}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};const container=document.createElement("div");document.body.append(container),container.classList.add("layout"),container.classList.add("fullscreen"),container.setAttribute("role","application"),(0,_toast.addToastRegion)(container),lockBodyScroll(),FocusLockManager.trapFocus(container);const helpers=getLayoutHelpers(container,FocusLockManager,focusOnClose);return showLoader&&helpers.showLoadingIcon(),fullscreen&&helpers.requestFullscreen(),helpers};const getLayoutHelpers=(layoutNode,FocusLockManager,focusOnClose)=>{const contentNode=document.createElement("div");layoutNode.append(contentNode);const loadingNode=document.createElement("div");layoutNode.append(loadingNode);const requestFullscreen=()=>{layoutNode.requestFullscreen?layoutNode.requestFullscreen():layoutNode.msRequestFullscreen?layoutNode.msRequestFullscreen():layoutNode.mozRequestFullscreen?layoutNode.mozRequestFullscreen():layoutNode.webkitRequestFullscreen?layoutNode.webkitRequestFullscreen():layoutNode.setTop(0)},exitFullscreen=()=>{if(document.exitRequestFullScreen){if(document.fullScreenElement!==layoutNode)return;document.exitRequestFullScreen()}else if(document.msExitFullscreen){if(document.msFullscreenElement!==layoutNode)return;document.msExitFullscreen()}else if(document.mozCancelFullScreen){if(document.mozFullScreenElement!==layoutNode)return;document.mozCancelFullScreen()}else if(document.webkitExitFullscreen){if(document.webkitFullscreenElement!==layoutNode)return;document.webkitExitFullscreen()}},hideLoadingIcon=()=>{let child=loadingNode.lastElementChild;for(;child;)loadingNode.removeChild(child),child=loadingNode.lastElementChild};return{close:()=>{if(exitFullscreen(),unlockBodyScroll(),FocusLockManager.untrapFocus(),layoutNode.remove(),focusOnClose)try{focusOnClose.focus()}catch(e){}},toggleFullscreen:()=>{document.exitRequestFullScreen?document.fullScreenElement===layoutNode?exitFullscreen():requestFullscreen():document.msExitFullscreen?document.msFullscreenElement===layoutNode?exitFullscreen():requestFullscreen():document.mozCancelFullScreen?document.mozFullScreenElement===layoutNode?exitFullscreen():requestFullscreen():document.webkitExitFullscreen&&(document.webkitFullscreenElement===layoutNode?exitFullscreen():requestFullscreen())},requestFullscreen:requestFullscreen,exitFullscreen:exitFullscreen,getContainer:()=>contentNode,setContent:content=>{hideLoadingIcon();let child=contentNode.lastElementChild;for(;child;)contentNode.removeChild(child),child=contentNode.lastElementChild;contentNode.append(content)},showLoadingIcon:()=>{(0,_loadingicon.addIconToContainer)(loadingNode)},hideLoadingIcon:hideLoadingIcon}},lockBodyScroll=()=>{document.querySelector("body").classList.add("overflow-hidden")},unlockBodyScroll=()=>{document.querySelector("body").classList.remove("overflow-hidden")}})); //# sourceMappingURL=fullscreen.min.js.mapPK-B\d'',amd/build/local/layout/fullscreen.min.js.mapnu[{"version":3,"file":"fullscreen.min.js","sources":["../../../src/local/layout/fullscreen.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Full screen window layout.\n *\n * @module mod_forum/local/layout/fullscreen\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {addIconToContainer} from 'core/loadingicon';\nimport {addToastRegion} from 'core/toast';\nimport * as FocusLockManager from 'core/local/aria/focuslock';\n\n/**\n * Get the composed layout.\n *\n * @method\n * @param {string} templateName\n * @param {object} context\n * @returns {LayoutHelper}\n */\n\nexport const createLayout = ({\n fullscreen = true,\n showLoader = false,\n focusOnClose = null,\n} = {}) => {\n const container = document.createElement('div');\n document.body.append(container);\n container.classList.add('layout');\n container.classList.add('fullscreen');\n container.setAttribute('role', 'application');\n addToastRegion(container);\n\n // Lock scrolling on the document body.\n lockBodyScroll();\n\n // Lock tab control.\n FocusLockManager.trapFocus(container);\n\n const helpers = getLayoutHelpers(container, FocusLockManager, focusOnClose);\n\n if (showLoader) {\n helpers.showLoadingIcon();\n }\n\n if (fullscreen) {\n helpers.requestFullscreen();\n }\n\n return helpers;\n};\n\n/**\n * LayoutHelper A helper object containing functions for managing the current fullscreen layout\n *\n * @typedef {object}\n * @property {Function} close A function to close the fullscreen layout\n * @property {Function} toggleFullscreen A function to toggle the fullscreen from active to disabled and back\n * @property {Function} requestFullscreen Make a request to the browser to make the window full screen.\n * Note: This must be called in response to a direct user action\n * @property {Function} exitFullscreen Exit the fullscreen mode\n * @property {Function} getContainer Get the container of the fullscreen layout\n * @property {Function} setContent Set the content of the fullscreen layout\n * @property {Function} showLoadingIcon Display the loading icon\n * @property {Function} hideLoadingIcon Hide the loading icon\n */\n\n/**\n * Get the layout helpers.\n *\n * @method\n * @private\n * @param {HTMLElement} layoutNode\n * @param {FocusLockManager} FocusLockManager\n * @param {Boolean} focusOnClose\n * @returns {LayoutHelper}\n */\nconst getLayoutHelpers = (layoutNode, FocusLockManager, focusOnClose) => {\n const contentNode = document.createElement('div');\n layoutNode.append(contentNode);\n\n const loadingNode = document.createElement('div');\n layoutNode.append(loadingNode);\n\n /**\n * Close and destroy the window container.\n */\n const close = () => {\n exitFullscreen();\n unlockBodyScroll();\n FocusLockManager.untrapFocus();\n\n layoutNode.remove();\n\n if (focusOnClose) {\n try {\n focusOnClose.focus();\n } catch (e) {\n // eslint-disable-line\n }\n }\n };\n\n /**\n * Attempt to make the conatiner full screen.\n */\n const requestFullscreen = () => {\n if (layoutNode.requestFullscreen) {\n layoutNode.requestFullscreen();\n } else if (layoutNode.msRequestFullscreen) {\n layoutNode.msRequestFullscreen();\n } else if (layoutNode.mozRequestFullscreen) {\n layoutNode.mozRequestFullscreen();\n } else if (layoutNode.webkitRequestFullscreen) {\n layoutNode.webkitRequestFullscreen();\n } else {\n // Not supported.\n // Hack to make this act like full-screen as much as possible.\n layoutNode.setTop(0);\n }\n };\n\n /**\n * Exit full screen but do not close the container fully.\n */\n const exitFullscreen = () => {\n if (document.exitRequestFullScreen) {\n if (document.fullScreenElement !== layoutNode) {\n return;\n }\n document.exitRequestFullScreen();\n } else if (document.msExitFullscreen) {\n if (document.msFullscreenElement !== layoutNode) {\n return;\n }\n document.msExitFullscreen();\n } else if (document.mozCancelFullScreen) {\n if (document.mozFullScreenElement !== layoutNode) {\n return;\n }\n document.mozCancelFullScreen();\n } else if (document.webkitExitFullscreen) {\n if (document.webkitFullscreenElement !== layoutNode) {\n return;\n }\n document.webkitExitFullscreen();\n }\n };\n\n const toggleFullscreen = () => {\n if (document.exitRequestFullScreen) {\n if (document.fullScreenElement === layoutNode) {\n exitFullscreen();\n } else {\n requestFullscreen();\n }\n } else if (document.msExitFullscreen) {\n if (document.msFullscreenElement === layoutNode) {\n exitFullscreen();\n } else {\n requestFullscreen();\n }\n } else if (document.mozCancelFullScreen) {\n if (document.mozFullScreenElement === layoutNode) {\n exitFullscreen();\n } else {\n requestFullscreen();\n }\n } else if (document.webkitExitFullscreen) {\n if (document.webkitFullscreenElement === layoutNode) {\n exitFullscreen();\n } else {\n requestFullscreen();\n }\n }\n };\n\n /**\n * Get the Node which is fullscreen.\n *\n * @return {Element}\n */\n const getContainer = () => {\n return contentNode;\n };\n\n const setContent = (content) => {\n hideLoadingIcon();\n\n // Note: It would be better to use replaceWith, but this is not compatible with IE.\n let child = contentNode.lastElementChild;\n while (child) {\n contentNode.removeChild(child);\n child = contentNode.lastElementChild;\n }\n contentNode.append(content);\n };\n\n const showLoadingIcon = () => {\n addIconToContainer(loadingNode);\n };\n\n const hideLoadingIcon = () => {\n // Hide the loading container.\n let child = loadingNode.lastElementChild;\n while (child) {\n loadingNode.removeChild(child);\n child = loadingNode.lastElementChild;\n }\n };\n\n /**\n * @return {Object}\n */\n return {\n close,\n\n toggleFullscreen,\n requestFullscreen,\n exitFullscreen,\n\n getContainer,\n setContent,\n\n showLoadingIcon,\n hideLoadingIcon,\n };\n};\n\nconst lockBodyScroll = () => {\n document.querySelector('body').classList.add('overflow-hidden');\n};\n\nconst unlockBodyScroll = () => {\n document.querySelector('body').classList.remove('overflow-hidden');\n};\n"],"names":["fullscreen","showLoader","focusOnClose","container","document","createElement","body","append","classList","add","setAttribute","lockBodyScroll","FocusLockManager","trapFocus","helpers","getLayoutHelpers","showLoadingIcon","requestFullscreen","layoutNode","contentNode","loadingNode","msRequestFullscreen","mozRequestFullscreen","webkitRequestFullscreen","setTop","exitFullscreen","exitRequestFullScreen","fullScreenElement","msExitFullscreen","msFullscreenElement","mozCancelFullScreen","mozFullScreenElement","webkitExitFullscreen","webkitFullscreenElement","hideLoadingIcon","child","lastElementChild","removeChild","close","unlockBodyScroll","untrapFocus","remove","focus","e","toggleFullscreen","getContainer","setContent","content","querySelector"],"mappings":";;;;;;;8CAoC4B,eAACA,WACzBA,YAAa,EADYC,WAEzBA,YAAa,EAFYC,aAGzBA,aAAe,6DACf,SACMC,UAAYC,SAASC,cAAc,OACzCD,SAASE,KAAKC,OAAOJ,WACrBA,UAAUK,UAAUC,IAAI,UACxBN,UAAUK,UAAUC,IAAI,cACxBN,UAAUO,aAAa,OAAQ,yCAChBP,WAGfQ,iBAGAC,iBAAiBC,UAAUV,iBAErBW,QAAUC,iBAAiBZ,UAAWS,iBAAkBV,qBAE1DD,YACAa,QAAQE,kBAGRhB,YACAc,QAAQG,oBAGLH,eA4BLC,iBAAmB,CAACG,WAAYN,iBAAkBV,sBAC9CiB,YAAcf,SAASC,cAAc,OAC3Ca,WAAWX,OAAOY,mBAEZC,YAAchB,SAASC,cAAc,OAC3Ca,WAAWX,OAAOa,mBAwBZH,kBAAoB,KAClBC,WAAWD,kBACXC,WAAWD,oBACJC,WAAWG,oBAClBH,WAAWG,sBACJH,WAAWI,qBAClBJ,WAAWI,uBACJJ,WAAWK,wBAClBL,WAAWK,0BAIXL,WAAWM,OAAO,IAOpBC,eAAiB,QACfrB,SAASsB,sBAAuB,IAC5BtB,SAASuB,oBAAsBT,kBAGnCd,SAASsB,6BACN,GAAItB,SAASwB,iBAAkB,IAC9BxB,SAASyB,sBAAwBX,kBAGrCd,SAASwB,wBACN,GAAIxB,SAAS0B,oBAAqB,IACjC1B,SAAS2B,uBAAyBb,kBAGtCd,SAAS0B,2BACN,GAAI1B,SAAS4B,qBAAsB,IAClC5B,SAAS6B,0BAA4Bf,kBAGzCd,SAAS4B,yBAyDXE,gBAAkB,SAEhBC,MAAQf,YAAYgB,sBACjBD,OACHf,YAAYiB,YAAYF,OACxBA,MAAQf,YAAYgB,wBAOrB,CACHE,MAhIU,QACVb,iBACAc,mBACA3B,iBAAiB4B,cAEjBtB,WAAWuB,SAEPvC,iBAEIA,aAAawC,QACf,MAAOC,MAwHbC,iBApEqB,KACjBxC,SAASsB,sBACLtB,SAASuB,oBAAsBT,WAC/BO,iBAEAR,oBAEGb,SAASwB,iBACZxB,SAASyB,sBAAwBX,WACjCO,iBAEAR,oBAEGb,SAAS0B,oBACZ1B,SAAS2B,uBAAyBb,WAClCO,iBAEAR,oBAEGb,SAAS4B,uBACZ5B,SAAS6B,0BAA4Bf,WACrCO,iBAEAR,sBA8CRA,kBAAAA,kBACAQ,eAAAA,eAEAoB,aAvCiB,IACV1B,YAuCP2B,WApCgBC,UAChBb,sBAGIC,MAAQhB,YAAYiB,sBACjBD,OACHhB,YAAYkB,YAAYF,OACxBA,MAAQhB,YAAYiB,iBAExBjB,YAAYZ,OAAOwC,UA6BnB/B,gBA1BoB,yCACDI,cA0BnBc,gBAAAA,kBAIFvB,eAAiB,KACnBP,SAAS4C,cAAc,QAAQxC,UAAUC,IAAI,oBAG3C8B,iBAAmB,KACrBnC,SAAS4C,cAAc,QAAQxC,UAAUiC,OAAO"}PK-B\s$__"amd/build/local/layouts.min.js.mapnu[{"version":3,"file":"layouts.min.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}PK-B\#@@amd/build/local/layouts.min.jsnu[define("mod_forum/local/layouts",["exports","./layout/fullscreen"],(function(_exports,_fullscreen){Object.defineProperty(_exports,"__esModule",{value:!0}),Object.defineProperty(_exports,"createFullScreenWindow",{enumerable:!0,get:function(){return _fullscreen.createLayout}})})); //# sourceMappingURL=layouts.min.js.mapPK-B\3+9QQamd/build/pin_toggle.min.jsnu[/** * This module is the highest level module for the calendar. It is * responsible for initialising all of the components required for * the calendar to run. It also coordinates the interaction between * components by listening for and responding to different events * triggered within the calendar UI. * * @module mod_forum/pin_toggle * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("mod_forum/pin_toggle",["jquery","core/ajax","core/str","core/templates","core/notification","mod_forum/repository","mod_forum/selectors","core/str"],(function($,Ajax,Str,Templates,Notification,Repository,Selectors,String){return{init:function(root,preventDefault,callback){root.on("click",Selectors.pin.toggle,(function(e){var toggleElement=$(this),forumid=toggleElement.data("forumid"),discussionid=toggleElement.data("discussionid"),pinstate=toggleElement.data("targetstate");Repository.setPinDiscussionState(forumid,discussionid,pinstate).then((function(context){return callback(toggleElement,context)})).then((function(){return String.get_string("pinupdated","forum").done((function(s){return Notification.addNotification({message:s,type:"info"})}))})).fail(Notification.exception),preventDefault&&e.preventDefault()}))}}})); //# sourceMappingURL=pin_toggle.min.js.mapPK-B\UUyy#amd/build/form-user-selector.min.jsnu[/** * Enrolled user selector module. * * @module mod_forum/form-user-selector * @copyright 2019 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("mod_forum/form-user-selector",["jquery","core/ajax","core/templates"],(function($,Ajax,Templates){return{processResults:function(selector,results){var users=[];return $.each(results,(function(index,user){users.push({value:user.id,label:user._label})})),users},transport:function(selector,query,success,failure){var courseid=$(selector).attr("courseid"),contextid=$(selector).attr("data-contextid");Ajax.call([{methodname:"core_enrol_search_users",args:{courseid:courseid,search:query,searchanywhere:!0,page:0,perpage:30,contextid:contextid}}])[0].then((function(results){var promises=[],i=0;return $.each(results,(function(index,user){promises.push(Templates.render("mod_forum/form-user-selector-suggestion",user))})),$.when.apply($.when,promises).then((function(){var args=arguments;$.each(results,(function(index,user){user._label=args[i],i++})),success(results)}))})).fail(failure)}}})); //# sourceMappingURL=form-user-selector.min.js.mapPK-B\Ct}``amd/build/posts_list.min.js.mapnu[{"version":3,"file":"posts_list.min.js","sources":["../src/posts_list.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * This module is the highest level module for the calendar. It is\n * responsible for initialising all of the components required for\n * the calendar to run. It also coordinates the interaction between\n * components by listening for and responding to different events\n * triggered within the calendar UI.\n *\n * @module mod_forum/posts_list\n * @copyright 2019 Peter Dias\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/templates',\n 'core/notification',\n 'core/pending',\n 'mod_forum/selectors',\n 'mod_forum/inpage_reply',\n 'core_form/changechecker',\n], function(\n $,\n Templates,\n Notification,\n Pending,\n Selectors,\n InPageReply,\n FormChangeChecker\n) {\n\n var registerEventListeners = function(root, throttlingwarningmsg) {\n root.on('click', Selectors.post.inpageReplyLink, function(e) {\n e.preventDefault();\n // After adding a reply a url hash is being generated that scrolls (points) to the newly added reply.\n // The hash being present causes this scrolling behavior to the particular reply to persists even when\n // another, non-related in-page replay link is being clicked which ultimately causes a bad user experience.\n // A particular solution for this problem would be changing the browser's history state when a url hash is\n // present.\n if (window.location.hash) {\n // Remove the fragment identifier from the url.\n var url = window.location.href.split('#')[0];\n history.pushState({}, document.title, url);\n }\n var pending = new Pending('inpage-reply');\n var currentTarget = $(e.currentTarget).parents(Selectors.post.forumCoreContent);\n var currentSubject = currentTarget.find(Selectors.post.forumSubject);\n var currentRoot = $(e.currentTarget).parents(Selectors.post.forumContent);\n var context = {\n postid: $(currentRoot).data('post-id'),\n \"reply_url\": $(e.currentTarget).attr('href'),\n sesskey: M.cfg.sesskey,\n parentsubject: currentSubject.data('replySubject'),\n canreplyprivately: $(e.currentTarget).data('can-reply-privately'),\n postformat: InPageReply.CONTENT_FORMATS.MOODLE,\n throttlingwarningmsg: throttlingwarningmsg\n };\n\n if (!currentRoot.find(Selectors.post.inpageReplyContent).length) {\n Templates.render('mod_forum/inpage_reply', context)\n .then(function(html, js) {\n return Templates.appendNodeContents(currentTarget, html, js);\n })\n .then(function() {\n return currentRoot.find(Selectors.post.inpageReplyContent)\n .slideToggle(300, pending.resolve).find('textarea').focus();\n })\n .then(function() {\n FormChangeChecker.watchFormById(`inpage-reply-${context.postid}`);\n return;\n })\n .catch(Notification.exception);\n } else {\n var form = currentRoot.find(Selectors.post.inpageReplyContent);\n form.slideToggle(300, pending.resolve);\n if (form.is(':visible')) {\n form.find('textarea').focus();\n }\n }\n });\n };\n\n return {\n init: function(root, throttlingwarningmsg) {\n registerEventListeners(root, throttlingwarningmsg);\n InPageReply.init(root);\n }\n };\n});\n"],"names":["define","$","Templates","Notification","Pending","Selectors","InPageReply","FormChangeChecker","init","root","throttlingwarningmsg","on","post","inpageReplyLink","e","preventDefault","window","location","hash","url","href","split","history","pushState","document","title","pending","currentTarget","parents","forumCoreContent","currentSubject","find","forumSubject","currentRoot","forumContent","context","postid","data","attr","sesskey","M","cfg","parentsubject","canreplyprivately","postformat","CONTENT_FORMATS","MOODLE","inpageReplyContent","length","form","slideToggle","resolve","is","focus","render","then","html","js","appendNodeContents","watchFormById","catch","exception","registerEventListeners"],"mappings":";;;;;;;;;;;AA0BAA,8BAAO,CACH,SACA,iBACA,oBACA,eACA,sBACA,yBACA,4BACD,SACCC,EACAC,UACAC,aACAC,QACAC,UACAC,YACAC,yBAsDO,CACHC,KAAM,SAASC,KAAMC,uBApDI,SAASD,KAAMC,sBACxCD,KAAKE,GAAG,QAASN,UAAUO,KAAKC,iBAAiB,SAASC,MACtDA,EAAEC,iBAMEC,OAAOC,SAASC,KAAM,KAElBC,IAAMH,OAAOC,SAASG,KAAKC,MAAM,KAAK,GAC1CC,QAAQC,UAAU,GAAIC,SAASC,MAAON,SAEtCO,QAAU,IAAItB,QAAQ,gBACtBuB,cAAgB1B,EAAEa,EAAEa,eAAeC,QAAQvB,UAAUO,KAAKiB,kBAC1DC,eAAiBH,cAAcI,KAAK1B,UAAUO,KAAKoB,cACnDC,YAAchC,EAAEa,EAAEa,eAAeC,QAAQvB,UAAUO,KAAKsB,cACxDC,QAAU,CACVC,OAAQnC,EAAEgC,aAAaI,KAAK,qBACfpC,EAAEa,EAAEa,eAAeW,KAAK,QACrCC,QAASC,EAAEC,IAAIF,QACfG,cAAeZ,eAAeO,KAAK,gBACnCM,kBAAmB1C,EAAEa,EAAEa,eAAeU,KAAK,uBAC3CO,WAAYtC,YAAYuC,gBAAgBC,OACxCpC,qBAAsBA,yBAGrBuB,YAAYF,KAAK1B,UAAUO,KAAKmC,oBAAoBC,OAclD,KACCC,KAAOhB,YAAYF,KAAK1B,UAAUO,KAAKmC,oBAC3CE,KAAKC,YAAY,IAAKxB,QAAQyB,SAC1BF,KAAKG,GAAG,aACRH,KAAKlB,KAAK,YAAYsB,aAjB1BnD,UAAUoD,OAAO,yBAA0BnB,SACtCoB,MAAK,SAASC,KAAMC,WACVvD,UAAUwD,mBAAmB/B,cAAe6B,KAAMC,OAE5DF,MAAK,kBACKtB,YAAYF,KAAK1B,UAAUO,KAAKmC,oBAClCG,YAAY,IAAKxB,QAAQyB,SAASpB,KAAK,YAAYsB,WAE3DE,MAAK,WACFhD,kBAAkBoD,qCAA8BxB,QAAQC,YAG3DwB,MAAMzD,aAAa0D,cAa5BC,CAAuBrD,KAAMC,sBAC7BJ,YAAYE,KAAKC"}PK-B\"qv**amd/build/discussion.min.jsnu[/** * Module for viewing a discussion. * * @module mod_forum/discussion * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("mod_forum/discussion",["jquery","core/custom_interaction_events","mod_forum/selectors","core/pubsub","mod_forum/forum_events","core/str","core/notification"],(function($,CustomEvents,Selectors,PubSub,ForumEvents,String,Notification){var isElementInInPageReplySection=function(element){return!!$(element).closest(Selectors.post.inpageReplyContent).length},initAccessibilityKeyboardNav=function(root){root.find(Selectors.post.post).each((function(index,post){var actions=$(post).find(Selectors.post.action),firstAction=actions.first();actions.attr("tabindex","-1"),firstAction.attr("tabindex",0)})),CustomEvents.define(root,[CustomEvents.events.up,CustomEvents.events.down,CustomEvents.events.next,CustomEvents.events.previous,CustomEvents.events.home,CustomEvents.events.end]),root.on(CustomEvents.events.up,(function(e,data){var activeElement=document.activeElement;if(!isElementInInPageReplySection(activeElement)){var focusPost=$(activeElement).closest(Selectors.post.post);focusPost.length?function(currentPost){var prevPost=currentPost.prev(Selectors.post.post);if(prevPost.length){var replyPost=prevPost.find(Selectors.post.post).last();replyPost.length?replyPost.focus():prevPost.focus()}else currentPost.parents(Selectors.post.post).first().focus()}(focusPost):root.find(Selectors.post.post).first().focus(),data.originalEvent.preventDefault()}})),root.on(CustomEvents.events.down,(function(e,data){var activeElement=document.activeElement;if(!isElementInInPageReplySection(activeElement)){var focusPost=$(activeElement).closest(Selectors.post.post);focusPost.length?function(currentPost){var replyPost=currentPost.find(Selectors.post.post).first();if(replyPost.length)replyPost.focus();else{var siblingPost=currentPost.next(Selectors.post.post);if(siblingPost.length)siblingPost.focus();else for(var parentPosts=currentPost.parents(Selectors.post.post).toArray(),i=0;i.\n\n/**\n * Handle discussion subscription toggling on a discussion list in\n * the forum view.\n *\n * @module mod_forum/favourite_toggle\n * @copyright 2019 Peter Dias \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/templates',\n 'core/notification',\n 'mod_forum/repository',\n 'mod_forum/selectors',\n 'core/str',\n ], function(\n $,\n Templates,\n Notification,\n Repository,\n Selectors,\n String\n ) {\n\n /**\n * Register event listeners for the subscription toggle.\n *\n * @param {object} root The discussion list root element\n * @param {boolean} preventDefault Should the default action of the event be prevented\n * @param {function} callback Success callback\n */\n var registerEventListeners = function(root, preventDefault, callback) {\n root.on('click', Selectors.favourite.toggle, function(e) {\n var toggleElement = $(this);\n var forumId = toggleElement.data('forumid');\n var discussionId = toggleElement.data('discussionid');\n var subscriptionState = toggleElement.data('targetstate');\n\n Repository.setFavouriteDiscussionState(forumId, discussionId, subscriptionState)\n .then(function(context) {\n return callback(toggleElement, context);\n })\n .then(function() {\n return String.get_string(\"favouriteupdated\", \"forum\")\n .done(function(s) {\n return Notification.addNotification({\n message: s,\n type: \"info\"\n });\n });\n })\n .catch(Notification.exception);\n\n if (preventDefault) {\n e.preventDefault();\n }\n });\n };\n\n return {\n init: registerEventListeners\n };\n});\n"],"names":["define","$","Templates","Notification","Repository","Selectors","String","init","root","preventDefault","callback","on","favourite","toggle","e","toggleElement","this","forumId","data","discussionId","subscriptionState","setFavouriteDiscussionState","then","context","get_string","done","s","addNotification","message","type","catch","exception"],"mappings":";;;;;;;;AAuBAA,oCAAO,CACC,SACA,iBACA,oBACA,uBACA,sBACA,aACD,SACCC,EACAC,UACAC,aACAC,WACAC,UACAC,cAsCG,CACHC,KA7ByB,SAASC,KAAMC,eAAgBC,UACxDF,KAAKG,GAAG,QAASN,UAAUO,UAAUC,QAAQ,SAASC,OAC9CC,cAAgBd,EAAEe,MAClBC,QAAUF,cAAcG,KAAK,WAC7BC,aAAeJ,cAAcG,KAAK,gBAClCE,kBAAoBL,cAAcG,KAAK,eAE3Cd,WAAWiB,4BAA4BJ,QAASE,aAAcC,mBACzDE,MAAK,SAASC,gBACJb,SAASK,cAAeQ,YAElCD,MAAK,kBACKhB,OAAOkB,WAAW,mBAAoB,SACxCC,MAAK,SAASC,UACJvB,aAAawB,gBAAgB,CAChCC,QAASF,EACTG,KAAM,eAIrBC,MAAM3B,aAAa4B,WAEpBtB,gBACAK,EAAEL"}PK-B\+amd/build/repository.min.js.mapnu[{"version":3,"file":"repository.min.js","sources":["../src/repository.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Forum repository class to encapsulate all of the AJAX requests that subscribe or unsubscribe\n * can be sent for forum.\n *\n * @module mod_forum/repository\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['core/ajax'], function(Ajax) {\n /**\n * Set the subscription state for a discussion in a forum.\n *\n * @param {number} forumId ID of the forum the discussion belongs to\n * @param {number} discussionId ID of the discussion with the subscription state\n * @param {boolean} targetState Set the subscribed state. True == subscribed; false == unsubscribed.\n * @return {object} jQuery promise\n */\n var setDiscussionSubscriptionState = function(forumId, discussionId, targetState) {\n var request = {\n methodname: 'mod_forum_set_subscription_state',\n args: {\n forumid: forumId,\n discussionid: discussionId,\n targetstate: targetState\n }\n };\n return Ajax.call([request])[0];\n };\n\n var addDiscussionPost = function(postid, subject, message, messageformat, isprivatereply, topreferredformat) {\n var request = {\n methodname: 'mod_forum_add_discussion_post',\n args: {\n postid: postid,\n message: message,\n messageformat: messageformat,\n subject: subject,\n options: [{\n name: \"private\",\n value: isprivatereply,\n }, {\n name: \"topreferredformat\",\n value: topreferredformat,\n }]\n }\n };\n return Ajax.call([request])[0];\n };\n\n /**\n * Set the favourite state for a discussion in a forum.\n *\n * @param {number} forumId ID of the forum the discussion belongs to\n * @param {number} discussionId ID of the discussion with the subscription state\n * @param {null|date} targetState Set the favourite state. True == favourited; false == unfavourited.\n * @return {object} jQuery promise\n */\n var setFavouriteDiscussionState = function(forumId, discussionId, targetState) {\n var request = {\n methodname: 'mod_forum_toggle_favourite_state',\n args: {\n discussionid: discussionId,\n targetstate: targetState\n }\n };\n return Ajax.call([request])[0];\n };\n\n var setDiscussionLockState = function(forumId, discussionId, targetState) {\n var request = {\n methodname: 'mod_forum_set_lock_state',\n args: {\n forumid: forumId,\n discussionid: discussionId,\n targetstate: targetState}\n };\n return Ajax.call([request])[0];\n };\n\n /**\n * Set the pinned state for the discussion provided.\n *\n * @param {number} forumid\n * @param {number} discussionid\n * @param {boolean} targetstate\n * @return {*|Promise}\n */\n var setPinDiscussionState = function(forumid, discussionid, targetstate) {\n var request = {\n methodname: 'mod_forum_set_pin_state',\n args: {\n discussionid: discussionid,\n targetstate: targetstate\n }\n };\n return Ajax.call([request])[0];\n };\n\n /**\n * Get the discussions for the user and cmid provided.\n *\n * @param {number} userid\n * @param {number} cmid\n * @param {string} sortby\n * @param {string} sortdirection\n * @return {*|Promise}\n */\n var getDiscussionByUserID = function(userid, cmid, sortby = 'modified', sortdirection = 'DESC') {\n var request = {\n methodname: 'mod_forum_get_discussion_posts_by_userid',\n args: {\n userid: userid,\n cmid: cmid,\n sortby: sortby,\n sortdirection: sortdirection,\n },\n };\n return Ajax.call([request])[0];\n };\n\n /**\n * Get the posts for the discussion ID provided.\n *\n * @param {number} discussionId\n * @param {String} sortby\n * @param {String} sortdirection\n * @return {*|Promise}\n */\n var getDiscussionPosts = function(discussionId, sortby = 'created', sortdirection = 'ASC') {\n var request = {\n methodname: 'mod_forum_get_discussion_posts',\n args: {\n discussionid: discussionId,\n sortby: sortby,\n sortdirection: sortdirection,\n },\n };\n return Ajax.call([request])[0];\n };\n\n return {\n setDiscussionSubscriptionState: setDiscussionSubscriptionState,\n addDiscussionPost: addDiscussionPost,\n setDiscussionLockState: setDiscussionLockState,\n setFavouriteDiscussionState: setFavouriteDiscussionState,\n setPinDiscussionState: setPinDiscussionState,\n getDiscussionByUserID: getDiscussionByUserID,\n getDiscussionPosts: getDiscussionPosts,\n };\n});\n"],"names":["define","Ajax","setDiscussionSubscriptionState","forumId","discussionId","targetState","request","methodname","args","forumid","discussionid","targetstate","call","addDiscussionPost","postid","subject","message","messageformat","isprivatereply","topreferredformat","options","name","value","setDiscussionLockState","setFavouriteDiscussionState","setPinDiscussionState","getDiscussionByUserID","userid","cmid","sortby","sortdirection","getDiscussionPosts"],"mappings":";;;;;;;;AAuBAA,8BAAO,CAAC,cAAc,SAASC,YAoIpB,CACHC,+BA5HiC,SAASC,QAASC,aAAcC,iBAC7DC,QAAU,CACVC,WAAY,mCACZC,KAAM,CACFC,QAASN,QACTO,aAAcN,aACdO,YAAaN,qBAGdJ,KAAKW,KAAK,CAACN,UAAU,IAoH5BO,kBAjHoB,SAASC,OAAQC,QAASC,QAASC,cAAeC,eAAgBC,uBAClFb,QAAU,CACVC,WAAY,gCACZC,KAAM,CACFM,OAAQA,OACRE,QAASA,QACTC,cAAeA,cACfF,QAASA,QACTK,QAAS,CAAC,CACNC,KAAM,UACNC,MAAOJ,gBACR,CACCG,KAAM,oBACNC,MAAOH,6BAIZlB,KAAKW,KAAK,CAACN,UAAU,IAiG5BiB,uBA3EyB,SAASpB,QAASC,aAAcC,iBACrDC,QAAU,CACVC,WAAY,2BACZC,KAAM,CACFC,QAASN,QACTO,aAAcN,aACdO,YAAaN,qBAEdJ,KAAKW,KAAK,CAACN,UAAU,IAoE5BkB,4BAvF8B,SAASrB,QAASC,aAAcC,iBAC1DC,QAAU,CACVC,WAAY,mCACZC,KAAM,CACFE,aAAcN,aACdO,YAAaN,qBAGdJ,KAAKW,KAAK,CAACN,UAAU,IAgF5BmB,sBA1DwB,SAAShB,QAASC,aAAcC,iBACpDL,QAAU,CACVC,WAAY,0BACZC,KAAM,CACFE,aAAcA,aACdC,YAAaA,qBAGdV,KAAKW,KAAK,CAACN,UAAU,IAmD5BoB,sBAvCwB,SAASC,OAAQC,UAAMC,8DAAS,WAAYC,qEAAgB,WAChFxB,QAAU,CACVC,WAAY,2CACZC,KAAM,CACFmB,OAAQA,OACRC,KAAMA,KACNC,OAAQA,OACRC,cAAeA,uBAGhB7B,KAAKW,KAAK,CAACN,UAAU,IA8B5ByB,mBAnBqB,SAAS3B,kBAAcyB,8DAAS,UAAWC,qEAAgB,UAC5ExB,QAAU,CACVC,WAAY,iCACZC,KAAM,CACFE,aAAcN,aACdyB,OAAQA,OACRC,cAAeA,uBAGhB7B,KAAKW,KAAK,CAACN,UAAU"}PK-B\amd/build/selectors.min.js.mapnu[{"version":3,"file":"selectors.min.js","sources":["../src/selectors.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Common CSS selectors for the forum UI.\n *\n * @module mod_forum/selectors\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([], function() {\n return {\n subscription: {\n toggle: \"[data-type='subscription-toggle'][data-action='toggle']\",\n },\n summary: {\n actions: \"[data-container='discussion-summary-actions']\"\n },\n post: {\n post: '[data-region=\"post\"]',\n action: '[data-region=\"post-action\"]',\n actionsContainer: '[data-region=\"post-actions-container\"]',\n authorName: '[data-region=\"author-name\"]',\n forumCoreContent: \"[data-region-content='forum-post-core']\",\n forumContent: \"[data-content='forum-post']\",\n forumSubject: \"[data-region-content='forum-post-core-subject']\",\n inpageCancelButton: \"button[name='cancelbtn']\",\n inpageReplyButton: \"button\",\n inpageReplyLink: \"[data-action='collapsible-link']\",\n inpageReplyCancelButton: \"[data-action='cancel-inpage-reply']\",\n inpageReplyCreateButton: \"[data-action='create-inpage-reply']\",\n inpageReplyContainer: '[data-region=\"inpage-reply-container\"]',\n inpageReplyContent: \"[data-content='inpage-reply-content']\",\n inpageReplyForm: \"form[data-content='inpage-reply-form']\",\n inpageSubmitBtn: \"[data-action='forum-inpage-submit']\",\n inpageSubmitBtnText: \"[data-region='submit-text']\",\n loadingIconContainer: \"[data-region='loading-icon-container']\",\n repliesContainer: \"[data-region='replies-container']\",\n replyCount: '[data-region=\"reply-count\"]',\n modeSelect: \"select[name='mode']\",\n showReplies: '[data-action=\"show-replies\"]',\n hideReplies: '[data-action=\"hide-replies\"]',\n repliesVisibilityToggleContainer: '[data-region=\"replies-visibility-toggle-container\"]'\n },\n lock: {\n toggle: \"[data-action='toggle'][data-type='lock-toggle']\",\n icon: \"[data-region='locked-icon']\"\n },\n favourite: {\n toggle: \"[data-type='favorite-toggle'][data-action='toggle']\",\n },\n pin: {\n toggle: \"[data-type='pin-toggle'][data-action='toggle']\",\n },\n discussion: {\n tools: '[data-container=\"discussion-tools\"]',\n item: '[data-region=\"discussion-list-item\"]',\n lockedLabel: \"[data-region='locked-label']\",\n subscribedLabel: \"[data-region='subscribed-label']\",\n timedLabel: \"[data-region='timed-label']\",\n },\n };\n});\n"],"names":["define","subscription","toggle","summary","actions","post","action","actionsContainer","authorName","forumCoreContent","forumContent","forumSubject","inpageCancelButton","inpageReplyButton","inpageReplyLink","inpageReplyCancelButton","inpageReplyCreateButton","inpageReplyContainer","inpageReplyContent","inpageReplyForm","inpageSubmitBtn","inpageSubmitBtnText","loadingIconContainer","repliesContainer","replyCount","modeSelect","showReplies","hideReplies","repliesVisibilityToggleContainer","lock","icon","favourite","pin","discussion","tools","item","lockedLabel","subscribedLabel","timedLabel"],"mappings":";;;;;;;AAsBAA,6BAAO,IAAI,iBACA,CACHC,aAAc,CACVC,OAAQ,2DAEZC,QAAS,CACLC,QAAS,iDAEbC,KAAM,CACFA,KAAM,uBACNC,OAAQ,8BACRC,iBAAkB,yCAClBC,WAAY,8BACZC,iBAAkB,0CAClBC,aAAc,8BACdC,aAAc,kDACdC,mBAAoB,2BACpBC,kBAAmB,SACnBC,gBAAiB,mCACjBC,wBAAyB,sCACzBC,wBAAyB,sCACzBC,qBAAsB,yCACtBC,mBAAoB,wCACpBC,gBAAiB,yCACjBC,gBAAiB,sCACjBC,oBAAqB,8BACrBC,qBAAsB,yCACtBC,iBAAkB,oCAClBC,WAAY,8BACZC,WAAY,sBACZC,YAAa,+BACbC,YAAa,+BACbC,iCAAkC,uDAEtCC,KAAM,CACF3B,OAAQ,kDACR4B,KAAM,+BAEVC,UAAW,CACP7B,OAAQ,uDAEZ8B,IAAK,CACD9B,OAAQ,kDAEZ+B,WAAY,CACRC,MAAO,sCACPC,KAAM,uCACNC,YAAa,+BACbC,gBAAiB,mCACjBC,WAAY"}PK-B\D/::amd/build/discussion.min.js.mapnu[{"version":3,"file":"discussion.min.js","sources":["../src/discussion.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module for viewing a discussion.\n *\n * @module mod_forum/discussion\n * @copyright 2019 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(\n[\n 'jquery',\n 'core/custom_interaction_events',\n 'mod_forum/selectors',\n 'core/pubsub',\n 'mod_forum/forum_events',\n 'core/str',\n 'core/notification',\n],\nfunction(\n $,\n CustomEvents,\n Selectors,\n PubSub,\n ForumEvents,\n String,\n Notification\n) {\n\n /**\n * Set the focus on the previous post in the list. Previous post is calculated\n * based on position in list as viewed top to bottom.\n *\n * @param {Object} currentPost The post that currently has focus\n */\n var focusPreviousPost = function(currentPost) {\n // See if there is a previous sibling post.\n var prevPost = currentPost.prev(Selectors.post.post);\n\n if (prevPost.length) {\n // The previous post might have replies that appear visually between\n // it and the current post (see nested view) so if that's the case\n // then the last reply will be the previous post in the list.\n var replyPost = prevPost.find(Selectors.post.post).last();\n\n if (replyPost.length) {\n // Focus the last reply.\n replyPost.focus();\n } else {\n // No replies so we can focus straight on the sibling.\n prevPost.focus();\n }\n } else {\n // If there are no siblings then jump up the tree to the parent\n // post and focus the first parent post we find.\n currentPost.parents(Selectors.post.post).first().focus();\n }\n };\n\n /**\n * Set the focus on the next post in the list. Previous post is calculated\n * based on position in list as viewed top to bottom.\n *\n * @param {Object} currentPost The post that currently has focus\n */\n var focusNextPost = function(currentPost) {\n // The next post in the visual list would be the first reply to this one\n // so let's see if we have one.\n var replyPost = currentPost.find(Selectors.post.post).first();\n\n if (replyPost.length) {\n // Got a reply.\n replyPost.focus();\n } else {\n // If we don't have a reply then the next post in the visual list would\n // be a sibling post (replying to the same parent).\n var siblingPost = currentPost.next(Selectors.post.post);\n\n if (siblingPost.length) {\n siblingPost.focus();\n } else {\n // No siblings either. That means we're the lowest level reply in a thread\n // so we need to walk back up the tree of posts and find an ancestor post that\n // has a sibling post we can focus.\n var parentPosts = currentPost.parents(Selectors.post.post).toArray();\n\n for (var i = 0; i < parentPosts.length; i++) {\n var ancestorSiblingPost = $(parentPosts[i]).next(Selectors.post.post);\n\n if (ancestorSiblingPost.length) {\n ancestorSiblingPost.focus();\n break;\n }\n }\n }\n }\n };\n\n /**\n * Check if the element is inside the in page reply section.\n *\n * @param {Object} element The element to check\n * @return {Boolean}\n */\n var isElementInInPageReplySection = function(element) {\n var inPageReply = $(element).closest(Selectors.post.inpageReplyContent);\n return inPageReply.length ? true : false;\n };\n\n /**\n * Initialise the keyboard accessibility controls for the discussion.\n *\n * @param {Object} root The discussion root element\n */\n var initAccessibilityKeyboardNav = function(root) {\n var posts = root.find(Selectors.post.post);\n\n // Take each post action out of the tab index.\n posts.each(function(index, post) {\n var actions = $(post).find(Selectors.post.action);\n var firstAction = actions.first();\n actions.attr('tabindex', '-1');\n firstAction.attr('tabindex', 0);\n });\n\n CustomEvents.define(root, [\n CustomEvents.events.up,\n CustomEvents.events.down,\n CustomEvents.events.next,\n CustomEvents.events.previous,\n CustomEvents.events.home,\n CustomEvents.events.end,\n ]);\n\n root.on(CustomEvents.events.up, function(e, data) {\n var activeElement = document.activeElement;\n\n if (isElementInInPageReplySection(activeElement)) {\n // Focus is currently inside the in page reply section so don't move focus\n // to another post.\n return;\n }\n\n var focusPost = $(activeElement).closest(Selectors.post.post);\n\n if (focusPost.length) {\n focusPreviousPost(focusPost);\n } else {\n root.find(Selectors.post.post).first().focus();\n }\n\n data.originalEvent.preventDefault();\n });\n\n root.on(CustomEvents.events.down, function(e, data) {\n var activeElement = document.activeElement;\n\n if (isElementInInPageReplySection(activeElement)) {\n // Focus is currently inside the in page reply section so don't move focus\n // to another post.\n return;\n }\n\n var focusPost = $(activeElement).closest(Selectors.post.post);\n\n if (focusPost.length) {\n focusNextPost(focusPost);\n } else {\n root.find(Selectors.post.post).first().focus();\n }\n\n data.originalEvent.preventDefault();\n });\n\n root.on(CustomEvents.events.home, function(e, data) {\n if (isElementInInPageReplySection(document.activeElement)) {\n // Focus is currently inside the in page reply section so don't move focus\n // to another post.\n return;\n }\n root.find(Selectors.post.post).first().focus();\n data.originalEvent.preventDefault();\n });\n\n root.on(CustomEvents.events.end, function(e, data) {\n if (isElementInInPageReplySection(document.activeElement)) {\n // Focus is currently inside the in page reply section so don't move focus\n // to another post.\n return;\n }\n root.find(Selectors.post.post).last().focus();\n data.originalEvent.preventDefault();\n });\n\n root.on(CustomEvents.events.next, Selectors.post.action, function(e, data) {\n var currentAction = $(e.target);\n var container = currentAction.closest(Selectors.post.actionsContainer);\n var actions = container.find(Selectors.post.action);\n var nextAction = currentAction.next(Selectors.post.action);\n\n actions.attr('tabindex', '-1');\n\n if (!nextAction.length) {\n nextAction = actions.first();\n }\n\n nextAction.attr('tabindex', 0);\n nextAction.focus();\n\n data.originalEvent.preventDefault();\n });\n\n root.on(CustomEvents.events.previous, Selectors.post.action, function(e, data) {\n var currentAction = $(e.target);\n var container = currentAction.closest(Selectors.post.actionsContainer);\n var actions = container.find(Selectors.post.action);\n var nextAction = currentAction.prev(Selectors.post.action);\n\n actions.attr('tabindex', '-1');\n\n if (!nextAction.length) {\n nextAction = actions.last();\n }\n\n nextAction.attr('tabindex', 0);\n nextAction.focus();\n\n data.originalEvent.preventDefault();\n });\n\n root.on(CustomEvents.events.home, Selectors.post.action, function(e, data) {\n var currentAction = $(e.target);\n var container = currentAction.closest(Selectors.post.actionsContainer);\n var actions = container.find(Selectors.post.action);\n var firstAction = actions.first();\n\n actions.attr('tabindex', '-1');\n firstAction.attr('tabindex', 0);\n firstAction.focus();\n\n e.stopPropagation();\n data.originalEvent.preventDefault();\n });\n\n root.on(CustomEvents.events.end, Selectors.post.action, function(e, data) {\n var currentAction = $(e.target);\n var container = currentAction.closest(Selectors.post.actionsContainer);\n var actions = container.find(Selectors.post.action);\n var lastAction = actions.last();\n\n actions.attr('tabindex', '-1');\n lastAction.attr('tabindex', 0);\n lastAction.focus();\n\n e.stopPropagation();\n data.originalEvent.preventDefault();\n });\n\n PubSub.subscribe(ForumEvents.SUBSCRIPTION_TOGGLED, function(data) {\n var subscribed = data.subscriptionState;\n var updateMessage = subscribed ? 'discussionsubscribed' : 'discussionunsubscribed';\n String.get_string(updateMessage, \"forum\")\n .then(function(s) {\n return Notification.addNotification({\n message: s,\n type: \"info\"\n });\n })\n .catch(Notification.exception);\n });\n };\n\n return {\n init: function(root) {\n initAccessibilityKeyboardNav(root);\n }\n };\n});\n"],"names":["define","$","CustomEvents","Selectors","PubSub","ForumEvents","String","Notification","isElementInInPageReplySection","element","closest","post","inpageReplyContent","length","initAccessibilityKeyboardNav","root","find","each","index","actions","action","firstAction","first","attr","events","up","down","next","previous","home","end","on","e","data","activeElement","document","focusPost","currentPost","prevPost","prev","replyPost","last","focus","parents","focusPreviousPost","originalEvent","preventDefault","siblingPost","parentPosts","toArray","i","ancestorSiblingPost","focusNextPost","currentAction","target","actionsContainer","nextAction","stopPropagation","lastAction","subscribe","SUBSCRIPTION_TOGGLED","updateMessage","subscriptionState","get_string","then","s","addNotification","message","type","catch","exception","init"],"mappings":";;;;;;;AAsBAA,8BACA,CACI,SACA,iCACA,sBACA,cACA,yBACA,WACA,sBAEJ,SACIC,EACAC,aACAC,UACAC,OACAC,YACAC,OACAC,kBA8EIC,8BAAgC,SAASC,iBACvBR,EAAEQ,SAASC,QAAQP,UAAUQ,KAAKC,oBACjCC,QAQnBC,6BAA+B,SAASC,MAC5BA,KAAKC,KAAKb,UAAUQ,KAAKA,MAG/BM,MAAK,SAASC,MAAOP,UACnBQ,QAAUlB,EAAEU,MAAMK,KAAKb,UAAUQ,KAAKS,QACtCC,YAAcF,QAAQG,QAC1BH,QAAQI,KAAK,WAAY,MACzBF,YAAYE,KAAK,WAAY,MAGjCrB,aAAaF,OAAOe,KAAM,CACtBb,aAAasB,OAAOC,GACpBvB,aAAasB,OAAOE,KACpBxB,aAAasB,OAAOG,KACpBzB,aAAasB,OAAOI,SACpB1B,aAAasB,OAAOK,KACpB3B,aAAasB,OAAOM,MAGxBf,KAAKgB,GAAG7B,aAAasB,OAAOC,IAAI,SAASO,EAAGC,UACpCC,cAAgBC,SAASD,kBAEzB1B,8BAA8B0B,oBAM9BE,UAAYnC,EAAEiC,eAAexB,QAAQP,UAAUQ,KAAKA,MAEpDyB,UAAUvB,OA9GE,SAASwB,iBAEzBC,SAAWD,YAAYE,KAAKpC,UAAUQ,KAAKA,SAE3C2B,SAASzB,OAAQ,KAIb2B,UAAYF,SAAStB,KAAKb,UAAUQ,KAAKA,MAAM8B,OAE/CD,UAAU3B,OAEV2B,UAAUE,QAGVJ,SAASI,aAKbL,YAAYM,QAAQxC,UAAUQ,KAAKA,MAAMW,QAAQoB,QA2F7CE,CAAkBR,WAElBrB,KAAKC,KAAKb,UAAUQ,KAAKA,MAAMW,QAAQoB,QAG3CT,KAAKY,cAAcC,qBAGvB/B,KAAKgB,GAAG7B,aAAasB,OAAOE,MAAM,SAASM,EAAGC,UACtCC,cAAgBC,SAASD,kBAEzB1B,8BAA8B0B,oBAM9BE,UAAYnC,EAAEiC,eAAexB,QAAQP,UAAUQ,KAAKA,MAEpDyB,UAAUvB,OApGF,SAASwB,iBAGrBG,UAAYH,YAAYrB,KAAKb,UAAUQ,KAAKA,MAAMW,WAElDkB,UAAU3B,OAEV2B,UAAUE,YACP,KAGCK,YAAcV,YAAYV,KAAKxB,UAAUQ,KAAKA,SAE9CoC,YAAYlC,OACZkC,YAAYL,qBAKRM,YAAcX,YAAYM,QAAQxC,UAAUQ,KAAKA,MAAMsC,UAElDC,EAAI,EAAGA,EAAIF,YAAYnC,OAAQqC,IAAK,KACrCC,oBAAsBlD,EAAE+C,YAAYE,IAAIvB,KAAKxB,UAAUQ,KAAKA,SAE5DwC,oBAAoBtC,OAAQ,CAC5BsC,oBAAoBT,iBA4E5BU,CAAchB,WAEdrB,KAAKC,KAAKb,UAAUQ,KAAKA,MAAMW,QAAQoB,QAG3CT,KAAKY,cAAcC,qBAGvB/B,KAAKgB,GAAG7B,aAAasB,OAAOK,MAAM,SAASG,EAAGC,MACtCzB,8BAA8B2B,SAASD,iBAK3CnB,KAAKC,KAAKb,UAAUQ,KAAKA,MAAMW,QAAQoB,QACvCT,KAAKY,cAAcC,qBAGvB/B,KAAKgB,GAAG7B,aAAasB,OAAOM,KAAK,SAASE,EAAGC,MACrCzB,8BAA8B2B,SAASD,iBAK3CnB,KAAKC,KAAKb,UAAUQ,KAAKA,MAAM8B,OAAOC,QACtCT,KAAKY,cAAcC,qBAGvB/B,KAAKgB,GAAG7B,aAAasB,OAAOG,KAAMxB,UAAUQ,KAAKS,QAAQ,SAASY,EAAGC,UAC7DoB,cAAgBpD,EAAE+B,EAAEsB,QAEpBnC,QADYkC,cAAc3C,QAAQP,UAAUQ,KAAK4C,kBAC7BvC,KAAKb,UAAUQ,KAAKS,QACxCoC,WAAaH,cAAc1B,KAAKxB,UAAUQ,KAAKS,QAEnDD,QAAQI,KAAK,WAAY,MAEpBiC,WAAW3C,SACZ2C,WAAarC,QAAQG,SAGzBkC,WAAWjC,KAAK,WAAY,GAC5BiC,WAAWd,QAEXT,KAAKY,cAAcC,oBAGvB/B,KAAKgB,GAAG7B,aAAasB,OAAOI,SAAUzB,UAAUQ,KAAKS,QAAQ,SAASY,EAAGC,UACjEoB,cAAgBpD,EAAE+B,EAAEsB,QAEpBnC,QADYkC,cAAc3C,QAAQP,UAAUQ,KAAK4C,kBAC7BvC,KAAKb,UAAUQ,KAAKS,QACxCoC,WAAaH,cAAcd,KAAKpC,UAAUQ,KAAKS,QAEnDD,QAAQI,KAAK,WAAY,MAEpBiC,WAAW3C,SACZ2C,WAAarC,QAAQsB,QAGzBe,WAAWjC,KAAK,WAAY,GAC5BiC,WAAWd,QAEXT,KAAKY,cAAcC,oBAGvB/B,KAAKgB,GAAG7B,aAAasB,OAAOK,KAAM1B,UAAUQ,KAAKS,QAAQ,SAASY,EAAGC,UAG7Dd,QAFgBlB,EAAE+B,EAAEsB,QACM5C,QAAQP,UAAUQ,KAAK4C,kBAC7BvC,KAAKb,UAAUQ,KAAKS,QACxCC,YAAcF,QAAQG,QAE1BH,QAAQI,KAAK,WAAY,MACzBF,YAAYE,KAAK,WAAY,GAC7BF,YAAYqB,QAEZV,EAAEyB,kBACFxB,KAAKY,cAAcC,oBAGvB/B,KAAKgB,GAAG7B,aAAasB,OAAOM,IAAK3B,UAAUQ,KAAKS,QAAQ,SAASY,EAAGC,UAG5Dd,QAFgBlB,EAAE+B,EAAEsB,QACM5C,QAAQP,UAAUQ,KAAK4C,kBAC7BvC,KAAKb,UAAUQ,KAAKS,QACxCsC,WAAavC,QAAQsB,OAEzBtB,QAAQI,KAAK,WAAY,MACzBmC,WAAWnC,KAAK,WAAY,GAC5BmC,WAAWhB,QAEXV,EAAEyB,kBACFxB,KAAKY,cAAcC,oBAGvB1C,OAAOuD,UAAUtD,YAAYuD,sBAAsB,SAAS3B,UAEpD4B,cADa5B,KAAK6B,kBACW,uBAAyB,yBAC1DxD,OAAOyD,WAAWF,cAAe,SAC5BG,MAAK,SAASC,UACJ1D,aAAa2D,gBAAgB,CAChCC,QAASF,EACTG,KAAM,YAGbC,MAAM9D,aAAa+D,qBAIzB,CACHC,KAAM,SAASxD,MACXD,6BAA6BC"}PK-B\WC͇!amd/build/favourite_toggle.min.jsnu[/** * Handle discussion subscription toggling on a discussion list in * the forum view. * * @module mod_forum/favourite_toggle * @copyright 2019 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("mod_forum/favourite_toggle",["jquery","core/templates","core/notification","mod_forum/repository","mod_forum/selectors","core/str"],(function($,Templates,Notification,Repository,Selectors,String){return{init:function(root,preventDefault,callback){root.on("click",Selectors.favourite.toggle,(function(e){var toggleElement=$(this),forumId=toggleElement.data("forumid"),discussionId=toggleElement.data("discussionid"),subscriptionState=toggleElement.data("targetstate");Repository.setFavouriteDiscussionState(forumId,discussionId,subscriptionState).then((function(context){return callback(toggleElement,context)})).then((function(){return String.get_string("favouriteupdated","forum").done((function(s){return Notification.addNotification({message:s,type:"info"})}))})).catch(Notification.exception),preventDefault&&e.preventDefault()}))}}})); //# sourceMappingURL=favourite_toggle.min.js.mapPK-B\0nnamd/build/selectors.min.jsnu[/** * Common CSS selectors for the forum UI. * * @module mod_forum/selectors * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("mod_forum/selectors",[],(function(){return{subscription:{toggle:"[data-type='subscription-toggle'][data-action='toggle']"},summary:{actions:"[data-container='discussion-summary-actions']"},post:{post:'[data-region="post"]',action:'[data-region="post-action"]',actionsContainer:'[data-region="post-actions-container"]',authorName:'[data-region="author-name"]',forumCoreContent:"[data-region-content='forum-post-core']",forumContent:"[data-content='forum-post']",forumSubject:"[data-region-content='forum-post-core-subject']",inpageCancelButton:"button[name='cancelbtn']",inpageReplyButton:"button",inpageReplyLink:"[data-action='collapsible-link']",inpageReplyCancelButton:"[data-action='cancel-inpage-reply']",inpageReplyCreateButton:"[data-action='create-inpage-reply']",inpageReplyContainer:'[data-region="inpage-reply-container"]',inpageReplyContent:"[data-content='inpage-reply-content']",inpageReplyForm:"form[data-content='inpage-reply-form']",inpageSubmitBtn:"[data-action='forum-inpage-submit']",inpageSubmitBtnText:"[data-region='submit-text']",loadingIconContainer:"[data-region='loading-icon-container']",repliesContainer:"[data-region='replies-container']",replyCount:'[data-region="reply-count"]',modeSelect:"select[name='mode']",showReplies:'[data-action="show-replies"]',hideReplies:'[data-action="hide-replies"]',repliesVisibilityToggleContainer:'[data-region="replies-visibility-toggle-container"]'},lock:{toggle:"[data-action='toggle'][data-type='lock-toggle']",icon:"[data-region='locked-icon']"},favourite:{toggle:"[data-type='favorite-toggle'][data-action='toggle']"},pin:{toggle:"[data-type='pin-toggle'][data-action='toggle']"},discussion:{tools:'[data-container="discussion-tools"]',item:'[data-region="discussion-list-item"]',lockedLabel:"[data-region='locked-label']",subscribedLabel:"[data-region='subscribed-label']",timedLabel:"[data-region='timed-label']"}}})); //# sourceMappingURL=selectors.min.js.mapPK-B\wf,v$v$$amd/build/discussion_list.min.js.mapnu[{"version":3,"file":"discussion_list.min.js","sources":["../src/discussion_list.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module for the list of discussions on when viewing a forum.\n *\n * @module mod_forum/discussion_list\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/templates',\n 'core/str',\n 'core/notification',\n 'mod_forum/subscription_toggle',\n 'mod_forum/selectors',\n 'mod_forum/repository',\n 'core/pubsub',\n 'mod_forum/forum_events',\n 'core_form/changechecker',\n], function(\n $,\n Templates,\n Str,\n Notification,\n SubscriptionToggle,\n Selectors,\n Repository,\n PubSub,\n ForumEvents,\n FormChangeChecker\n) {\n var registerEventListeners = function(root) {\n PubSub.subscribe(ForumEvents.SUBSCRIPTION_TOGGLED, function(data) {\n var discussionId = data.discussionId;\n var subscribed = data.subscriptionState;\n var discussionListItem = root.find(Selectors.discussion.item + '[data-discussionid= ' + discussionId + ']');\n var subscribedLabel = discussionListItem.find(Selectors.discussion.subscribedLabel);\n if (subscribed) {\n discussionListItem.addClass('subscribed');\n subscribedLabel.removeAttr('hidden');\n } else {\n discussionListItem.removeClass('subscribed');\n subscribedLabel.attr('hidden', true);\n }\n });\n\n root.on('click', Selectors.post.inpageCancelButton, function(e) {\n // Tell formchangechecker to reset the form state.\n FormChangeChecker.resetFormDirtyState(e.currentTarget);\n });\n\n root.on('click', Selectors.favourite.toggle, function(e) {\n e.preventDefault();\n\n var toggleElement = $(this);\n var forumId = toggleElement.data('forumid');\n var discussionId = toggleElement.data('discussionid');\n var subscriptionState = toggleElement.data('targetstate');\n Repository.setFavouriteDiscussionState(forumId, discussionId, subscriptionState)\n .then(function() {\n return location.reload();\n })\n .catch(Notification.exception);\n });\n\n root.on('click', Selectors.pin.toggle, function(e) {\n e.preventDefault();\n var toggleElement = $(this);\n var forumId = toggleElement.data('forumid');\n var discussionId = toggleElement.data('discussionid');\n var state = toggleElement.data('targetstate');\n Repository.setPinDiscussionState(forumId, discussionId, state)\n .then(function() {\n return location.reload();\n })\n .catch(Notification.exception);\n });\n\n root.on('click', Selectors.lock.toggle, function(e) {\n var toggleElement = $(this);\n var forumId = toggleElement.data('forumid');\n var discussionId = toggleElement.data('discussionid');\n var state = toggleElement.data('state');\n\n Repository.setDiscussionLockState(forumId, discussionId, state)\n .then(function(context) {\n var icon = toggleElement.parents(Selectors.summary.actions).find(Selectors.lock.icon);\n var lockedLabel = toggleElement.parents(Selectors.discussion.item).find(Selectors.discussion.lockedLabel);\n if (context.locked) {\n icon.removeClass('hidden');\n lockedLabel.removeAttr('hidden');\n } else {\n icon.addClass('hidden');\n lockedLabel.attr('hidden', true);\n }\n return context;\n })\n .then(function(context) {\n context.forumid = forumId;\n return Templates.render('mod_forum/discussion_lock_toggle', context);\n })\n .then(function(html, js) {\n return Templates.replaceNode(toggleElement, html, js);\n })\n .then(function() {\n return Str.get_string('lockupdated', 'forum')\n .done(function(s) {\n return Notification.addNotification({\n message: s,\n type: \"info\"\n });\n });\n })\n .catch(Notification.exception);\n\n e.preventDefault();\n });\n };\n\n return {\n init: function(root) {\n SubscriptionToggle.init(root, false, function(toggleElement, context) {\n var toggleId = toggleElement.attr('id');\n var newTargetState = context.userstate.subscribed ? 0 : 1;\n toggleElement.data('targetstate', newTargetState);\n\n var stringKey = context.userstate.subscribed ? 'unsubscribediscussion' : 'subscribediscussion';\n return Str.get_string(stringKey, 'mod_forum')\n .then(function(string) {\n toggleElement.closest('td').find('label[for=\"' + toggleId + '\"]').find('span').text(string);\n return string;\n });\n });\n registerEventListeners(root);\n }\n };\n});\n"],"names":["define","$","Templates","Str","Notification","SubscriptionToggle","Selectors","Repository","PubSub","ForumEvents","FormChangeChecker","init","root","toggleElement","context","toggleId","attr","newTargetState","userstate","subscribed","data","stringKey","get_string","then","string","closest","find","text","subscribe","SUBSCRIPTION_TOGGLED","discussionId","subscriptionState","discussionListItem","discussion","item","subscribedLabel","addClass","removeAttr","removeClass","on","post","inpageCancelButton","e","resetFormDirtyState","currentTarget","favourite","toggle","preventDefault","this","forumId","setFavouriteDiscussionState","location","reload","catch","exception","pin","state","setPinDiscussionState","lock","setDiscussionLockState","icon","parents","summary","actions","lockedLabel","locked","forumid","render","html","js","replaceNode","done","s","addNotification","message","type","registerEventListeners"],"mappings":";;;;;;;AAsBAA,mCAAO,CACH,SACA,iBACA,WACA,oBACA,gCACA,sBACA,uBACA,cACA,yBACA,4BACD,SACCC,EACAC,UACAC,IACAC,aACAC,mBACAC,UACAC,WACAC,OACAC,YACAC,yBA0FO,CACHC,KAAM,SAASC,MACXP,mBAAmBM,KAAKC,MAAM,GAAO,SAASC,cAAeC,aACrDC,SAAWF,cAAcG,KAAK,MAC9BC,eAAiBH,QAAQI,UAAUC,WAAa,EAAI,EACxDN,cAAcO,KAAK,cAAeH,oBAE9BI,UAAYP,QAAQI,UAAUC,WAAa,wBAA0B,6BAClEhB,IAAImB,WAAWD,UAAW,aAC5BE,MAAK,SAASC,eACXX,cAAcY,QAAQ,MAAMC,KAAK,cAAgBX,SAAW,MAAMW,KAAK,QAAQC,KAAKH,QAC7EA,aAnGE,SAASZ,MAClCJ,OAAOoB,UAAUnB,YAAYoB,sBAAsB,SAAST,UACpDU,aAAeV,KAAKU,aACpBX,WAAaC,KAAKW,kBAClBC,mBAAqBpB,KAAKc,KAAKpB,UAAU2B,WAAWC,KAAO,uBAAyBJ,aAAe,KACnGK,gBAAkBH,mBAAmBN,KAAKpB,UAAU2B,WAAWE,iBAC/DhB,YACAa,mBAAmBI,SAAS,cAC5BD,gBAAgBE,WAAW,YAE3BL,mBAAmBM,YAAY,cAC/BH,gBAAgBnB,KAAK,UAAU,OAIvCJ,KAAK2B,GAAG,QAASjC,UAAUkC,KAAKC,oBAAoB,SAASC,GAEzDhC,kBAAkBiC,oBAAoBD,EAAEE,kBAG5ChC,KAAK2B,GAAG,QAASjC,UAAUuC,UAAUC,QAAQ,SAASJ,GAClDA,EAAEK,qBAEElC,cAAgBZ,EAAE+C,MAClBC,QAAUpC,cAAcO,KAAK,WAC7BU,aAAejB,cAAcO,KAAK,gBAClCW,kBAAoBlB,cAAcO,KAAK,eAC3Cb,WAAW2C,4BAA4BD,QAASnB,aAAcC,mBACzDR,MAAK,kBACK4B,SAASC,YAEnBC,MAAMjD,aAAakD,cAG5B1C,KAAK2B,GAAG,QAASjC,UAAUiD,IAAIT,QAAQ,SAASJ,GAC5CA,EAAEK,qBACElC,cAAgBZ,EAAE+C,MAClBC,QAAUpC,cAAcO,KAAK,WAC7BU,aAAejB,cAAcO,KAAK,gBAClCoC,MAAQ3C,cAAcO,KAAK,eAC/Bb,WAAWkD,sBAAsBR,QAASnB,aAAc0B,OACnDjC,MAAK,kBACK4B,SAASC,YAEnBC,MAAMjD,aAAakD,cAG5B1C,KAAK2B,GAAG,QAASjC,UAAUoD,KAAKZ,QAAQ,SAASJ,OACzC7B,cAAgBZ,EAAE+C,MAClBC,QAAUpC,cAAcO,KAAK,WAC7BU,aAAejB,cAAcO,KAAK,gBAClCoC,MAAQ3C,cAAcO,KAAK,SAE/Bb,WAAWoD,uBAAuBV,QAASnB,aAAc0B,OACpDjC,MAAK,SAAST,aACP8C,KAAO/C,cAAcgD,QAAQvD,UAAUwD,QAAQC,SAASrC,KAAKpB,UAAUoD,KAAKE,MAC5EI,YAAcnD,cAAcgD,QAAQvD,UAAU2B,WAAWC,MAAMR,KAAKpB,UAAU2B,WAAW+B,oBACzFlD,QAAQmD,QACRL,KAAKtB,YAAY,UACjB0B,YAAY3B,WAAW,YAEvBuB,KAAKxB,SAAS,UACd4B,YAAYhD,KAAK,UAAU,IAExBF,WAEVS,MAAK,SAAST,gBACXA,QAAQoD,QAAUjB,QACX/C,UAAUiE,OAAO,mCAAoCrD,YAE/DS,MAAK,SAAS6C,KAAMC,WACVnE,UAAUoE,YAAYzD,cAAeuD,KAAMC,OAErD9C,MAAK,kBACKpB,IAAImB,WAAW,cAAe,SAChCiD,MAAK,SAASC,UACJpE,aAAaqE,gBAAgB,CAChCC,QAASF,EACTG,KAAM,eAIrBtB,MAAMjD,aAAakD,WAExBZ,EAAEK,oBAkBF6B,CAAuBhE"}PK-B\ [l~~amd/build/forum_events.min.jsnu[/** * Events for the forum activity. * * @module mod_forum/forum_events * @copyright 2019 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("mod_forum/forum_events",[],(function(){return{SUBSCRIPTION_TOGGLED:"mod_forum/subscription_toggle:subscriptionToggled"}})); //# sourceMappingURL=forum_events.min.js.mapPK-B\{!amd/build/forum_events.min.js.mapnu[{"version":3,"file":"forum_events.min.js","sources":["../src/forum_events.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Events for the forum activity.\n *\n * @module mod_forum/forum_events\n * @copyright 2019 Jun Pataleta \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([], function() {\n return {\n SUBSCRIPTION_TOGGLED: 'mod_forum/subscription_toggle:subscriptionToggled',\n };\n});\n"],"names":["define","SUBSCRIPTION_TOGGLED"],"mappings":";;;;;;;AAsBAA,gCAAO,IAAI,iBACA,CACHC,qBAAsB"}PK-B\qv>@amd/build/posts_list.min.jsnu[/** * This module is the highest level module for the calendar. It is * responsible for initialising all of the components required for * the calendar to run. It also coordinates the interaction between * components by listening for and responding to different events * triggered within the calendar UI. * * @module mod_forum/posts_list * @copyright 2019 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("mod_forum/posts_list",["jquery","core/templates","core/notification","core/pending","mod_forum/selectors","mod_forum/inpage_reply","core_form/changechecker"],(function($,Templates,Notification,Pending,Selectors,InPageReply,FormChangeChecker){return{init:function(root,throttlingwarningmsg){!function(root,throttlingwarningmsg){root.on("click",Selectors.post.inpageReplyLink,(function(e){if(e.preventDefault(),window.location.hash){var url=window.location.href.split("#")[0];history.pushState({},document.title,url)}var pending=new Pending("inpage-reply"),currentTarget=$(e.currentTarget).parents(Selectors.post.forumCoreContent),currentSubject=currentTarget.find(Selectors.post.forumSubject),currentRoot=$(e.currentTarget).parents(Selectors.post.forumContent),context={postid:$(currentRoot).data("post-id"),reply_url:$(e.currentTarget).attr("href"),sesskey:M.cfg.sesskey,parentsubject:currentSubject.data("replySubject"),canreplyprivately:$(e.currentTarget).data("can-reply-privately"),postformat:InPageReply.CONTENT_FORMATS.MOODLE,throttlingwarningmsg:throttlingwarningmsg};if(currentRoot.find(Selectors.post.inpageReplyContent).length){var form=currentRoot.find(Selectors.post.inpageReplyContent);form.slideToggle(300,pending.resolve),form.is(":visible")&&form.find("textarea").focus()}else Templates.render("mod_forum/inpage_reply",context).then((function(html,js){return Templates.appendNodeContents(currentTarget,html,js)})).then((function(){return currentRoot.find(Selectors.post.inpageReplyContent).slideToggle(300,pending.resolve).find("textarea").focus()})).then((function(){FormChangeChecker.watchFormById("inpage-reply-".concat(context.postid))})).catch(Notification.exception)}))}(root,throttlingwarningmsg),InPageReply.init(root)}}})); //# sourceMappingURL=posts_list.min.js.mapPK-B\ 99(amd/build/subscription_toggle.min.js.mapnu[{"version":3,"file":"subscription_toggle.min.js","sources":["../src/subscription_toggle.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Handle discussion subscription toggling on a discussion list in\n * the forum view.\n *\n * @module mod_forum/subscription_toggle\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/templates',\n 'core/notification',\n 'mod_forum/repository',\n 'mod_forum/selectors',\n 'core/pubsub',\n 'mod_forum/forum_events',\n ], function(\n $,\n Templates,\n Notification,\n Repository,\n Selectors,\n PubSub,\n ForumEvents\n ) {\n\n /**\n * Register event listeners for the subscription toggle.\n *\n * @param {object} root The discussion list root element\n * @param {boolean} preventDefault Should the default action of the event be prevented\n * @param {function} callback Success callback\n */\n var registerEventListeners = function(root, preventDefault, callback) {\n root.on('click', Selectors.subscription.toggle, function(e) {\n var toggleElement = $(this);\n var forumId = toggleElement.data('forumid');\n var discussionId = toggleElement.data('discussionid');\n var subscriptionState = toggleElement.data('targetstate');\n\n Repository.setDiscussionSubscriptionState(forumId, discussionId, subscriptionState)\n .then(function(context) {\n PubSub.publish(ForumEvents.SUBSCRIPTION_TOGGLED, {\n discussionId: discussionId,\n subscriptionState: subscriptionState\n });\n return callback(toggleElement, context);\n })\n .catch(Notification.exception);\n\n if (preventDefault) {\n e.preventDefault();\n }\n });\n };\n\n return {\n init: registerEventListeners\n };\n});\n"],"names":["define","$","Templates","Notification","Repository","Selectors","PubSub","ForumEvents","init","root","preventDefault","callback","on","subscription","toggle","e","toggleElement","this","forumId","data","discussionId","subscriptionState","setDiscussionSubscriptionState","then","context","publish","SUBSCRIPTION_TOGGLED","catch","exception"],"mappings":";;;;;;;;AAuBAA,uCAAO,CACC,SACA,iBACA,oBACA,uBACA,sBACA,cACA,2BACD,SACCC,EACAC,UACAC,aACAC,WACAC,UACAC,OACAC,mBAiCG,CACHC,KAxByB,SAASC,KAAMC,eAAgBC,UACxDF,KAAKG,GAAG,QAASP,UAAUQ,aAAaC,QAAQ,SAASC,OACjDC,cAAgBf,EAAEgB,MAClBC,QAAUF,cAAcG,KAAK,WAC7BC,aAAeJ,cAAcG,KAAK,gBAClCE,kBAAoBL,cAAcG,KAAK,eAE3Cf,WAAWkB,+BAA+BJ,QAASE,aAAcC,mBAC5DE,MAAK,SAASC,gBACXlB,OAAOmB,QAAQlB,YAAYmB,qBAAsB,CAC7CN,aAAcA,aACdC,kBAAmBA,oBAEhBV,SAASK,cAAeQ,YAElCG,MAAMxB,aAAayB,WAEpBlB,gBACAK,EAAEL"}PK-B\%pffamd/build/lock_toggle.min.jsnu[/** * Handle the manual locking of individual discussions * * @module mod_forum/lock_toggle * @copyright 2019 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("mod_forum/lock_toggle",["jquery","core/templates","core/notification","mod_forum/repository","mod_forum/selectors"],(function($,Templates,Notification,Repository,Selectors){return{init:function(root,preventDefault){root.on("click",Selectors.lock.toggle,(function(e){var toggleElement=$(this),forumId=toggleElement.data("forumid"),discussionId=toggleElement.data("discussionid"),state=toggleElement.data("state");Repository.setDiscussionLockState(forumId,discussionId,state).then((function(){return location.reload()})).catch(Notification.exception),preventDefault&&e.preventDefault()}))}}})); //# sourceMappingURL=lock_toggle.min.js.mapPK-B\W amd/build/discussion_list.min.jsnu[/** * Module for the list of discussions on when viewing a forum. * * @module mod_forum/discussion_list * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("mod_forum/discussion_list",["jquery","core/templates","core/str","core/notification","mod_forum/subscription_toggle","mod_forum/selectors","mod_forum/repository","core/pubsub","mod_forum/forum_events","core_form/changechecker"],(function($,Templates,Str,Notification,SubscriptionToggle,Selectors,Repository,PubSub,ForumEvents,FormChangeChecker){return{init:function(root){SubscriptionToggle.init(root,!1,(function(toggleElement,context){var toggleId=toggleElement.attr("id"),newTargetState=context.userstate.subscribed?0:1;toggleElement.data("targetstate",newTargetState);var stringKey=context.userstate.subscribed?"unsubscribediscussion":"subscribediscussion";return Str.get_string(stringKey,"mod_forum").then((function(string){return toggleElement.closest("td").find('label[for="'+toggleId+'"]').find("span").text(string),string}))})),function(root){PubSub.subscribe(ForumEvents.SUBSCRIPTION_TOGGLED,(function(data){var discussionId=data.discussionId,subscribed=data.subscriptionState,discussionListItem=root.find(Selectors.discussion.item+"[data-discussionid= "+discussionId+"]"),subscribedLabel=discussionListItem.find(Selectors.discussion.subscribedLabel);subscribed?(discussionListItem.addClass("subscribed"),subscribedLabel.removeAttr("hidden")):(discussionListItem.removeClass("subscribed"),subscribedLabel.attr("hidden",!0))})),root.on("click",Selectors.post.inpageCancelButton,(function(e){FormChangeChecker.resetFormDirtyState(e.currentTarget)})),root.on("click",Selectors.favourite.toggle,(function(e){e.preventDefault();var toggleElement=$(this),forumId=toggleElement.data("forumid"),discussionId=toggleElement.data("discussionid"),subscriptionState=toggleElement.data("targetstate");Repository.setFavouriteDiscussionState(forumId,discussionId,subscriptionState).then((function(){return location.reload()})).catch(Notification.exception)})),root.on("click",Selectors.pin.toggle,(function(e){e.preventDefault();var toggleElement=$(this),forumId=toggleElement.data("forumid"),discussionId=toggleElement.data("discussionid"),state=toggleElement.data("targetstate");Repository.setPinDiscussionState(forumId,discussionId,state).then((function(){return location.reload()})).catch(Notification.exception)})),root.on("click",Selectors.lock.toggle,(function(e){var toggleElement=$(this),forumId=toggleElement.data("forumid"),discussionId=toggleElement.data("discussionid"),state=toggleElement.data("state");Repository.setDiscussionLockState(forumId,discussionId,state).then((function(context){var icon=toggleElement.parents(Selectors.summary.actions).find(Selectors.lock.icon),lockedLabel=toggleElement.parents(Selectors.discussion.item).find(Selectors.discussion.lockedLabel);return context.locked?(icon.removeClass("hidden"),lockedLabel.removeAttr("hidden")):(icon.addClass("hidden"),lockedLabel.attr("hidden",!0)),context})).then((function(context){return context.forumid=forumId,Templates.render("mod_forum/discussion_lock_toggle",context)})).then((function(html,js){return Templates.replaceNode(toggleElement,html,js)})).then((function(){return Str.get_string("lockupdated","forum").done((function(s){return Notification.addNotification({message:s,type:"info"})}))})).catch(Notification.exception),e.preventDefault()}))}(root)}}})); //# sourceMappingURL=discussion_list.min.js.mapPK-B\ɰ'amd/build/form-user-selector.min.js.mapnu[{"version":3,"file":"form-user-selector.min.js","sources":["../src/form-user-selector.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Enrolled user selector module.\n *\n * @module mod_forum/form-user-selector\n * @copyright 2019 Shamim Rezaie\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'core/ajax', 'core/templates'], function($, Ajax, Templates) {\n return /** @alias module:mod_forum/form-user-selector */ {\n processResults: function(selector, results) {\n var users = [];\n $.each(results, function(index, user) {\n users.push({\n value: user.id,\n label: user._label\n });\n });\n return users;\n },\n\n transport: function(selector, query, success, failure) {\n var promise;\n var courseid = $(selector).attr('courseid');\n var contextid = $(selector).attr('data-contextid');\n\n promise = Ajax.call([{\n methodname: 'core_enrol_search_users',\n args: {\n courseid: courseid,\n search: query,\n searchanywhere: true,\n page: 0,\n perpage: 30,\n contextid: contextid,\n }\n }]);\n\n promise[0].then(function(results) {\n var promises = [],\n i = 0;\n\n // Render the label.\n $.each(results, function(index, user) {\n promises.push(Templates.render('mod_forum/form-user-selector-suggestion', user));\n });\n\n // Apply the label to the results.\n return $.when.apply($.when, promises).then(function() {\n var args = arguments;\n $.each(results, function(index, user) {\n user._label = args[i];\n i++;\n });\n success(results);\n return;\n });\n\n }).fail(failure);\n }\n\n };\n\n});\n"],"names":["define","$","Ajax","Templates","processResults","selector","results","users","each","index","user","push","value","id","label","_label","transport","query","success","failure","courseid","attr","contextid","call","methodname","args","search","searchanywhere","page","perpage","then","promises","i","render","when","apply","arguments","fail"],"mappings":";;;;;;;AAuBAA,sCAAO,CAAC,SAAU,YAAa,mBAAmB,SAASC,EAAGC,KAAMC,iBACP,CACrDC,eAAgB,SAASC,SAAUC,aAC3BC,MAAQ,UACZN,EAAEO,KAAKF,SAAS,SAASG,MAAOC,MAC5BH,MAAMI,KAAK,CACPC,MAAOF,KAAKG,GACZC,MAAOJ,KAAKK,YAGbR,OAGXS,UAAW,SAASX,SAAUY,MAAOC,QAASC,aAEtCC,SAAWnB,EAAEI,UAAUgB,KAAK,YAC5BC,UAAYrB,EAAEI,UAAUgB,KAAK,kBAEvBnB,KAAKqB,KAAK,CAAC,CACjBC,WAAY,0BACZC,KAAM,CACFL,SAAUA,SACVM,OAAQT,MACRU,gBAAgB,EAChBC,KAAM,EACNC,QAAS,GACTP,UAAWA,cAIX,GAAGQ,MAAK,SAASxB,aACjByB,SAAW,GACXC,EAAI,SAGR/B,EAAEO,KAAKF,SAAS,SAASG,MAAOC,MAC5BqB,SAASpB,KAAKR,UAAU8B,OAAO,0CAA2CvB,UAIvET,EAAEiC,KAAKC,MAAMlC,EAAEiC,KAAMH,UAAUD,MAAK,eACnCL,KAAOW,UACXnC,EAAEO,KAAKF,SAAS,SAASG,MAAOC,MAC5BA,KAAKK,OAASU,KAAKO,GACnBA,OAEJd,QAAQZ,eAIb+B,KAAKlB"}PK-B\ȫs WW)amd/build/discussion_nested_v2.min.js.mapnu[{"version":3,"file":"discussion_nested_v2.min.js","sources":["../src/discussion_nested_v2.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module for viewing a discussion in nested v2 view.\n *\n * @module mod_forum/discussion_nested_v2\n * @copyright 2019 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport $ from 'jquery';\nimport AutoRows from 'core/auto_rows';\nimport CustomEvents from 'core/custom_interaction_events';\nimport * as FormChangeChecker from 'core_form/changechecker';\nimport Notification from 'core/notification';\nimport Templates from 'core/templates';\nimport Discussion from 'mod_forum/discussion';\nimport InPageReply from 'mod_forum/inpage_reply';\nimport LockToggle from 'mod_forum/lock_toggle';\nimport FavouriteToggle from 'mod_forum/favourite_toggle';\nimport Pin from 'mod_forum/pin_toggle';\nimport Selectors from 'mod_forum/selectors';\nimport Subscribe from 'mod_forum/subscription_toggle';\n\nconst ANIMATION_DURATION = 150;\n\n/**\n * Get the closest post container element from the given element.\n *\n * @param {Object} element jQuery element to search from\n * @return {Object} jQuery element\n */\nconst getPostContainer = (element) => {\n return element.closest(Selectors.post.post);\n};\n\n/**\n * Get the closest post container element from the given element.\n *\n * @param {Object} element jQuery element to search from\n * @param {Number} id Id of the post to find.\n * @return {Object} jQuery element\n */\nconst getPostContainerById = (element, id) => {\n return element.find(`${Selectors.post.post}[data-post-id=${id}]`);\n};\n\n/**\n * Get the parent post container elements from the given element.\n *\n * @param {Object} element jQuery element to search from\n * @return {Object} jQuery element\n */\nconst getParentPostContainers = (element) => {\n return element.parents(Selectors.post.post);\n};\n\n/**\n * Get the post content container element from the post container element.\n *\n * @param {Object} postContainer jQuery element for the post container\n * @return {Object} jQuery element\n */\nconst getPostContentContainer = (postContainer) => {\n return postContainer.children().not(Selectors.post.repliesContainer).find(Selectors.post.forumCoreContent);\n};\n\n/**\n * Get the in page reply container element from the post container element.\n *\n * @param {Object} postContainer jQuery element for the post container\n * @return {Object} jQuery element\n */\nconst getInPageReplyContainer = (postContainer) => {\n return postContainer.children().filter(Selectors.post.inpageReplyContainer);\n};\n\n/**\n * Get the in page reply form element from the post container element.\n *\n * @param {Object} postContainer jQuery element for the post container\n * @return {Object} jQuery element\n */\nconst getInPageReplyForm = (postContainer) => {\n return getInPageReplyContainer(postContainer).find(Selectors.post.inpageReplyContent);\n};\n\n/**\n * Get the in page reply create (reply) button element from the post container element.\n *\n * @param {Object} postContainer jQuery element for the post container\n * @return {Object} jQuery element\n */\nconst getInPageReplyCreateButton = (postContainer) => {\n return getPostContentContainer(postContainer).find(Selectors.post.inpageReplyCreateButton);\n};\n\n/**\n * Get the replies visibility toggle container (show/hide replies button container) element\n * from the post container element.\n *\n * @param {Object} postContainer jQuery element for the post container\n * @return {Object} jQuery element\n */\nconst getRepliesVisibilityToggleContainer = (postContainer) => {\n return postContainer.children(Selectors.post.repliesVisibilityToggleContainer);\n};\n\n/**\n * Get the replies container element from the post container element.\n *\n * @param {Object} postContainer jQuery element for the post container\n * @return {Object} jQuery element\n */\nconst getRepliesContainer = (postContainer) => {\n return postContainer.children(Selectors.post.repliesContainer);\n};\n\n/**\n * Check if the post has any replies.\n *\n * @param {Object} postContainer jQuery element for the post container\n * @return {Bool}\n */\nconst hasReplies = (postContainer) => {\n return getRepliesContainer(postContainer).children().length > 0;\n};\n\n/**\n * Get the show replies button element from the replies visibility toggle container element.\n *\n * @param {Object} replyVisibilityToggleContainer jQuery element for the toggle container\n * @return {Object} jQuery element\n */\nconst getShowRepliesButton = (replyVisibilityToggleContainer) => {\n return replyVisibilityToggleContainer.find(Selectors.post.showReplies);\n};\n\n/**\n * Get the hide replies button element from the replies visibility toggle container element.\n *\n * @param {Object} replyVisibilityToggleContainer jQuery element for the toggle container\n * @return {Object} jQuery element\n */\nconst getHideRepliesButton = (replyVisibilityToggleContainer) => {\n return replyVisibilityToggleContainer.find(Selectors.post.hideReplies);\n};\n\n/**\n * Check if the replies are visible.\n *\n * @param {Object} postContainer jQuery element for the post container\n * @return {Bool}\n */\nconst repliesVisible = (postContainer) => {\n const repliesContainer = getRepliesContainer(postContainer);\n return repliesContainer.is(':visible');\n};\n\n/**\n * Show the post replies.\n *\n * @param {Object} postContainer jQuery element for the post container\n * @param {Number|null} postIdToSee Id of the post to scroll into view (if any)\n */\nconst showReplies = (postContainer, postIdToSee = null) => {\n const repliesContainer = getRepliesContainer(postContainer);\n const replyVisibilityToggleContainer = getRepliesVisibilityToggleContainer(postContainer);\n const showButton = getShowRepliesButton(replyVisibilityToggleContainer);\n const hideButton = getHideRepliesButton(replyVisibilityToggleContainer);\n\n showButton.addClass('hidden');\n hideButton.removeClass('hidden');\n\n repliesContainer.slideDown({\n duration: ANIMATION_DURATION,\n queue: false,\n complete: () => {\n if (postIdToSee) {\n const postContainerToSee = getPostContainerById(repliesContainer, postIdToSee);\n if (postContainerToSee.length) {\n postContainerToSee[0].scrollIntoView();\n }\n }\n }\n }).css('display', 'none').fadeIn(ANIMATION_DURATION);\n};\n\n/**\n * Hide the post replies.\n *\n * @param {Object} postContainer jQuery element for the post container\n */\nconst hideReplies = (postContainer) => {\n const repliesContainer = getRepliesContainer(postContainer);\n const replyVisibilityToggleContainer = getRepliesVisibilityToggleContainer(postContainer);\n const showButton = getShowRepliesButton(replyVisibilityToggleContainer);\n const hideButton = getHideRepliesButton(replyVisibilityToggleContainer);\n\n showButton.removeClass('hidden');\n hideButton.addClass('hidden');\n\n repliesContainer.slideUp({\n duration: ANIMATION_DURATION,\n queue: false\n }).fadeOut(ANIMATION_DURATION);\n};\n\n/** Variable to hold the showInPageReplyForm function after it's built. */\nlet showInPageReplyForm = null;\n\n/**\n * Build the showInPageReplyForm function with the given additional template context.\n *\n * @param {Object} additionalTemplateContext Additional render context for the in page reply template.\n * @return {Function}\n */\nconst buildShowInPageReplyFormFunction = (additionalTemplateContext) => {\n /**\n * Show the in page reply form in the given in page reply container. The form\n * display will be animated.\n *\n * @param {Object} postContainer jQuery element for the post container\n */\n return async(postContainer) => {\n\n const inPageReplyContainer = getInPageReplyContainer(postContainer);\n const repliesVisibilityToggleContainer = getRepliesVisibilityToggleContainer(postContainer);\n const inPageReplyCreateButton = getInPageReplyCreateButton(postContainer);\n\n if (!hasInPageReplyForm(inPageReplyContainer)) {\n try {\n const html = await renderInPageReplyTemplate(additionalTemplateContext, inPageReplyCreateButton, postContainer);\n Templates.appendNodeContents(inPageReplyContainer, html, '');\n } catch (e) {\n Notification.exception(e);\n }\n\n FormChangeChecker.watchForm(postContainer[0].querySelector('form'));\n }\n\n inPageReplyCreateButton.fadeOut(ANIMATION_DURATION, () => {\n const inPageReplyForm = getInPageReplyForm(postContainer);\n inPageReplyForm.slideDown({\n duration: ANIMATION_DURATION,\n queue: false,\n complete: () => {\n inPageReplyForm.find('textarea').focus();\n }\n }).css('display', 'none').fadeIn(ANIMATION_DURATION);\n\n if (repliesVisibilityToggleContainer.length && hasReplies(postContainer)) {\n repliesVisibilityToggleContainer.fadeIn(ANIMATION_DURATION);\n hideReplies(postContainer);\n }\n });\n };\n};\n\n/**\n * Hide the in page reply form in the given in page reply container. The form\n * display will be animated.\n *\n * @param {Object} postContainer jQuery element for the post container\n * @param {Number|null} postIdToSee Id of the post to scroll into view (if any)\n */\nconst hideInPageReplyForm = (postContainer, postIdToSee = null) => {\n const inPageReplyForm = getInPageReplyForm(postContainer);\n const inPageReplyCreateButton = getInPageReplyCreateButton(postContainer);\n const repliesVisibilityToggleContainer = getRepliesVisibilityToggleContainer(postContainer);\n\n if (repliesVisibilityToggleContainer.length && hasReplies(postContainer)) {\n repliesVisibilityToggleContainer.fadeOut(ANIMATION_DURATION);\n if (!repliesVisible(postContainer)) {\n showReplies(postContainer, postIdToSee);\n }\n }\n\n inPageReplyForm.slideUp({\n duration: ANIMATION_DURATION,\n queue: false,\n complete: () => {\n inPageReplyCreateButton.fadeIn(ANIMATION_DURATION);\n }\n }).fadeOut(200);\n};\n\n/**\n * Check if the in page reply container contains the in page reply form.\n *\n * @param {Object} inPageReplyContainer jQuery element for the in page reply container\n * @return {Bool}\n */\nconst hasInPageReplyForm = (inPageReplyContainer) => {\n return inPageReplyContainer.find(Selectors.post.inpageReplyContent).length > 0;\n};\n\n/**\n * Render the template to generate the in page reply form HTML.\n *\n * @param {Object} additionalTemplateContext Additional render context for the in page reply template\n * @param {Object} button jQuery element for the reply button that was clicked\n * @param {Object} postContainer jQuery element for the post container\n * @return {Object} jQuery promise\n */\nconst renderInPageReplyTemplate = (additionalTemplateContext, button, postContainer) => {\n const postContentContainer = getPostContentContainer(postContainer);\n const currentSubject = postContentContainer.find(Selectors.post.forumSubject).text();\n const currentAuthorName = postContentContainer.find(Selectors.post.authorName).text();\n const context = {\n postid: postContainer.data('post-id'),\n \"reply_url\": button.attr('data-href'),\n sesskey: M.cfg.sesskey,\n parentsubject: currentSubject,\n parentauthorname: currentAuthorName,\n canreplyprivately: button.data('can-reply-privately'),\n postformat: InPageReply.CONTENT_FORMATS.MOODLE,\n ...additionalTemplateContext\n };\n\n return Templates.render('mod_forum/inpage_reply_v2', context);\n};\n\n/**\n * Increment the total reply count in the show/hide replies buttons for the post.\n *\n * @param {Object} postContainer jQuery element for the post container\n */\nconst incrementTotalReplyCount = (postContainer) => {\n getRepliesVisibilityToggleContainer(postContainer).find(Selectors.post.replyCount).each((index, element) => {\n const currentCount = parseInt(element.innerText, 10);\n element.innerText = currentCount + 1;\n });\n};\n\n/**\n * Create all of the event listeners for the discussion.\n *\n * @param {Object} root jQuery element for the discussion container\n */\nconst registerEventListeners = (root) => {\n CustomEvents.define(root, [CustomEvents.events.activate]);\n // Auto expanding text area for in page reply.\n AutoRows.init(root);\n\n // Reply button is clicked.\n root.on(CustomEvents.events.activate, Selectors.post.inpageReplyCreateButton, (e, data) => {\n data.originalEvent.preventDefault();\n const postContainer = getPostContainer($(e.currentTarget));\n showInPageReplyForm(postContainer);\n });\n\n // Cancel in page reply button.\n root.on(CustomEvents.events.activate, Selectors.post.inpageReplyCancelButton, (e, data) => {\n data.originalEvent.preventDefault();\n const postContainer = getPostContainer($(e.currentTarget));\n hideInPageReplyForm(postContainer);\n });\n\n // Show replies button clicked.\n root.on(CustomEvents.events.activate, Selectors.post.showReplies, (e, data) => {\n data.originalEvent.preventDefault();\n const postContainer = getPostContainer($(e.target));\n showReplies(postContainer);\n });\n\n // Hide replies button clicked.\n root.on(CustomEvents.events.activate, Selectors.post.hideReplies, (e, data) => {\n data.originalEvent.preventDefault();\n const postContainer = getPostContainer($(e.target));\n hideReplies(postContainer);\n });\n\n // Post created with in page reply.\n root.on(InPageReply.EVENTS.POST_CREATED, Selectors.post.inpageSubmitBtn, (e, newPostId) => {\n const currentTarget = $(e.currentTarget);\n const postContainer = getPostContainer(currentTarget);\n const postContainers = getParentPostContainers(currentTarget);\n hideInPageReplyForm(postContainer, newPostId);\n\n postContainers.each((index, container) => {\n incrementTotalReplyCount($(container));\n });\n });\n};\n\n/**\n * Initialise the javascript for the discussion in nested v2 display mode.\n *\n * @param {Object} root jQuery element for the discussion container\n * @param {Object} context Additional render context for the in page reply template\n */\nexport const init = (root, context) => {\n // Build the showInPageReplyForm function with the additional render context.\n showInPageReplyForm = buildShowInPageReplyFormFunction(context);\n // Add discussion event listeners.\n registerEventListeners(root);\n // Initialise default discussion javascript (keyboard nav etc).\n Discussion.init(root);\n // Add in page reply javascript.\n InPageReply.init(root);\n\n // Initialise the settings menu javascript.\n const discussionToolsContainer = root.find(Selectors.discussion.tools);\n LockToggle.init(discussionToolsContainer, false);\n FavouriteToggle.init(discussionToolsContainer, false, (toggleElement, response) => {\n const newTargetState = response.userstate.favourited ? 0 : 1;\n return toggleElement.data('targetstate', newTargetState);\n });\n Pin.init(discussionToolsContainer, false, (toggleElement, response) => {\n const newTargetState = response.pinned ? 0 : 1;\n return toggleElement.data('targetstate', newTargetState);\n });\n Subscribe.init(discussionToolsContainer, false, (toggleElement, response) => {\n const newTargetState = response.userstate.subscribed ? 0 : 1;\n toggleElement.data('targetstate', newTargetState);\n });\n};\n"],"names":["getPostContainer","element","closest","Selectors","post","getPostContainerById","id","find","getPostContentContainer","postContainer","children","not","repliesContainer","forumCoreContent","getInPageReplyContainer","filter","inpageReplyContainer","getInPageReplyForm","inpageReplyContent","getInPageReplyCreateButton","inpageReplyCreateButton","getRepliesVisibilityToggleContainer","repliesVisibilityToggleContainer","getRepliesContainer","hasReplies","length","getShowRepliesButton","replyVisibilityToggleContainer","showReplies","getHideRepliesButton","hideReplies","repliesVisible","is","postIdToSee","showButton","hideButton","addClass","removeClass","slideDown","duration","queue","complete","postContainerToSee","scrollIntoView","css","fadeIn","slideUp","fadeOut","showInPageReplyForm","hideInPageReplyForm","inPageReplyForm","inPageReplyCreateButton","hasInPageReplyForm","inPageReplyContainer","renderInPageReplyTemplate","additionalTemplateContext","button","postContentContainer","currentSubject","forumSubject","text","currentAuthorName","authorName","context","postid","data","attr","sesskey","M","cfg","parentsubject","parentauthorname","canreplyprivately","postformat","InPageReply","CONTENT_FORMATS","MOODLE","Templates","render","registerEventListeners","root","define","CustomEvents","events","activate","init","on","e","originalEvent","preventDefault","currentTarget","inpageReplyCancelButton","target","EVENTS","POST_CREATED","inpageSubmitBtn","newPostId","postContainers","parents","each","index","container","replyCount","currentCount","parseInt","innerText","incrementTotalReplyCount","async","html","appendNodeContents","exception","FormChangeChecker","watchForm","querySelector","focus","discussionToolsContainer","discussion","tools","toggleElement","response","newTargetState","userstate","favourited","pinned","subscribed"],"mappings":";;;;;;;k4CA4CMA,iBAAoBC,SACfA,QAAQC,QAAQC,mBAAUC,KAAKA,MAUpCC,qBAAuB,CAACJ,QAASK,KAC5BL,QAAQM,eAAQJ,mBAAUC,KAAKA,8BAAqBE,SAmBzDE,wBAA2BC,eACtBA,cAAcC,WAAWC,IAAIR,mBAAUC,KAAKQ,kBAAkBL,KAAKJ,mBAAUC,KAAKS,kBASvFC,wBAA2BL,eACtBA,cAAcC,WAAWK,OAAOZ,mBAAUC,KAAKY,sBASpDC,mBAAsBR,eACjBK,wBAAwBL,eAAeF,KAAKJ,mBAAUC,KAAKc,oBAShEC,2BAA8BV,eACzBD,wBAAwBC,eAAeF,KAAKJ,mBAAUC,KAAKgB,yBAUhEC,oCAAuCZ,eAClCA,cAAcC,SAASP,mBAAUC,KAAKkB,kCAS3CC,oBAAuBd,eAClBA,cAAcC,SAASP,mBAAUC,KAAKQ,kBAS3CY,WAAcf,eACTc,oBAAoBd,eAAeC,WAAWe,OAAS,EAS5DC,qBAAwBC,gCACnBA,+BAA+BpB,KAAKJ,mBAAUC,KAAKwB,aASxDC,qBAAwBF,gCACnBA,+BAA+BpB,KAAKJ,mBAAUC,KAAK0B,aASxDC,eAAkBtB,eACKc,oBAAoBd,eACrBuB,GAAG,YASzBJ,YAAc,SAACnB,mBAAewB,mEAAc,WACxCrB,iBAAmBW,oBAAoBd,eACvCkB,+BAAiCN,oCAAoCZ,eACrEyB,WAAaR,qBAAqBC,gCAClCQ,WAAaN,qBAAqBF,gCAExCO,WAAWE,SAAS,UACpBD,WAAWE,YAAY,UAEvBzB,iBAAiB0B,UAAU,CACvBC,SAvJmB,IAwJnBC,OAAO,EACPC,SAAU,QACFR,YAAa,OACPS,mBAAqBrC,qBAAqBO,iBAAkBqB,aAC9DS,mBAAmBjB,QACnBiB,mBAAmB,GAAGC,qBAInCC,IAAI,UAAW,QAAQC,OAjKH,MAyKrBf,YAAerB,sBACXG,iBAAmBW,oBAAoBd,eACvCkB,+BAAiCN,oCAAoCZ,eACrEyB,WAAaR,qBAAqBC,gCAClCQ,WAAaN,qBAAqBF,gCAExCO,WAAWG,YAAY,UACvBF,WAAWC,SAAS,UAEpBxB,iBAAiBkC,QAAQ,CACrBP,SAnLmB,IAoLnBC,OAAO,IACRO,QArLoB,UAyLvBC,oBAAsB,WAyDpBC,oBAAsB,SAACxC,mBAAewB,mEAAc,WAChDiB,gBAAkBjC,mBAAmBR,eACrC0C,wBAA0BhC,2BAA2BV,eACrDa,iCAAmCD,oCAAoCZ,eAEzEa,iCAAiCG,QAAUD,WAAWf,iBACtDa,iCAAiCyB,QAxPd,KAyPdhB,eAAetB,gBAChBmB,YAAYnB,cAAewB,cAInCiB,gBAAgBJ,QAAQ,CACpBP,SA/PmB,IAgQnBC,OAAO,EACPC,SAAU,KACNU,wBAAwBN,OAlQT,QAoQpBE,QAAQ,MASTK,mBAAsBC,sBACjBA,qBAAqB9C,KAAKJ,mBAAUC,KAAKc,oBAAoBO,OAAS,EAW3E6B,0BAA4B,CAACC,0BAA2BC,OAAQ/C,uBAC5DgD,qBAAuBjD,wBAAwBC,eAC/CiD,eAAiBD,qBAAqBlD,KAAKJ,mBAAUC,KAAKuD,cAAcC,OACxEC,kBAAoBJ,qBAAqBlD,KAAKJ,mBAAUC,KAAK0D,YAAYF,OACzEG,QAAU,CACZC,OAAQvD,cAAcwD,KAAK,qBACdT,OAAOU,KAAK,aACzBC,QAASC,EAAEC,IAAIF,QACfG,cAAeZ,eACfa,iBAAkBV,kBAClBW,kBAAmBhB,OAAOS,KAAK,uBAC/BQ,WAAYC,sBAAYC,gBAAgBC,UACrCrB,kCAGAsB,mBAAUC,OAAO,4BAA6Bf,UAoBnDgB,uBAA0BC,0CACfC,OAAOD,KAAM,CAACE,mCAAaC,OAAOC,8BAEtCC,KAAKL,MAGdA,KAAKM,GAAGJ,mCAAaC,OAAOC,SAAUjF,mBAAUC,KAAKgB,yBAAyB,CAACmE,EAAGtB,QAC9EA,KAAKuB,cAAcC,uBACbhF,cAAgBT,kBAAiB,mBAAEuF,EAAEG,gBAC3C1C,oBAAoBvC,kBAIxBuE,KAAKM,GAAGJ,mCAAaC,OAAOC,SAAUjF,mBAAUC,KAAKuF,yBAAyB,CAACJ,EAAGtB,QAC9EA,KAAKuB,cAAcC,uBACbhF,cAAgBT,kBAAiB,mBAAEuF,EAAEG,gBAC3CzC,oBAAoBxC,kBAIxBuE,KAAKM,GAAGJ,mCAAaC,OAAOC,SAAUjF,mBAAUC,KAAKwB,aAAa,CAAC2D,EAAGtB,QAClEA,KAAKuB,cAAcC,uBACbhF,cAAgBT,kBAAiB,mBAAEuF,EAAEK,SAC3ChE,YAAYnB,kBAIhBuE,KAAKM,GAAGJ,mCAAaC,OAAOC,SAAUjF,mBAAUC,KAAK0B,aAAa,CAACyD,EAAGtB,QAClEA,KAAKuB,cAAcC,uBACbhF,cAAgBT,kBAAiB,mBAAEuF,EAAEK,SAC3C9D,YAAYrB,kBAIhBuE,KAAKM,GAAGZ,sBAAYmB,OAAOC,aAAc3F,mBAAUC,KAAK2F,iBAAiB,CAACR,EAAGS,mBACnEN,eAAgB,mBAAEH,EAAEG,eACpBjF,cAAgBT,iBAAiB0F,eACjCO,eAAyCP,cAnUpCQ,QAAQ/F,mBAAUC,KAAKA,MAoUlC6C,oBAAoBxC,cAAeuF,WAEnCC,eAAeE,MAAK,CAACC,MAAOC,aApDF5F,CAAAA,gBAC9BY,oCAAoCZ,eAAeF,KAAKJ,mBAAUC,KAAKkG,YAAYH,MAAK,CAACC,MAAOnG,iBACtFsG,aAAeC,SAASvG,QAAQwG,UAAW,IACjDxG,QAAQwG,UAAYF,aAAe,MAkD/BG,EAAyB,mBAAEL,iCAWnB,CAACrB,KAAMjB,WA/KeR,IAAAA,0BAAAA,0BAiLiBQ,QAAvDf,oBA1KO2D,MAAAA,sBAEGtD,qBAAuBvC,wBAAwBL,eAC/Ca,iCAAmCD,oCAAoCZ,eACvE0C,wBAA0BhC,2BAA2BV,mBAEtD2C,mBAAmBC,sBAAuB,WAEjCuD,WAAatD,0BAA0BC,0BAA2BJ,wBAAyB1C,kCACvFoG,mBAAmBxD,qBAAsBuD,KAAM,IAC3D,MAAOrB,yBACQuB,UAAUvB,GAG3BwB,kBAAkBC,UAAUvG,cAAc,GAAGwG,cAAc,SAG/D9D,wBAAwBJ,QAzNL,KAyNiC,WAC1CG,gBAAkBjC,mBAAmBR,eAC3CyC,gBAAgBZ,UAAU,CACtBC,SA5NW,IA6NXC,OAAO,EACPC,SAAU,KACNS,gBAAgB3C,KAAK,YAAY2G,WAEtCtE,IAAI,UAAW,QAAQC,OAjOX,KAmOXvB,iCAAiCG,QAAUD,WAAWf,iBACtDa,iCAAiCuB,OApOtB,KAqOXf,YAAYrB,oBA+IxBsE,uBAAuBC,0BAEZK,KAAKL,4BAEJK,KAAKL,YAGXmC,yBAA2BnC,KAAKzE,KAAKJ,mBAAUiH,WAAWC,4BACrDhC,KAAK8B,0BAA0B,6BAC1B9B,KAAK8B,0BAA0B,GAAO,CAACG,cAAeC,kBAC5DC,eAAiBD,SAASE,UAAUC,WAAa,EAAI,SACpDJ,cAAcrD,KAAK,cAAeuD,uCAEzCnC,KAAK8B,0BAA0B,GAAO,CAACG,cAAeC,kBAChDC,eAAiBD,SAASI,OAAS,EAAI,SACtCL,cAAcrD,KAAK,cAAeuD,gDAEnCnC,KAAK8B,0BAA0B,GAAO,CAACG,cAAeC,kBACtDC,eAAiBD,SAASE,UAAUG,WAAa,EAAI,EAC3DN,cAAcrD,KAAK,cAAeuD"}PK-B\Qf!0 0 unsubscribeall.phpnu[. /** * @package mod_forum * @copyright 2008 Petr Skoda (http://skodak.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once("../../config.php"); require_once("lib.php"); $confirm = optional_param('confirm', false, PARAM_BOOL); $PAGE->set_url('/mod/forum/unsubscribeall.php'); // Do not autologin guest. Only proper users can have forum subscriptions. require_login(null, false); $PAGE->set_context(context_user::instance($USER->id)); $return = $CFG->wwwroot.'/'; if (isguestuser()) { redirect($return); } $strunsubscribeall = get_string('unsubscribeall', 'forum'); $PAGE->navbar->add(get_string('modulename', 'forum')); $PAGE->navbar->add($strunsubscribeall); $PAGE->set_title($strunsubscribeall); $PAGE->set_heading($COURSE->fullname); echo $OUTPUT->header(); echo $OUTPUT->heading($strunsubscribeall); if (data_submitted() and $confirm and confirm_sesskey()) { $forums = \mod_forum\subscriptions::get_unsubscribable_forums(); foreach($forums as $forum) { \mod_forum\subscriptions::unsubscribe_user($USER->id, $forum, context_module::instance($forum->cm), true); } $DB->delete_records('forum_discussion_subs', array('userid' => $USER->id)); $DB->set_field('user', 'autosubscribe', 0, array('id'=>$USER->id)); echo $OUTPUT->box(get_string('unsubscribealldone', 'forum')); echo $OUTPUT->continue_button($return); echo $OUTPUT->footer(); die; } else { $count = new stdClass(); $count->forums = count(\mod_forum\subscriptions::get_unsubscribable_forums()); $count->discussions = $DB->count_records('forum_discussion_subs', array('userid' => $USER->id)); if ($count->forums || $count->discussions) { if ($count->forums && $count->discussions) { $msg = get_string('unsubscribeallconfirm', 'forum', $count); } else if ($count->forums) { $msg = get_string('unsubscribeallconfirmforums', 'forum', $count); } else if ($count->discussions) { $msg = get_string('unsubscribeallconfirmdiscussions', 'forum', $count); } echo $OUTPUT->confirm($msg, new moodle_url('unsubscribeall.php', array('confirm'=>1)), $return); echo $OUTPUT->footer(); die; } else { echo $OUTPUT->box(get_string('unsubscribeallempty', 'forum')); echo $OUTPUT->continue_button($return); echo $OUTPUT->footer(); die; } } PK-B\atests/output_email_test.phpnu[. namespace mod_forum; use mod_forum\output\forum_post_email; /** * Tests for the forum output/email class. * * @package mod_forum * @copyright 2016 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class output_email_test extends \advanced_testcase { /** * Data provider for the postdate function tests. */ public static function postdate_provider(): array { return array( 'Timed discussions disabled, timestart unset' => array( 'globalconfig' => array( 'forum_enabletimedposts' => 0, ), 'forumconfig' => array( ), 'postconfig' => array( 'modified' => 1000, ), 'discussionconfig' => array( ), 'expectation' => 1000, ), 'Timed discussions disabled, timestart set and newer' => array( 'globalconfig' => array( 'forum_enabletimedposts' => 0, ), 'forumconfig' => array( ), 'postconfig' => array( 'modified' => 1000, ), 'discussionconfig' => array( 'timestart' => 2000, ), 'expectation' => 1000, ), 'Timed discussions disabled, timestart set but older' => array( 'globalconfig' => array( 'forum_enabletimedposts' => 0, ), 'forumconfig' => array( ), 'postconfig' => array( 'modified' => 1000, ), 'discussionconfig' => array( 'timestart' => 500, ), 'expectation' => 1000, ), 'Timed discussions enabled, timestart unset' => array( 'globalconfig' => array( 'forum_enabletimedposts' => 1, ), 'forumconfig' => array( ), 'postconfig' => array( 'modified' => 1000, ), 'discussionconfig' => array( ), 'expectation' => 1000, ), 'Timed discussions enabled, timestart set and newer' => array( 'globalconfig' => array( 'forum_enabletimedposts' => 1, ), 'forumconfig' => array( ), 'postconfig' => array( 'modified' => 1000, ), 'discussionconfig' => array( 'timestart' => 2000, ), 'expectation' => 2000, ), 'Timed discussions enabled, timestart set but older' => array( 'globalconfig' => array( 'forum_enabletimedposts' => 1, ), 'forumconfig' => array( ), 'postconfig' => array( 'modified' => 1000, ), 'discussionconfig' => array( 'timestart' => 500, ), 'expectation' => 1000, ), ); } /** * Test for the forum email renderable postdate. * * @dataProvider postdate_provider * * @param array $globalconfig The configuration to set on $CFG * @param array $forumconfig The configuration for this forum * @param array $postconfig The configuration for this post * @param array $discussionconfig The configuration for this discussion * @param string $expectation The expected date */ public function test_postdate($globalconfig, $forumconfig, $postconfig, $discussionconfig, $expectation): void { global $CFG, $DB; $this->resetAfterTest(true); // Apply the global configuration. foreach ($globalconfig as $key => $value) { $CFG->$key = $value; } // Create the fixture. $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', (object) array('course' => $course->id)); $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id, false, MUST_EXIST); $this->getDataGenerator()->enrol_user($user->id, $course->id); // Create a new discussion. $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion( (object) array_merge($discussionconfig, array( 'course' => $course->id, 'forum' => $forum->id, 'userid' => $user->id, ))); // Apply the discussion configuration. // Some settings are ignored by the generator and must be set manually. $discussion = $DB->get_record('forum_discussions', array('id' => $discussion->id)); foreach ($discussionconfig as $key => $value) { $discussion->$key = $value; } $DB->update_record('forum_discussions', $discussion); // Apply the post configuration. // Some settings are ignored by the generator and must be set manually. $post = $DB->get_record('forum_posts', array('discussion' => $discussion->id)); foreach ($postconfig as $key => $value) { $post->$key = $value; } $DB->update_record('forum_posts', $post); // Create the renderable. $renderable = new forum_post_email( $course, $cm, $forum, $discussion, $post, $user, $user, true ); // Check the postdate matches our expectations. $this->assertEquals(userdate($expectation, "", \core_date::get_user_timezone($user)), $renderable->get_postdate()); } } PK-B\ ZUUtests/vaults_post_test.phpnu[. namespace mod_forum; use mod_forum_tests_generator_trait; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/generator_trait.php'); /** * The post vault tests. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \mod_forum\local\vaults\post */ final class vaults_post_test extends \advanced_testcase { // Make use of the test generator trait. use mod_forum_tests_generator_trait; /** @var \mod_forum\local\vaults\post */ private $vault; /** * Set up function for tests. */ public function setUp(): void { parent::setUp(); $vaultfactory = \mod_forum\local\container::get_vault_factory(); $this->vault = $vaultfactory->get_post_vault(); } /** * Teardown for all tests. */ public function tearDown(): void { unset($this->vault); parent::tearDown(); } /** * Test get_from_id. */ public function test_get_from_id(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); [$discussion, $post] = $this->helper_post_to_forum($forum, $user); $postentity = $this->vault->get_from_id($post->id); $this->assertEquals($post->id, $postentity->get_id()); } /** * Test get_from_discussion_id. * * @covers ::get_from_discussion_id */ public function test_get_from_discussion_id(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); [$discussion1, $post1] = $this->helper_post_to_forum($forum, $user); $post2 = $this->helper_reply_to_post($post1, $user); $post3 = $this->helper_reply_to_post($post1, $user); [$discussion2, $post4] = $this->helper_post_to_forum($forum, $user); $entities = array_values($this->vault->get_from_discussion_id($user, $discussion1->id, false)); $this->assertCount(3, $entities); $this->assertEquals($post1->id, $entities[0]->get_id()); $this->assertEquals($post2->id, $entities[1]->get_id()); $this->assertEquals($post3->id, $entities[2]->get_id()); $entities = array_values($this->vault->get_from_discussion_id($user, $discussion1->id + 1000, false)); $this->assertCount(0, $entities); } /** * Ensure that selecting posts in a discussion only returns posts that the user can see, when considering private * replies. * * @covers ::get_from_discussion_id */ public function test_get_from_discussion_id_private_replies(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, ]); [$student, $otherstudent] = $this->helper_create_users($course, 2, 'student'); [$teacher, $otherteacher] = $this->helper_create_users($course, 2, 'teacher'); [$discussion, $post] = $this->helper_post_to_forum($forum, $teacher); $reply = $this->helper_post_to_discussion($forum, $discussion, $teacher, [ 'privatereplyto' => $student->id, ]); // The user is the author. $entities = array_values($this->vault->get_from_discussion_id($teacher, $discussion->id, true)); $this->assertCount(2, $entities); $this->assertEquals($post->id, $entities[0]->get_id()); $this->assertEquals($reply->id, $entities[1]->get_id()); // The user is the intended recipient. $entities = array_values($this->vault->get_from_discussion_id($student, $discussion->id, false)); $this->assertCount(2, $entities); $this->assertEquals($post->id, $entities[0]->get_id()); $this->assertEquals($reply->id, $entities[1]->get_id()); // The user is another teacher.. $entities = array_values($this->vault->get_from_discussion_id($otherteacher, $discussion->id, true)); $this->assertCount(2, $entities); $this->assertEquals($post->id, $entities[0]->get_id()); $this->assertEquals($reply->id, $entities[1]->get_id()); // The user is a different student. $entities = array_values($this->vault->get_from_discussion_id($otherstudent, $discussion->id, false)); $this->assertCount(1, $entities); $this->assertEquals($post->id, $entities[0]->get_id()); } /** * Test get_from_discussion_ids when no discussion ids were provided. * * @covers ::get_from_discussion_ids */ public function test_get_from_discussion_ids_empty(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); $this->assertEquals([], $this->vault->get_from_discussion_ids($user, [], false)); } /** * Test get_from_discussion_ids. * * @covers ::get_from_discussion_ids */ public function test_get_from_discussion_ids(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); [$discussion1, $post1] = $this->helper_post_to_forum($forum, $user); $post2 = $this->helper_reply_to_post($post1, $user); $post3 = $this->helper_reply_to_post($post1, $user); [$discussion2, $post4] = $this->helper_post_to_forum($forum, $user); $entities = $this->vault->get_from_discussion_ids($user, [$discussion1->id], false); $this->assertCount(3, $entities); $this->assertArrayHasKey($post1->id, $entities); // Order is not guaranteed, so just verify element existence. $this->assertArrayHasKey($post2->id, $entities); $this->assertArrayHasKey($post3->id, $entities); $entities = $this->vault->get_from_discussion_ids($user, [$discussion1->id, $discussion2->id], false); $this->assertCount(4, $entities); $this->assertArrayHasKey($post1->id, $entities); // Order is not guaranteed, so just verify element existence. $this->assertArrayHasKey($post2->id, $entities); $this->assertArrayHasKey($post3->id, $entities); $this->assertArrayHasKey($post4->id, $entities); // Test ordering by id descending. $entities = $this->vault->get_from_discussion_ids($user, [$discussion1->id, $discussion2->id], false, 'id DESC'); $this->assertEquals($post4->id, array_values($entities)[0]->get_id()); $this->assertEquals($post3->id, array_values($entities)[1]->get_id()); $this->assertEquals($post2->id, array_values($entities)[2]->get_id()); $this->assertEquals($post1->id, array_values($entities)[3]->get_id()); // Test ordering by id ascending. $entities = $this->vault->get_from_discussion_ids($user, [$discussion1->id, $discussion2->id], false, 'id ASC'); $this->assertEquals($post1->id, array_values($entities)[0]->get_id()); $this->assertEquals($post2->id, array_values($entities)[1]->get_id()); $this->assertEquals($post3->id, array_values($entities)[2]->get_id()); $this->assertEquals($post4->id, array_values($entities)[3]->get_id()); } /** * Ensure that selecting posts in a discussion only returns posts that the user can see, when considering private * replies. * * @covers ::get_from_discussion_ids */ public function test_get_from_discussion_ids_private_replies(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, ]); [$student, $otherstudent] = $this->helper_create_users($course, 2, 'student'); [$teacher, $otherteacher] = $this->helper_create_users($course, 2, 'teacher'); // Create the posts structure below. // Forum: // -> Post (student 1) // ---> Post private reply (teacher 1) // -> Otherpost (teacher 1) // ---> Otherpost private reply (teacher 2) // ---> Otherpost reply (student 1) // ----> Otherpost reply private reply (teacher 1). [$discussion, $post] = $this->helper_post_to_forum($forum, $student); $postprivatereply = $this->helper_reply_to_post($post, $teacher, [ 'privatereplyto' => $student->id ]); [$otherdiscussion, $otherpost] = $this->helper_post_to_forum($forum, $teacher); $otherpostprivatereply = $this->helper_reply_to_post($otherpost, $otherteacher, [ 'privatereplyto' => $teacher->id, ]); $otherpostreply = $this->helper_reply_to_post($otherpost, $student); $otherpostreplyprivatereply = $this->helper_reply_to_post($otherpostreply, $teacher, [ 'privatereplyto' => $student->id ]); // Teacher 1. Request all posts from the vault, telling the vault that the teacher CAN see private replies made by anyone. $entities = $this->vault->get_from_discussion_ids($teacher, [$discussion->id, $otherdiscussion->id], true); $this->assertCount(6, $entities); $this->assertArrayHasKey($post->id, $entities); // Order is not guaranteed, so just verify element existence. $this->assertArrayHasKey($postprivatereply->id, $entities); $this->assertArrayHasKey($otherpost->id, $entities); $this->assertArrayHasKey($otherpostprivatereply->id, $entities); $this->assertArrayHasKey($otherpostreply->id, $entities); $this->assertArrayHasKey($otherpostreplyprivatereply->id, $entities); // Student 1. Request all posts from the vault, telling the vault that the student CAN'T see private replies made by anyone. // Teacher2's private reply to otherpost is omitted. $entities = $this->vault->get_from_discussion_ids($student, [$discussion->id, $otherdiscussion->id], false); $this->assertCount(5, $entities); $this->assertArrayHasKey($post->id, $entities); // Order is not guaranteed, so just verify element existence. $this->assertArrayHasKey($postprivatereply->id, $entities); $this->assertArrayHasKey($otherpost->id, $entities); $this->assertArrayHasKey($otherpostreply->id, $entities); $this->assertArrayHasKey($otherpostreplyprivatereply->id, $entities); // Student 1. Request all posts from the vault, telling the vault that student CAN see all private replies made. // The private reply made by teacher 2 to otherpost is now included. $entities = $this->vault->get_from_discussion_ids($student, [$discussion->id, $otherdiscussion->id], true); $this->assertCount(6, $entities); $this->assertArrayHasKey($post->id, $entities); // Order is not guaranteed, so just verify element existence. $this->assertArrayHasKey($postprivatereply->id, $entities); $this->assertArrayHasKey($otherpost->id, $entities); $this->assertArrayHasKey($otherpostprivatereply->id, $entities); $this->assertArrayHasKey($otherpostreply->id, $entities); $this->assertArrayHasKey($otherpostreplyprivatereply->id, $entities); // Teacher 2. Request all posts from the vault, telling the vault that teacher2 CAN see all private replies made. $entities = $this->vault->get_from_discussion_ids($otherteacher, [$discussion->id, $otherdiscussion->id], true); $this->assertCount(6, $entities); $this->assertArrayHasKey($post->id, $entities); // Order is not guaranteed, so just verify element existence. $this->assertArrayHasKey($postprivatereply->id, $entities); $this->assertArrayHasKey($otherpost->id, $entities); $this->assertArrayHasKey($otherpostprivatereply->id, $entities); $this->assertArrayHasKey($otherpostreply->id, $entities); $this->assertArrayHasKey($otherpostreplyprivatereply->id, $entities); // Teacher 2. Request all posts from the vault, telling the vault that teacher2 CANNOT see all private replies made. // The private replies not relating to teacher 2 directly are omitted. $entities = $this->vault->get_from_discussion_ids($otherteacher, [$discussion->id, $otherdiscussion->id], false); $this->assertCount(4, $entities); $this->assertArrayHasKey($post->id, $entities); // Order is not guaranteed, so just verify element existence. $this->assertArrayHasKey($otherpost->id, $entities); $this->assertArrayHasKey($otherpostprivatereply->id, $entities); $this->assertArrayHasKey($otherpostreply->id, $entities); // Student 2. Request all posts from the vault, telling the vault that student2 CAN'T see all private replies made. // All private replies are omitted, as none relate to student2. $entities = $this->vault->get_from_discussion_ids($otherstudent, [$discussion->id, $otherdiscussion->id], false); $this->assertCount(3, $entities); $this->assertArrayHasKey($post->id, $entities); // Order is not guaranteed, so just verify element existence. $this->assertArrayHasKey($otherpost->id, $entities); $this->assertArrayHasKey($otherpostreply->id, $entities); } /** * Test get_replies_to_post. * * @covers ::get_replies_to_post */ public function test_get_replies_to_post(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $forumgenerator = $datagenerator->get_plugin_generator('mod_forum'); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); [$discussion1, $post1] = $this->helper_post_to_forum($forum, $user); // Create a post with the same created time as the parent post to ensure // we've covered every possible scenario. $post2 = $forumgenerator->create_post((object) [ 'discussion' => $post1->discussion, 'parent' => $post1->id, 'userid' => $user->id, 'mailnow' => 1, 'subject' => 'Some subject', 'created' => $post1->created ]); $post3 = $this->helper_reply_to_post($post1, $user); $post4 = $this->helper_reply_to_post($post2, $user); [$discussion2, $post5] = $this->helper_post_to_forum($forum, $user); $entityfactory = \mod_forum\local\container::get_entity_factory(); $post1 = $entityfactory->get_post_from_stdClass($post1); $post2 = $entityfactory->get_post_from_stdClass($post2); $post3 = $entityfactory->get_post_from_stdClass($post3); $post4 = $entityfactory->get_post_from_stdClass($post4); $entities = $this->vault->get_replies_to_post($user, $post1, false); $this->assertCount(3, $entities); $this->assertEquals($post2->get_id(), $entities[0]->get_id()); $this->assertEquals($post3->get_id(), $entities[1]->get_id()); $this->assertEquals($post4->get_id(), $entities[2]->get_id()); $entities = $this->vault->get_replies_to_post($user, $post2, false); $this->assertCount(1, $entities); $this->assertEquals($post4->get_id(), $entities[0]->get_id()); $entities = $this->vault->get_replies_to_post($user, $post3, false); $this->assertCount(0, $entities); } /** * Test get_replies_to_post with private replies. * * @covers ::get_replies_to_post */ public function test_get_replies_to_post_private_replies(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, ]); // Generate a structure: // Initial post p [student] // -> Reply pa [otherstudent] // ---> Reply paa [student] // ---> Private Reply pab [teacher] // -> Private Reply pb [teacher] // -> Reply pc [otherstudent] // ---> Reply pca [student] // -----> Reply pcaa [otherstudent] // -------> Private Reply pcaaa [teacher]. [$student, $otherstudent] = $this->helper_create_users($course, 2, 'student'); [$teacher, $otherteacher] = $this->helper_create_users($course, 2, 'teacher'); [$discussion, $p] = $this->helper_post_to_forum($forum, $student); $pa = $this->helper_reply_to_post($p, $otherstudent); $paa = $this->helper_reply_to_post($pa, $student); $pab = $this->helper_reply_to_post($pa, $teacher, ['privatereplyto' => $otherstudent->id]); $pb = $this->helper_reply_to_post($p, $teacher, ['privatereplyto' => $student->id]); $pc = $this->helper_reply_to_post($p, $otherteacher); $pca = $this->helper_reply_to_post($pc, $student); $pcaa = $this->helper_reply_to_post($pca, $otherstudent); $pcaaa = $this->helper_reply_to_post($pcaa, $teacher, ['privatereplyto' => $otherstudent->id]); $entityfactory = \mod_forum\local\container::get_entity_factory(); $ep = $entityfactory->get_post_from_stdClass($p); $epa = $entityfactory->get_post_from_stdClass($pa); $epaa = $entityfactory->get_post_from_stdClass($paa); $epab = $entityfactory->get_post_from_stdClass($pab); $epb = $entityfactory->get_post_from_stdClass($pb); $epc = $entityfactory->get_post_from_stdClass($pc); $epca = $entityfactory->get_post_from_stdClass($pca); $epcaa = $entityfactory->get_post_from_stdClass($pcaa); $epcaaa = $entityfactory->get_post_from_stdClass($pcaaa); // As `student`, you should see all public posts, plus all private replies intended for you. $entities = $this->vault->get_replies_to_post($student, $ep, false); $this->assertCount(6, $entities); $this->assertEquals($epa->get_id(), $entities[0]->get_id()); $this->assertEquals($epaa->get_id(), $entities[1]->get_id()); $this->assertEquals($epb->get_id(), $entities[2]->get_id()); $this->assertEquals($epc->get_id(), $entities[3]->get_id()); $this->assertEquals($epca->get_id(), $entities[4]->get_id()); $this->assertEquals($epcaa->get_id(), $entities[5]->get_id()); $entities = $this->vault->get_replies_to_post($student, $epa, false); $this->assertCount(1, $entities); $this->assertEquals($epaa->get_id(), $entities[0]->get_id()); $this->assertEmpty($this->vault->get_replies_to_post($student, $epaa, false)); $this->assertEmpty($this->vault->get_replies_to_post($student, $epab, false)); $this->assertEmpty($this->vault->get_replies_to_post($student, $epb, false)); $this->assertEmpty($this->vault->get_replies_to_post($student, $epcaa, false)); $this->assertEmpty($this->vault->get_replies_to_post($student, $epcaaa, false)); $entities = $this->vault->get_replies_to_post($student, $epc, false); $this->assertCount(2, $entities); $this->assertEquals($epca->get_id(), $entities[0]->get_id()); $this->assertEquals($epcaa->get_id(), $entities[1]->get_id()); // As `otherstudent`, you should see all public posts, plus all private replies intended for you. $entities = $this->vault->get_replies_to_post($otherstudent, $ep, false); $this->assertCount(7, $entities); $this->assertEquals($epa->get_id(), $entities[0]->get_id()); $this->assertEquals($epaa->get_id(), $entities[1]->get_id()); $this->assertEquals($epab->get_id(), $entities[2]->get_id()); $this->assertEquals($epc->get_id(), $entities[3]->get_id()); $this->assertEquals($epca->get_id(), $entities[4]->get_id()); $this->assertEquals($epcaa->get_id(), $entities[5]->get_id()); $this->assertEquals($epcaaa->get_id(), $entities[6]->get_id()); $entities = $this->vault->get_replies_to_post($otherstudent, $epa, false); $this->assertCount(2, $entities); $this->assertEquals($epaa->get_id(), $entities[0]->get_id()); $this->assertEquals($epab->get_id(), $entities[1]->get_id()); $this->assertEmpty($this->vault->get_replies_to_post($otherstudent, $epaa, false)); $this->assertEmpty($this->vault->get_replies_to_post($otherstudent, $epab, false)); $this->assertEmpty($this->vault->get_replies_to_post($otherstudent, $epb, false)); $this->assertEmpty($this->vault->get_replies_to_post($otherstudent, $epcaaa, false)); $entities = $this->vault->get_replies_to_post($otherstudent, $epc, false); $this->assertCount(3, $entities); $this->assertEquals($epca->get_id(), $entities[0]->get_id()); $this->assertEquals($epcaa->get_id(), $entities[1]->get_id()); $this->assertEquals($epcaaa->get_id(), $entities[2]->get_id()); // The teacher who authored the private replies can see all. $entities = $this->vault->get_replies_to_post($teacher, $ep, true); $this->assertCount(8, $entities); $this->assertEquals($epa->get_id(), $entities[0]->get_id()); $this->assertEquals($epaa->get_id(), $entities[1]->get_id()); $this->assertEquals($epab->get_id(), $entities[2]->get_id()); $this->assertEquals($epb->get_id(), $entities[3]->get_id()); $this->assertEquals($epc->get_id(), $entities[4]->get_id()); $this->assertEquals($epca->get_id(), $entities[5]->get_id()); $this->assertEquals($epcaa->get_id(), $entities[6]->get_id()); $this->assertEquals($epcaaa->get_id(), $entities[7]->get_id()); $entities = $this->vault->get_replies_to_post($teacher, $epa, true); $this->assertCount(2, $entities); $this->assertEquals($epaa->get_id(), $entities[0]->get_id()); $this->assertEquals($epab->get_id(), $entities[1]->get_id()); $this->assertEmpty($this->vault->get_replies_to_post($teacher, $epaa, true)); $this->assertEmpty($this->vault->get_replies_to_post($teacher, $epab, true)); $this->assertEmpty($this->vault->get_replies_to_post($teacher, $epb, true)); $this->assertEmpty($this->vault->get_replies_to_post($teacher, $epcaaa, true)); $entities = $this->vault->get_replies_to_post($teacher, $epc, true); $this->assertCount(3, $entities); $this->assertEquals($epca->get_id(), $entities[0]->get_id()); $this->assertEquals($epcaa->get_id(), $entities[1]->get_id()); $this->assertEquals($epcaaa->get_id(), $entities[2]->get_id()); // Any other teacher can also see all. $entities = $this->vault->get_replies_to_post($otherteacher, $ep, true); $this->assertCount(8, $entities); $this->assertEquals($epa->get_id(), $entities[0]->get_id()); $this->assertEquals($epaa->get_id(), $entities[1]->get_id()); $this->assertEquals($epab->get_id(), $entities[2]->get_id()); $this->assertEquals($epb->get_id(), $entities[3]->get_id()); $this->assertEquals($epc->get_id(), $entities[4]->get_id()); $this->assertEquals($epca->get_id(), $entities[5]->get_id()); $this->assertEquals($epcaa->get_id(), $entities[6]->get_id()); $this->assertEquals($epcaaa->get_id(), $entities[7]->get_id()); $entities = $this->vault->get_replies_to_post($otherteacher, $epa, true); $this->assertCount(2, $entities); $this->assertEquals($epaa->get_id(), $entities[0]->get_id()); $this->assertEquals($epab->get_id(), $entities[1]->get_id()); $this->assertEmpty($this->vault->get_replies_to_post($otherteacher, $epaa, true)); $this->assertEmpty($this->vault->get_replies_to_post($otherteacher, $epab, true)); $this->assertEmpty($this->vault->get_replies_to_post($otherteacher, $epb, true)); $this->assertEmpty($this->vault->get_replies_to_post($otherteacher, $epcaaa, true)); $entities = $this->vault->get_replies_to_post($otherteacher, $epc, true); $this->assertCount(3, $entities); $this->assertEquals($epca->get_id(), $entities[0]->get_id()); $this->assertEquals($epcaa->get_id(), $entities[1]->get_id()); $this->assertEquals($epcaaa->get_id(), $entities[2]->get_id()); } /** * Test get_reply_count_for_discussion_ids when no discussion ids were provided. * * @covers ::get_reply_count_for_discussion_ids */ public function test_get_reply_count_for_discussion_ids_empty(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); $this->assertCount(0, $this->vault->get_reply_count_for_discussion_ids($user, [], false)); } /** * Test get_reply_count_for_discussion_ids. * * @covers ::get_reply_count_for_discussion_ids */ public function test_get_reply_count_for_discussion_ids(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); [$discussion1, $post1] = $this->helper_post_to_forum($forum, $user); $post2 = $this->helper_reply_to_post($post1, $user); $post3 = $this->helper_reply_to_post($post1, $user); $post4 = $this->helper_reply_to_post($post2, $user); [$discussion2, $post5] = $this->helper_post_to_forum($forum, $user); $post6 = $this->helper_reply_to_post($post5, $user); [$discussion3, $post7] = $this->helper_post_to_forum($forum, $user); $counts = $this->vault->get_reply_count_for_discussion_ids($user, [$discussion1->id], false); $this->assertCount(1, $counts); $this->assertEquals(3, $counts[$discussion1->id]); $counts = $this->vault->get_reply_count_for_discussion_ids($user, [$discussion1->id, $discussion2->id], false); $this->assertCount(2, $counts); $this->assertEquals(3, $counts[$discussion1->id]); $this->assertEquals(1, $counts[$discussion2->id]); $counts = $this->vault->get_reply_count_for_discussion_ids($user, [ $discussion1->id, $discussion2->id, $discussion3->id ], false); $this->assertCount(2, $counts); $this->assertEquals(3, $counts[$discussion1->id]); $this->assertEquals(1, $counts[$discussion2->id]); $counts = $this->vault->get_reply_count_for_discussion_ids($user, [ $discussion1->id, $discussion2->id, $discussion3->id, $discussion3->id + 1000 ], false); $this->assertCount(2, $counts); $this->assertEquals(3, $counts[$discussion1->id]); $this->assertEquals(1, $counts[$discussion2->id]); } /** * Test get_reply_count_for_discussion_ids. * * @covers ::get_reply_count_for_discussion_ids */ public function test_get_reply_count_for_discussion_ids_private_replies(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, ]); // Generate a structure: // Initial post p [student] // -> Reply pa [otherstudent] // ---> Reply paa [student] // ---> Private Reply pab [teacher] // -> Private Reply pb [teacher] // -> Reply pc [otherstudent] // ---> Reply pca [student] // -----> Reply pcaa [otherstudent] // -------> Private Reply pcaaa [teacher]. [$student, $otherstudent] = $this->helper_create_users($course, 2, 'student'); [$teacher, $otherteacher] = $this->helper_create_users($course, 2, 'teacher'); [$discussion, $p] = $this->helper_post_to_forum($forum, $student); $pa = $this->helper_reply_to_post($p, $otherstudent); $paa = $this->helper_reply_to_post($pa, $student); $pab = $this->helper_reply_to_post($pa, $teacher, ['privatereplyto' => $otherstudent->id]); $pb = $this->helper_reply_to_post($p, $teacher, ['privatereplyto' => $student->id]); $pc = $this->helper_reply_to_post($p, $otherteacher); $pca = $this->helper_reply_to_post($pc, $student); $pcaa = $this->helper_reply_to_post($pca, $otherstudent); $pcaaa = $this->helper_reply_to_post($pcaa, $teacher, ['privatereplyto' => $otherstudent->id]); $this->assertEquals([$discussion->id => 6], $this->vault->get_reply_count_for_discussion_ids($student, [$discussion->id], false)); $this->assertEquals([$discussion->id => 7], $this->vault->get_reply_count_for_discussion_ids($otherstudent, [$discussion->id], false)); $this->assertEquals([$discussion->id => 8], $this->vault->get_reply_count_for_discussion_ids($teacher, [$discussion->id], true)); $this->assertEquals([$discussion->id => 8], $this->vault->get_reply_count_for_discussion_ids($otherteacher, [$discussion->id], true)); } /** * Test get_reply_count_for_discussion_id. * * @covers ::get_reply_count_for_post_id_in_discussion_id */ public function test_get_reply_count_for_post_id_in_discussion_id(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); [$discussion1, $post1] = $this->helper_post_to_forum($forum, $user); $post2 = $this->helper_reply_to_post($post1, $user); $post3 = $this->helper_reply_to_post($post1, $user); $post4 = $this->helper_reply_to_post($post2, $user); [$discussion2, $post5] = $this->helper_post_to_forum($forum, $user); $post6 = $this->helper_reply_to_post($post5, $user); [$discussion3, $post7] = $this->helper_post_to_forum($forum, $user); $this->assertEquals(3, $this->vault->get_reply_count_for_post_id_in_discussion_id($user, $post1->id, $discussion1->id, false)); $this->assertEquals(1, $this->vault->get_reply_count_for_post_id_in_discussion_id($user, $post5->id, $discussion2->id, false)); $this->assertEquals(0, $this->vault->get_reply_count_for_post_id_in_discussion_id($user, $post7->id, $discussion3->id, false)); $this->assertEquals(0, $this->vault->get_reply_count_for_post_id_in_discussion_id($user, $post7->id + 1000, $discussion3->id, false)); } /** * Test get_reply_count_for_post_id_in_discussion_id. * * @covers ::get_reply_count_for_post_id_in_discussion_id */ public function test_get_reply_count_for_post_id_in_discussion_id_private_replies(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, ]); // Generate a structure: // Initial post p [student] // -> Reply pa [otherstudent] // ---> Reply paa [student] // ---> Private Reply pab [teacher] // -> Private Reply pb [teacher] // -> Reply pc [otherstudent] // ---> Reply pca [student] // -----> Reply pcaa [otherstudent] // -------> Private Reply pcaaa [teacher]. [$student, $otherstudent] = $this->helper_create_users($course, 2, 'student'); [$teacher, $otherteacher] = $this->helper_create_users($course, 2, 'teacher'); [$discussion, $p] = $this->helper_post_to_forum($forum, $student); $pa = $this->helper_reply_to_post($p, $otherstudent); $paa = $this->helper_reply_to_post($pa, $student); $pab = $this->helper_reply_to_post($pa, $teacher, ['privatereplyto' => $otherstudent->id]); $pb = $this->helper_reply_to_post($p, $teacher, ['privatereplyto' => $student->id]); $pc = $this->helper_reply_to_post($p, $otherteacher); $pca = $this->helper_reply_to_post($pc, $student); $pcaa = $this->helper_reply_to_post($pca, $otherstudent); $pcaaa = $this->helper_reply_to_post($pcaa, $teacher, ['privatereplyto' => $otherstudent->id]); $this->assertEquals(6, $this->vault->get_reply_count_for_post_id_in_discussion_id($student, $p->id, $discussion->id, false)); $this->assertEquals(7, $this->vault->get_reply_count_for_post_id_in_discussion_id($otherstudent, $p->id, $discussion->id, false)); $this->assertEquals(8, $this->vault->get_reply_count_for_post_id_in_discussion_id($teacher, $p->id, $discussion->id, true)); $this->assertEquals(8, $this->vault->get_reply_count_for_post_id_in_discussion_id($otherteacher, $p->id, $discussion->id, true)); } /** * Test get_unread_count_for_discussion_ids. * * @covers ::get_unread_count_for_discussion_ids */ public function test_get_unread_count_for_discussion_ids(): void { global $CFG; $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $otheruser = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); [$discussion1, $post1] = $this->helper_post_to_forum($forum, $user); $post2 = $this->helper_reply_to_post($post1, $user); $post3 = $this->helper_reply_to_post($post1, $user); $post4 = $this->helper_reply_to_post($post2, $user); [$discussion2, $post5] = $this->helper_post_to_forum($forum, $user); $post6 = $this->helper_reply_to_post($post5, $user); $modgenerator = $this->getDataGenerator()->get_plugin_generator('mod_forum'); $post7 = $modgenerator->create_post((object) [ 'discussion' => $post5->discussion, 'parent' => $post5->id, 'userid' => $user->id, 'mailnow' => 1, 'subject' => 'old post', // Two days ago which makes it an "old post". 'modified' => time() - 172800 ]); forum_tp_add_read_record($user->id, $post1->id); forum_tp_add_read_record($user->id, $post4->id); $CFG->forum_oldpostdays = 1; $counts = $this->vault->get_unread_count_for_discussion_ids($user, [$discussion1->id], false); $this->assertCount(1, $counts); $this->assertEquals(2, $counts[$discussion1->id]); $counts = $this->vault->get_unread_count_for_discussion_ids($user, [$discussion1->id, $discussion2->id], false); $this->assertCount(2, $counts); $this->assertEquals(2, $counts[$discussion1->id]); $this->assertEquals(2, $counts[$discussion2->id]); $counts = $this->vault->get_unread_count_for_discussion_ids($user, [ $discussion1->id, $discussion2->id, $discussion2->id + 1000 ], false); $this->assertCount(2, $counts); $this->assertEquals(2, $counts[$discussion1->id]); $this->assertEquals(2, $counts[$discussion2->id]); $counts = $this->vault->get_unread_count_for_discussion_ids($otheruser, [$discussion1->id, $discussion2->id], false); $this->assertCount(2, $counts); $this->assertEquals(4, $counts[$discussion1->id]); $this->assertEquals(2, $counts[$discussion2->id]); } /** * Test get_unread_count_for_discussion_ids when no discussion ids were provided. * * @covers ::get_unread_count_for_discussion_ids */ public function test_get_unread_count_for_discussion_ids_empty(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); $this->assertEquals([], $this->vault->get_unread_count_for_discussion_ids($user, [], false)); } /** * Test get_latest_posts_for_discussion_ids. * * @covers ::get_latest_posts_for_discussion_ids */ public function test_get_latest_posts_for_discussion_ids(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $course = $datagenerator->create_course(); [$teacher, $otherteacher] = $this->helper_create_users($course, 2, 'teacher'); [$user, $user2] = $this->helper_create_users($course, 2, 'student'); $now = time(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); [$discussion1, $post1] = $this->helper_post_to_forum($forum, $user); $post2 = $this->helper_reply_to_post($post1, $user, ['created' => $now + 100]); $post3 = $this->helper_reply_to_post($post1, $user, ['created' => $now + 101]); $post4 = $this->helper_reply_to_post($post2, $user, ['created' => $now + 102]); [$discussion2, $post5] = $this->helper_post_to_forum($forum, $user); $post6 = $this->helper_reply_to_post($post5, $user); $post7 = $this->helper_reply_to_post($post5, $user); $post8 = $this->helper_reply_to_post($post6, $user); [$discussion3, $post9] = $this->helper_post_to_forum($forum, $user); $post10 = $this->helper_post_to_discussion($forum, $discussion3, $teacher, [ 'privatereplyto' => $user->id, ]); $ids = $this->vault->get_latest_posts_for_discussion_ids($user, [$discussion1->id], false); $this->assertCount(1, $ids); $this->assertEquals($post4->id, $ids[$discussion1->id]->get_id()); $ids = $this->vault->get_latest_posts_for_discussion_ids($user, [$discussion1->id, $discussion2->id], false); $this->assertCount(2, $ids); $this->assertEquals($post4->id, $ids[$discussion1->id]->get_id()); $this->assertEquals($post8->id, $ids[$discussion2->id]->get_id()); $ids = $this->vault->get_latest_posts_for_discussion_ids($user, [$discussion1->id, $discussion2->id, $discussion3->id], false); $this->assertCount(3, $ids); $this->assertEquals($post4->id, $ids[$discussion1->id]->get_id()); $this->assertEquals($post8->id, $ids[$discussion2->id]->get_id()); $this->assertEquals($post10->id, $ids[$discussion3->id]->get_id()); // Checks the user who doesn't have access to the private reply. $ids = $this->vault->get_latest_posts_for_discussion_ids($user2, [$discussion1->id, $discussion2->id, $discussion3->id], false); $this->assertCount(3, $ids); $this->assertEquals($post4->id, $ids[$discussion1->id]->get_id()); $this->assertEquals($post8->id, $ids[$discussion2->id]->get_id()); $this->assertEquals($post9->id, $ids[$discussion3->id]->get_id()); // Checks the user with the private reply to. $ids = $this->vault->get_latest_posts_for_discussion_ids($user, [ $discussion1->id, $discussion2->id, $discussion3->id, $discussion3->id + 1000 ], false); $this->assertCount(3, $ids); $this->assertEquals($post4->id, $ids[$discussion1->id]->get_id()); $this->assertEquals($post8->id, $ids[$discussion2->id]->get_id()); $this->assertEquals($post10->id, $ids[$discussion3->id]->get_id()); } /** * Test get_latest_posts_for_discussion_ids when no discussion ids were provided. * * @covers ::get_latest_posts_for_discussion_ids */ public function test_get_latest_posts_for_discussion_ids_empty(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); $this->assertEquals([], $this->vault->get_latest_posts_for_discussion_ids($user, [], false)); } /** * Test get_first_post_for_discussion_ids. * * @covers ::get_first_post_for_discussion_ids */ public function test_get_first_post_for_discussion_ids(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); $now = time(); [$discussion1, $post1] = $this->helper_post_to_forum($forum, $user); $post2 = $this->helper_reply_to_post($post1, $user, ['created' => $now + 100]); $post3 = $this->helper_reply_to_post($post1, $user, ['created' => $now + 101]); $post4 = $this->helper_reply_to_post($post2, $user, ['created' => $now + 102]); [$discussion2, $post5] = $this->helper_post_to_forum($forum, $user); $post6 = $this->helper_reply_to_post($post5, $user); $post7 = $this->helper_reply_to_post($post5, $user); $post8 = $this->helper_reply_to_post($post6, $user); [$discussion3, $post9] = $this->helper_post_to_forum($forum, $user); $firstposts = $this->vault->get_first_post_for_discussion_ids([$discussion1->id]); $this->assertCount(1, $firstposts); $this->assertEquals($post1->id, reset($firstposts)->get_id()); $firstposts = $this->vault->get_first_post_for_discussion_ids([$discussion1->id, $discussion2->id]); $this->assertCount(2, $firstposts); $this->assertEquals($post1->id, $firstposts[$post1->id]->get_id()); $this->assertEquals($post5->id, $firstposts[$post5->id]->get_id()); $firstposts = $this->vault->get_first_post_for_discussion_ids([$discussion1->id, $discussion2->id, $discussion3->id]); $this->assertCount(3, $firstposts); $this->assertEquals($post1->id, $firstposts[$post1->id]->get_id()); $this->assertEquals($post5->id, $firstposts[$post5->id]->get_id()); $this->assertEquals($post9->id, $firstposts[$post9->id]->get_id()); $firstposts = $this->vault->get_first_post_for_discussion_ids([ $discussion1->id, $discussion2->id, $discussion3->id, $discussion3->id + 1000 ]); $this->assertCount(3, $firstposts); $this->assertEquals($post1->id, $firstposts[$post1->id]->get_id()); $this->assertEquals($post5->id, $firstposts[$post5->id]->get_id()); $this->assertEquals($post9->id, $firstposts[$post9->id]->get_id()); } /** * Test get_first_post_for_discussion_ids when no discussion ids were provided. * * @covers ::get_first_post_for_discussion_ids */ public function test_get_first_post_for_discussion_ids_empty(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); $this->assertEquals([], $this->vault->get_first_post_for_discussion_ids([])); } /** * Test get_from_filters. * * @covers ::get_from_filters */ public function test_get_from_filters(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $course = $datagenerator->create_course(); [$user, $user2] = $this->helper_create_users($course, 2, 'student'); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); [$discussion1, $post1] = $this->helper_post_to_forum($forum, $user); $post2 = $this->helper_reply_to_post($post1, $user); $post3 = $this->helper_reply_to_post($post1, $user); [$discussion2, $post4] = $this->helper_post_to_forum($forum, $user); $discussionids = [$discussion1->id, $discussion2->id]; $userids = [$user->id]; $entities = array_values($this->vault->get_from_filters($user, ['discussionids' => $discussionids, 'userids' => $userids], true, 'id ASC')); $this->assertCount(4, $entities); $this->assertEquals($post1->id, $entities[0]->get_id()); $this->assertEquals($post2->id, $entities[1]->get_id()); $this->assertEquals($post3->id, $entities[2]->get_id()); $this->assertEquals($post4->id, $entities[3]->get_id()); $entities = $this->vault->get_from_filters($user, ['discussionids' => $discussion1->id, 'userids' => $userids], false); $this->assertCount(3, $entities); $this->assertArrayHasKey($post1->id, $entities); $this->assertArrayHasKey($post2->id, $entities); $this->assertArrayHasKey($post3->id, $entities); $discussionids = [$discussion1->id, $discussion2->id]; $userids = [$user->id, $user2->id]; $entities = $this->vault->get_from_filters($user, ['discussionids' => $discussionids, 'userids' => $userids], false); $this->assertCount(4, $entities); $this->assertArrayHasKey($post1->id, $entities); $this->assertArrayHasKey($post2->id, $entities); $this->assertArrayHasKey($post3->id, $entities); $this->assertArrayHasKey($post4->id, $entities); // Test ordering by id descending. $entities = $this->vault->get_from_filters($user, ['discussionids' => $discussionids, 'userids' => $user->id], false, 'id DESC'); $this->assertEquals($post4->id, array_values($entities)[0]->get_id()); $this->assertEquals($post3->id, array_values($entities)[1]->get_id()); $this->assertEquals($post2->id, array_values($entities)[2]->get_id()); $this->assertEquals($post1->id, array_values($entities)[3]->get_id()); // Test ordering by id ascending. $entities = $this->vault->get_from_filters($user, ['discussionids' => $discussionids, 'userids' => $user->id], false, 'id ASC'); $this->assertEquals($post1->id, array_values($entities)[0]->get_id()); $this->assertEquals($post2->id, array_values($entities)[1]->get_id()); $this->assertEquals($post3->id, array_values($entities)[2]->get_id()); $this->assertEquals($post4->id, array_values($entities)[3]->get_id()); } public function test_get_from_filters_from_to_dates(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $course = $datagenerator->create_course(); [$user, $user2] = $this->helper_create_users($course, 2, 'student'); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); [$discussion1, $post1] = $this->helper_post_to_forum($forum, $user); $date = new \DateTime('2019-07-05'); $post2 = $this->helper_reply_to_post($post1, $user, ['created' => $date->getTimestamp()]); $post3 = $this->helper_reply_to_post($post1, $user, ['created' => $date->getTimestamp()]); $date->modify('+1 month'); $post4 = $this->helper_reply_to_post($post1, $user, ['created' => $date->getTimestamp()]); $post5 = $this->helper_reply_to_post($post1, $user, ['created' => $date->getTimestamp()]); $post6 = $this->helper_reply_to_post($post1, $user, ['created' => $date->getTimestamp()]); [$discussion2, $post4] = $this->helper_post_to_forum($forum, $user); $datefilter = new \DateTime('2019-07-01'); $filters = ['from' => $datefilter->getTimestamp()]; $entities = $this->vault->get_from_filters($user, $filters, false); $this->assertCount(7, $entities); $filters['to'] = $datefilter->modify('+1 month')->getTimestamp(); $entities = $this->vault->get_from_filters($user, $filters, false); $this->assertCount(2, $entities); } /** * Test get_from_filters when no discussion ids were provided. * * @covers ::get_from_filters */ public function test_get_from_filters_empty(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $course = $datagenerator->create_course(); [$student1] = $this->helper_create_users($course, 1, 'student'); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); $this->helper_post_to_forum($forum, $student1); $this->assertEquals([], $this->vault->get_from_filters($student1, [], false)); } /** * Ensure that selecting posts in a discussion only returns posts that the user can see, when considering private * replies. * * @covers ::get_from_filters */ public function test_get_from_filters_private_replies(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, ]); [$student, $otherstudent] = $this->helper_create_users($course, 2, 'student'); [$teacher, $otherteacher] = $this->helper_create_users($course, 2, 'teacher'); // Create the posts structure below. // Forum: // -> Post (student 1) // ---> Post private reply (teacher 1) // -> Otherpost (teacher 1) // ---> Otherpost private reply (teacher 2) // ---> Otherpost reply (student 1) // ----> Otherpost reply private reply (teacher 1). [$discussion, $post] = $this->helper_post_to_forum($forum, $student); $postprivatereply = $this->helper_reply_to_post($post, $teacher, [ 'privatereplyto' => $student->id ]); [$otherdiscussion, $otherpost] = $this->helper_post_to_forum($forum, $teacher); $otherpostprivatereply = $this->helper_reply_to_post($otherpost, $otherteacher, [ 'privatereplyto' => $teacher->id, ]); $otherpostreply = $this->helper_reply_to_post($otherpost, $student); $otherpostreplyprivatereply = $this->helper_reply_to_post($otherpostreply, $teacher, [ 'privatereplyto' => $student->id ]); $userids = [$otherstudent->id, $teacher->id, $otherteacher->id]; $discussionids = [$discussion->id, $otherdiscussion->id]; // Teacher 1. Request all posts from the vault, telling the vault that the teacher CAN see private replies made by anyone. $entities = $this->vault->get_from_filters($teacher, ['discussionids' => $discussionids, 'userids' => $userids], true); $this->assertCount(4, $entities); $this->assertArrayHasKey($postprivatereply->id, $entities); $this->assertArrayHasKey($otherpost->id, $entities); $this->assertArrayHasKey($otherpostprivatereply->id, $entities); $this->assertArrayHasKey($otherpostreplyprivatereply->id, $entities); // Student 1. Request all posts from the vault, telling the vault that the student CAN'T see private replies made by anyone. // Teacher2's private reply to otherpost is omitted. $entities = $this->vault->get_from_filters($student, ['discussionids' => $discussionids, 'userids' => $userids], false); $this->assertCount(3, $entities); $this->assertArrayHasKey($postprivatereply->id, $entities); $this->assertArrayHasKey($otherpost->id, $entities); $this->assertArrayHasKey($otherpostreplyprivatereply->id, $entities); // Student 1. Request all posts from the vault, telling the vault that student CAN see all private replies made. // The private reply made by teacher 2 to otherpost is now included. $entities = $this->vault->get_from_filters($student, ['discussionids' => $discussionids, 'userids' => $userids], true); $this->assertCount(4, $entities); $this->assertArrayHasKey($postprivatereply->id, $entities); $this->assertArrayHasKey($otherpost->id, $entities); $this->assertArrayHasKey($otherpostprivatereply->id, $entities); $this->assertArrayHasKey($otherpostreplyprivatereply->id, $entities); // Teacher 2. Request all posts from the vault, telling the vault that teacher2 CAN see all private replies made. $entities = $this->vault->get_from_filters($otherteacher, ['discussionids' => $discussionids, 'userids' => $userids], true); $this->assertCount(4, $entities); $this->assertArrayHasKey($otherpost->id, $entities); $this->assertArrayHasKey($otherpostprivatereply->id, $entities); $this->assertArrayHasKey($otherpostreplyprivatereply->id, $entities); // Teacher 2. Request all posts from the vault, telling the vault that teacher2 CANNOT see all private replies made. // The private replies not relating to teacher 2 directly are omitted. $entities = $this->vault->get_from_filters($otherteacher, ['discussionids' => $discussionids, 'userids' => $userids], false); $this->assertCount(2, $entities); $this->assertArrayHasKey($otherpost->id, $entities); $this->assertArrayHasKey($otherpostprivatereply->id, $entities); // Student 2. Request all posts from the vault, telling the vault that student2 CAN'T see all private replies made. // All private replies are omitted, as none relate to student2. $entities = $this->vault->get_from_filters($otherstudent, ['discussionids' => $discussionids, 'userids' => $userids], false); $this->assertCount(1, $entities); $this->assertArrayHasKey($otherpost->id, $entities); } } PK-B\S"tests/managers_capability_test.phpnu[. namespace mod_forum; use mod_forum\local\container; use mod_forum\local\entities\forum; use mod_forum\local\managers\capability as capability_manager; use mod_forum_tests_generator_trait; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once(__DIR__ . '/generator_trait.php'); /** * The capability manager tests. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \mod_forum\local\managers\capability */ final class managers_capability_test extends \advanced_testcase { // Make use of the test generator trait. use mod_forum_tests_generator_trait; /** @var stdClass */ private $user; /** @var \mod_forum\local\factories\entity */ private $entityfactory; /** @var \mod_forum\local\factories\manager */ private $managerfactory; /** @var stdClass */ private $course; /** @var stdClass */ private $forumrecord; /** @var stdClass */ private $coursemodule; /** @var context */ private $context; /** @var int */ private $roleid; /** @var \mod_forum\local\entities\discussion */ private $discussion; /** @var stdClass */ private $discussionrecord; /** @var \mod_forum\local\entities\post */ private $post; /** @var stdClass */ private $postrecord; /** * Setup function before each test. */ public function setUp(): void { global $DB; parent::setUp(); // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); $datagenerator = $this->getDataGenerator(); $this->user = $datagenerator->create_user(); $this->managerfactory = container::get_manager_factory(); $this->entityfactory = container::get_entity_factory(); $this->course = $datagenerator->create_course(); $this->forumrecord = $datagenerator->create_module('forum', ['course' => $this->course->id]); $this->coursemodule = get_coursemodule_from_instance('forum', $this->forumrecord->id); $this->context = \context_module::instance($this->coursemodule->id); $this->roleid = $DB->get_field('role', 'id', ['shortname' => 'teacher'], MUST_EXIST); $datagenerator->enrol_user($this->user->id, $this->course->id, 'teacher'); [$discussion, $post] = $this->helper_post_to_forum($this->forumrecord, $this->user, ['timemodified' => time() - 100]); $this->discussion = $this->entityfactory->get_discussion_from_stdClass($discussion); $this->discussionrecord = $discussion; $this->post = $this->entityfactory->get_post_from_stdClass( (object) array_merge((array) $post, ['timecreated' => time() - 100]) ); $this->postrecord = $post; $this->setUser($this->user); } /** * Tear down function after each test. */ public function tearDown(): void { // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); parent::tearDown(); } /** * Helper function to create a forum entity. * * @param array $forumproperties List of properties to override the prebuilt forum * @return forum */ private function create_forum(array $forumproperties = []) { $forumrecord = (object) array_merge((array) $this->forumrecord, $forumproperties); return $this->entityfactory->get_forum_from_stdClass( $forumrecord, $this->context, $this->coursemodule, $this->course ); } /** * Helper function to assign a capability to the prebuilt role (teacher). * * @param string $capability Name of the capability * @param context|null $context The context to assign the capability in */ private function give_capability($capability, $context = null) { $context = $context ?? $this->context; assign_capability($capability, CAP_ALLOW, $this->roleid, $context->id, true); } /** * Helper function to prevent a capability to the prebuilt role (teacher). * * @param string $capability Name of the capability * @param context|null $context The context to assign the capability in */ private function prevent_capability($capability, $context = null) { $context = $context ?? $this->context; assign_capability($capability, CAP_PREVENT, $this->roleid, $context->id, true); } /** * Test can_subscribe_to_forum. * * @covers ::can_subscribe_to_forum */ public function test_can_subscribe_to_forum(): void { $this->resetAfterTest(); $forum = $this->create_forum(); $guestuser = $this->getDataGenerator()->create_user(); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->assertFalse($capabilitymanager->can_subscribe_to_forum($guestuser)); $this->assertTrue($capabilitymanager->can_subscribe_to_forum($this->user)); } /** * Test can_create_discussions. * * @covers ::can_create_discussions */ public function test_can_create_discussions(): void { $this->resetAfterTest(); $forum = $this->create_forum(); $guestuser = $this->getDataGenerator()->create_user(); $user = $this->user; $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->assertFalse($capabilitymanager->can_create_discussions($guestuser)); $this->prevent_capability('mod/forum:startdiscussion'); $this->assertFalse($capabilitymanager->can_create_discussions($user)); $this->give_capability('mod/forum:startdiscussion'); $this->assertTrue($capabilitymanager->can_create_discussions($user)); $forum = $this->create_forum(['type' => 'news']); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->prevent_capability('mod/forum:addnews'); $this->assertFalse($capabilitymanager->can_create_discussions($user)); $this->give_capability('mod/forum:addnews'); $this->assertTrue($capabilitymanager->can_create_discussions($user)); $forum = $this->create_forum(['type' => 'qanda']); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->prevent_capability('mod/forum:addquestion'); $this->assertFalse($capabilitymanager->can_create_discussions($user)); $this->give_capability('mod/forum:addquestion'); $this->assertTrue($capabilitymanager->can_create_discussions($user)); // Test a forum in group mode. $forumrecord = $this->getDataGenerator()->create_module( 'forum', ['course' => $this->course->id, 'groupmode' => SEPARATEGROUPS] ); $coursemodule = get_coursemodule_from_instance('forum', $forumrecord->id); $context = \context_module::instance($coursemodule->id); $forum = $this->entityfactory->get_forum_from_stdClass( $forumrecord, $context, $coursemodule, $this->course ); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->assertFalse($capabilitymanager->can_create_discussions($user)); $this->give_capability('moodle/site:accessallgroups', $context); $this->assertTrue($capabilitymanager->can_create_discussions($user)); $this->prevent_capability('moodle/site:accessallgroups', $context); $this->assertFalse($capabilitymanager->can_create_discussions($user)); $group = $this->getDataGenerator()->create_group(['courseid' => $this->course->id]); $this->getDataGenerator()->create_group_member(['userid' => $user->id, 'groupid' => $group->id]); $this->assertTrue($capabilitymanager->can_create_discussions($user, $group->id)); // Test if cut off date is reached. $now = time(); $forum = $this->create_forum(['cutoffdate' => $now + 86400 , 'blockafter' => 5, 'blockperiod' => 86400]); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->prevent_capability('mod/forum:postwithoutthrottling'); $this->assertTrue($capabilitymanager->can_create_discussions($user)); $forum = $this->create_forum(['cutoffdate' => $now + 86400 , 'blockafter' => 1, 'blockperiod' => 86400]); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->prevent_capability('mod/forum:postwithoutthrottling'); $this->assertFalse($capabilitymanager->can_create_discussions($user)); } /** * Test can_access_all_groups. * * @covers ::can_access_all_groups */ public function test_can_access_all_groups(): void { $this->resetAfterTest(); $forum = $this->create_forum(); $user = $this->user; $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->prevent_capability('moodle/site:accessallgroups'); $this->assertFalse($capabilitymanager->can_access_all_groups($user)); $this->give_capability('moodle/site:accessallgroups'); $this->assertTrue($capabilitymanager->can_access_all_groups($user)); } /** * Test can_access_group. * * @covers ::can_access_group */ public function test_can_access_group(): void { $this->resetAfterTest(); $forum = $this->create_forum(); $user = $this->user; $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $group = $this->getDataGenerator()->create_group(['courseid' => $this->course->id]); $this->prevent_capability('moodle/site:accessallgroups'); $this->assertFalse($capabilitymanager->can_access_group($user, $group->id)); $this->give_capability('moodle/site:accessallgroups'); $this->assertTrue($capabilitymanager->can_access_group($user, $group->id)); $this->prevent_capability('moodle/site:accessallgroups'); $this->getDataGenerator()->create_group_member(['userid' => $user->id, 'groupid' => $group->id]); $this->assertTrue($capabilitymanager->can_access_group($user, $group->id)); } /** * Test can_view_discussions. * * @covers ::can_view_discussions */ public function test_can_view_discussions(): void { $this->resetAfterTest(); $forum = $this->create_forum(); $user = $this->user; $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->prevent_capability('mod/forum:viewdiscussion'); $this->assertFalse($capabilitymanager->can_view_discussions($user)); $this->give_capability('mod/forum:viewdiscussion'); $this->assertTrue($capabilitymanager->can_view_discussions($user)); } /** * Test can_move_discussions. * * @covers ::can_move_discussions */ public function test_can_move_discussions(): void { $this->resetAfterTest(); $forum = $this->create_forum(); $user = $this->user; $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->prevent_capability('mod/forum:movediscussions'); $this->assertFalse($capabilitymanager->can_move_discussions($user)); $this->give_capability('mod/forum:movediscussions'); $this->assertTrue($capabilitymanager->can_move_discussions($user)); $forum = $this->create_forum(['type' => 'single']); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->assertFalse($capabilitymanager->can_move_discussions($user)); } /** * Test can_pin_discussions. * * @covers ::can_pin_discussions */ public function test_can_pin_discussions(): void { $this->resetAfterTest(); $forum = $this->create_forum(); $user = $this->user; $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->prevent_capability('mod/forum:pindiscussions'); $this->assertFalse($capabilitymanager->can_pin_discussions($user)); $this->give_capability('mod/forum:pindiscussions'); $this->assertTrue($capabilitymanager->can_pin_discussions($user)); } /** * Test can_split_discussions. * * @covers ::can_split_discussions */ public function test_can_split_discussions(): void { $this->resetAfterTest(); $forum = $this->create_forum(); $user = $this->user; $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->prevent_capability('mod/forum:splitdiscussions'); $this->assertFalse($capabilitymanager->can_split_discussions($user)); $this->give_capability('mod/forum:splitdiscussions'); $this->assertTrue($capabilitymanager->can_split_discussions($user)); $forum = $this->create_forum(['type' => 'single']); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->assertFalse($capabilitymanager->can_split_discussions($user)); } /** * Test can_export_discussions. * * @covers ::can_export_discussions */ public function test_can_export_discussions(): void { global $CFG; $this->resetAfterTest(); $CFG->enableportfolios = true; $forum = $this->create_forum(); $user = $this->user; $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->prevent_capability('mod/forum:exportdiscussion'); $this->assertFalse($capabilitymanager->can_export_discussions($user)); $this->give_capability('mod/forum:exportdiscussion'); $this->assertTrue($capabilitymanager->can_export_discussions($user)); $CFG->enableportfolios = false; $this->assertFalse($capabilitymanager->can_export_discussions($user)); } /** * Test can_manually_control_post_read_status. * * @covers ::can_manually_control_post_read_status */ public function test_can_manually_control_post_read_status(): void { global $CFG, $DB; $this->resetAfterTest(); $CFG->forum_usermarksread = true; $forum = $this->create_forum(); $user = $this->user; $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $cache = \cache::make('mod_forum', 'forum_is_tracked'); $user->trackforums = true; $prefid = $DB->insert_record('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forum->get_id()]); $this->assertFalse($capabilitymanager->can_manually_control_post_read_status($user)); $cache->purge(); $DB->delete_records('forum_track_prefs', ['id' => $prefid]); $this->assertTrue($capabilitymanager->can_manually_control_post_read_status($user)); $cache->purge(); $CFG->forum_usermarksread = false; $this->assertFalse($capabilitymanager->can_manually_control_post_read_status($user)); } /** * Test must_post_before_viewing_discussion. * * @covers ::must_post_before_viewing_discussion */ public function test_must_post_before_viewing_discussion(): void { $this->resetAfterTest(); $forum = $this->create_forum(); $user = $this->user; $discussion = $this->discussion; $newuser = $this->getDataGenerator()->create_user(); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->getDataGenerator()->enrol_user($newuser->id, $this->course->id, 'teacher'); $this->assertFalse($capabilitymanager->must_post_before_viewing_discussion($newuser, $discussion)); $forum = $this->create_forum(['type' => 'qanda']); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->prevent_capability('mod/forum:viewqandawithoutposting'); $this->assertTrue($capabilitymanager->must_post_before_viewing_discussion($newuser, $discussion)); $this->give_capability('mod/forum:viewqandawithoutposting'); $this->assertFalse($capabilitymanager->must_post_before_viewing_discussion($newuser, $discussion)); $this->prevent_capability('mod/forum:viewqandawithoutposting'); // The pre-generated user has a pre-generated post in the disussion already. $this->assertFalse($capabilitymanager->must_post_before_viewing_discussion($user, $discussion)); } /** * Test can_subscribe_to_discussion. * * @covers ::can_subscribe_to_discussion */ public function test_can_subscribe_to_discussion(): void { $this->resetAfterTest(); $forum = $this->create_forum(); $discussion = $this->discussion; $guestuser = $this->getDataGenerator()->create_user(); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->assertFalse($capabilitymanager->can_subscribe_to_discussion($guestuser, $discussion)); $this->assertTrue($capabilitymanager->can_subscribe_to_discussion($this->user, $discussion)); } /** * Test can_move_discussion. * * @covers ::can_move_discussion */ public function test_can_move_discussion(): void { $this->resetAfterTest(); $forum = $this->create_forum(); $discussion = $this->discussion; $user = $this->user; $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->prevent_capability('mod/forum:movediscussions'); $this->assertFalse($capabilitymanager->can_move_discussion($user, $discussion)); $this->give_capability('mod/forum:movediscussions'); $this->assertTrue($capabilitymanager->can_move_discussion($user, $discussion)); $forum = $this->create_forum(['type' => 'single']); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->assertFalse($capabilitymanager->can_move_discussion($user, $discussion)); } /** * Test can_pin_discussion. * * @covers ::can_pin_discussion */ public function test_can_pin_discussion(): void { $this->resetAfterTest(); $forum = $this->create_forum(); $discussion = $this->discussion; $user = $this->user; $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->prevent_capability('mod/forum:pindiscussions'); $this->assertFalse($capabilitymanager->can_pin_discussion($user, $discussion)); $this->give_capability('mod/forum:pindiscussions'); $this->assertTrue($capabilitymanager->can_pin_discussion($user, $discussion)); } /** * Test can_post_in_discussion. * * @covers ::can_post_in_discussion */ public function test_can_post_in_discussion(): void { $this->resetAfterTest(); $discussion = $this->discussion; $user = $this->user; // Locked discussions. $lockedforum = $this->create_forum(['lockdiscussionafter' => 1]); $capabilitymanager = $this->managerfactory->get_capability_manager($lockedforum); $this->give_capability('mod/forum:canoverridediscussionlock'); $this->assertTrue($capabilitymanager->can_post_in_discussion($user, $discussion)); $this->prevent_capability('mod/forum:canoverridediscussionlock'); $this->assertFalse($capabilitymanager->can_post_in_discussion($user, $discussion)); // News forum. $newsforum = $this->create_forum(['type' => 'news']); $capabilitymanager = $this->managerfactory->get_capability_manager($newsforum); $this->give_capability('mod/forum:replynews'); $this->assertTrue($capabilitymanager->can_post_in_discussion($user, $discussion)); $this->prevent_capability('mod/forum:replynews'); $this->assertFalse($capabilitymanager->can_post_in_discussion($user, $discussion)); // General forum. $forum = $this->create_forum(); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->give_capability('mod/forum:replypost'); $this->assertTrue($capabilitymanager->can_post_in_discussion($user, $discussion)); $this->prevent_capability('mod/forum:replypost'); $this->assertFalse($capabilitymanager->can_post_in_discussion($user, $discussion)); // Forum in separate group mode. $forumrecord = $this->getDataGenerator()->create_module( 'forum', ['course' => $this->course->id, 'groupmode' => SEPARATEGROUPS] ); $coursemodule = get_coursemodule_from_instance('forum', $forumrecord->id); $context = \context_module::instance($coursemodule->id); $forum = $this->entityfactory->get_forum_from_stdClass( $forumrecord, $context, $coursemodule, $this->course ); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->give_capability('moodle/site:accessallgroups', $context); $this->assertTrue($capabilitymanager->can_post_in_discussion($user, $discussion)); $this->prevent_capability('moodle/site:accessallgroups', $context); $this->assertFalse($capabilitymanager->can_post_in_discussion($user, $discussion)); $group = $this->getDataGenerator()->create_group(['courseid' => $this->course->id]); $discussion = $this->entityfactory->get_discussion_from_stdClass( (object) array_merge((array) $this->discussionrecord, ['groupid' => $group->id]) ); $this->assertFalse($capabilitymanager->can_post_in_discussion($user, $discussion)); $this->getDataGenerator()->create_group_member(['userid' => $user->id, 'groupid' => $group->id]); $this->assertTrue($capabilitymanager->can_post_in_discussion($user, $discussion)); // Forum in visible group mode. $forumrecord = $this->getDataGenerator()->create_module( 'forum', ['course' => $this->course->id, 'groupmode' => VISIBLEGROUPS] ); $coursemodule = get_coursemodule_from_instance('forum', $forumrecord->id); $context = \context_module::instance($coursemodule->id); $forum = $this->entityfactory->get_forum_from_stdClass( $forumrecord, $context, $coursemodule, $this->course ); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->give_capability('moodle/site:accessallgroups', $context); $this->assertTrue($capabilitymanager->can_post_in_discussion($user, $discussion)); $this->prevent_capability('moodle/site:accessallgroups', $context); $this->assertTrue($capabilitymanager->can_post_in_discussion($user, $discussion)); $group = $this->getDataGenerator()->create_group(['courseid' => $this->course->id]); $discussion = $this->entityfactory->get_discussion_from_stdClass( (object) array_merge((array) $this->discussionrecord, ['groupid' => $group->id]) ); $this->assertFalse($capabilitymanager->can_post_in_discussion($user, $discussion)); $this->getDataGenerator()->create_group_member(['userid' => $user->id, 'groupid' => $group->id]); $this->assertTrue($capabilitymanager->can_post_in_discussion($user, $discussion)); $now = time(); $forum = $this->create_forum(['cutoffdate' => $now + 86400 , 'blockafter' => 5, 'blockperiod' => 86400]); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->prevent_capability('mod/forum:postwithoutthrottling'); $this->give_capability('mod/forum:replypost'); $this->assertTrue($capabilitymanager->can_post_in_discussion($user, $discussion)); $forum = $this->create_forum(['cutoffdate' => $now + 86400 , 'blockafter' => 1, 'blockperiod' => 86400]); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->prevent_capability('mod/forum:postwithoutthrottling'); $this->assertFalse($capabilitymanager->can_post_in_discussion($user, $discussion)); } /** * Test can_edit_post. * * @covers ::can_edit_post */ public function test_can_edit_post(): void { global $CFG; $this->resetAfterTest(); $forum = $this->create_forum(); $discussion = $this->discussion; // The generated post is created 100 seconds in the past. $post = $this->post; $user = $this->user; $otheruser = $this->getDataGenerator()->create_user(); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->prevent_capability('mod/forum:editanypost'); // 200 seconds to edit. $CFG->maxeditingtime = 200; $this->assertTrue($capabilitymanager->can_edit_post($user, $discussion, $post)); // Can not edit within editing time if $post->mailnow > 0 (selected). $CFG->maxeditingtime = 200; $post = $this->entityfactory->get_post_from_stdClass( (object) array_merge((array) $this->postrecord, ['mailnow' => 1]) ); $this->assertFalse($capabilitymanager->can_edit_post($user, $discussion, $post)); // Back to normal - mailnow not selected. $post = $this->entityfactory->get_post_from_stdClass( (object) array_merge((array) $this->postrecord, ['mailnow' => 0]) ); // 10 seconds to edit. No longer in editing time. $CFG->maxeditingtime = 10; $this->assertFalse($capabilitymanager->can_edit_post($user, $discussion, $post)); // Can edit outside of editing time with this capability. $this->give_capability('mod/forum:editanypost'); $this->assertTrue($capabilitymanager->can_edit_post($user, $discussion, $post)); $CFG->maxeditingtime = 200; $this->assertFalse($capabilitymanager->can_edit_post($otheruser, $discussion, $post)); $this->prevent_capability('mod/forum:editanypost'); // News forum. $forum = $this->create_forum(['type' => 'news']); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); // Discussion hasn't started yet. $discussion = $this->entityfactory->get_discussion_from_stdClass( (object) array_merge((array) $this->discussionrecord, ['timestart' => time() + 100]) ); $this->assertFalse($capabilitymanager->can_edit_post($user, $discussion, $post)); // Back to a discussion that has started. $discussion = $this->discussion; // Post is a reply. $post = $this->entityfactory->get_post_from_stdClass( (object) array_merge((array) $this->postrecord, ['parent' => 5]) ); $this->assertFalse($capabilitymanager->can_edit_post($user, $discussion, $post)); $post = $this->post; // Discussion has started and post isn't a reply so we can edit it. $this->assertTrue($capabilitymanager->can_edit_post($user, $discussion, $post)); // Single forum. $forum = $this->create_forum(['type' => 'single']); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); // Create a new post that definitely isn't the first post of the discussion. // Only the author, and a user with editanypost can edit it. $post = $this->entityfactory->get_post_from_stdClass( (object) array_merge((array) $this->postrecord, ['id' => $post->get_id() + 100]) ); $this->give_capability('mod/forum:editanypost'); $this->assertTrue($capabilitymanager->can_edit_post($user, $discussion, $post)); $this->assertFalse($capabilitymanager->can_edit_post($otheruser, $discussion, $post)); $post = $this->post; // Set the first post of the discussion to our post. $discussion = $this->entityfactory->get_discussion_from_stdClass( (object) array_merge((array) $this->discussionrecord, ['firstpost' => $post->get_id()]) ); $this->prevent_capability('moodle/course:manageactivities'); $this->assertFalse($capabilitymanager->can_edit_post($user, $discussion, $post)); $this->give_capability('moodle/course:manageactivities'); $this->assertTrue($capabilitymanager->can_edit_post($user, $discussion, $post)); } /** * Test can_delete_post. * * @covers ::can_delete_post */ public function test_can_delete_post(): void { global $CFG; $this->resetAfterTest(); // Single forum. $forum = $this->create_forum(['type' => 'single']); // The generated post is created 100 seconds in the past. $post = $this->post; $user = $this->user; $capabilitymanager = $this->managerfactory->get_capability_manager($forum); // Set the first post of the discussion to our post. $discussion = $this->entityfactory->get_discussion_from_stdClass( (object) array_merge((array) $this->discussionrecord, ['firstpost' => $post->get_id()]) ); // Can't delete the first post of a single discussion forum. $this->assertFalse($capabilitymanager->can_delete_post($user, $discussion, $post)); // Set the first post of the discussion to something else. $discussion = $this->entityfactory->get_discussion_from_stdClass( (object) array_merge((array) $this->discussionrecord, ['firstpost' => $post->get_id() - 1]) ); $this->assertTrue($capabilitymanager->can_delete_post($user, $discussion, $post)); // Back to a general forum. $forum = $this->create_forum(); $this->prevent_capability('mod/forum:deleteanypost'); $this->give_capability('mod/forum:deleteownpost'); // 200 second editing time to make sure our post is still within it. $CFG->maxeditingtime = 200; // Can not delete within editing time if $post->mailnow > 0 (selected). $post = $this->entityfactory->get_post_from_stdClass( (object) array_merge((array) $this->postrecord, ['mailnow' => 1]) ); $this->assertFalse($capabilitymanager->can_delete_post($user, $discussion, $post)); // Back to normal - mailnow not selected. $post = $this->entityfactory->get_post_from_stdClass( (object) array_merge((array) $this->postrecord, ['mailnow' => 0]) ); // Make the post owned by someone else. $post = $this->entityfactory->get_post_from_stdClass( (object) array_merge((array) $this->postrecord, ['userid' => $user->id - 1]) ); // Can't delete someone else's post. $this->assertFalse($capabilitymanager->can_delete_post($user, $discussion, $post)); // Back to our post. $post = $this->post; // Not in editing time. $CFG->maxeditingtime = 10; $this->assertFalse($capabilitymanager->can_delete_post($user, $discussion, $post)); $CFG->maxeditingtime = 200; // Remove the capability to delete own post. $this->prevent_capability('mod/forum:deleteownpost'); $this->assertFalse($capabilitymanager->can_delete_post($user, $discussion, $post)); $this->give_capability('mod/forum:deleteownpost'); $this->assertTrue($capabilitymanager->can_delete_post($user, $discussion, $post)); $this->give_capability('mod/forum:deleteanypost'); $CFG->maxeditingtime = 10; $this->assertTrue($capabilitymanager->can_delete_post($user, $discussion, $post)); } /** * Test can_split_post. * * @covers ::can_split_post */ public function test_can_split_post(): void { $this->resetAfterTest(); $forum = $this->create_forum(); $user = $this->user; $discussion = $this->discussion; $post = $this->post; $capabilitymanager = $this->managerfactory->get_capability_manager($forum); // Make the post a reply. $post = $this->entityfactory->get_post_from_stdClass( (object) array_merge((array) $this->postrecord, ['parent' => 5]) ); $this->prevent_capability('mod/forum:splitdiscussions'); $this->assertFalse($capabilitymanager->can_split_post($user, $discussion, $post)); $this->give_capability('mod/forum:splitdiscussions'); $this->assertTrue($capabilitymanager->can_split_post($user, $discussion, $post)); // Make the post have no parent. $post = $this->entityfactory->get_post_from_stdClass( (object) array_merge((array) $this->postrecord, ['parent' => 0]) ); $this->assertFalse($capabilitymanager->can_split_post($user, $discussion, $post)); $forum = $this->create_forum(['type' => 'single']); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); // Make the post a reply. $post = $this->entityfactory->get_post_from_stdClass( (object) array_merge((array) $this->postrecord, ['parent' => 5]) ); // Can't split a single discussion forum. $this->assertFalse($capabilitymanager->can_split_post($user, $discussion, $post)); // Make the post a private reply. $post = $this->entityfactory->get_post_from_stdClass( (object) array_merge((array) $this->postrecord, ['parent' => 5, 'privatereplyto' => $user->id]) ); // Can't split at a private reply. $this->assertFalse($capabilitymanager->can_split_post($user, $discussion, $post)); } /** * Test can_reply_to_post. * * @covers ::can_reply_to_post */ public function test_can_reply_to_post(): void { $this->resetAfterTest(); $discussion = $this->discussion; $user = $this->user; $post = $this->post; // Locked discussions. $lockedforum = $this->create_forum(['lockdiscussionafter' => 1]); $capabilitymanager = $this->managerfactory->get_capability_manager($lockedforum); $this->give_capability('mod/forum:canoverridediscussionlock'); $this->assertTrue($capabilitymanager->can_reply_to_post($user, $discussion, $post)); $this->prevent_capability('mod/forum:canoverridediscussionlock'); $this->assertFalse($capabilitymanager->can_reply_to_post($user, $discussion, $post)); // News forum. $newsforum = $this->create_forum(['type' => 'news']); $capabilitymanager = $this->managerfactory->get_capability_manager($newsforum); $this->give_capability('mod/forum:replynews'); $this->assertTrue($capabilitymanager->can_reply_to_post($user, $discussion, $post)); $this->prevent_capability('mod/forum:replynews'); $this->assertFalse($capabilitymanager->can_reply_to_post($user, $discussion, $post)); // General forum. $forum = $this->create_forum(); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->give_capability('mod/forum:replypost'); $this->assertTrue($capabilitymanager->can_reply_to_post($user, $discussion, $post)); $this->prevent_capability('mod/forum:replypost'); $this->assertFalse($capabilitymanager->can_reply_to_post($user, $discussion, $post)); // Forum in separate group mode. $forumrecord = $this->getDataGenerator()->create_module( 'forum', ['course' => $this->course->id, 'groupmode' => SEPARATEGROUPS] ); $coursemodule = get_coursemodule_from_instance('forum', $forumrecord->id); $context = \context_module::instance($coursemodule->id); $forum = $this->entityfactory->get_forum_from_stdClass( $forumrecord, $context, $coursemodule, $this->course ); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->give_capability('moodle/site:accessallgroups', $context); $this->assertTrue($capabilitymanager->can_reply_to_post($user, $discussion, $post)); $this->prevent_capability('moodle/site:accessallgroups', $context); $this->assertFalse($capabilitymanager->can_reply_to_post($user, $discussion, $post)); $group = $this->getDataGenerator()->create_group(['courseid' => $this->course->id]); $discussion = $this->entityfactory->get_discussion_from_stdClass( (object) array_merge((array) $this->discussionrecord, ['groupid' => $group->id]) ); $this->assertFalse($capabilitymanager->can_reply_to_post($user, $discussion, $post)); $this->getDataGenerator()->create_group_member(['userid' => $user->id, 'groupid' => $group->id]); $this->assertTrue($capabilitymanager->can_reply_to_post($user, $discussion, $post)); // Forum in visible group mode. $forumrecord = $this->getDataGenerator()->create_module( 'forum', ['course' => $this->course->id, 'groupmode' => VISIBLEGROUPS] ); $coursemodule = get_coursemodule_from_instance('forum', $forumrecord->id); $context = \context_module::instance($coursemodule->id); $forum = $this->entityfactory->get_forum_from_stdClass( $forumrecord, $context, $coursemodule, $this->course ); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->give_capability('moodle/site:accessallgroups', $context); $this->assertTrue($capabilitymanager->can_reply_to_post($user, $discussion, $post)); $this->prevent_capability('moodle/site:accessallgroups', $context); $this->assertTrue($capabilitymanager->can_reply_to_post($user, $discussion, $post)); $group = $this->getDataGenerator()->create_group(['courseid' => $this->course->id]); $discussion = $this->entityfactory->get_discussion_from_stdClass( (object) array_merge((array) $this->discussionrecord, ['groupid' => $group->id]) ); $this->assertFalse($capabilitymanager->can_reply_to_post($user, $discussion, $post)); $this->getDataGenerator()->create_group_member(['userid' => $user->id, 'groupid' => $group->id]); $this->assertTrue($capabilitymanager->can_reply_to_post($user, $discussion, $post)); // Make the post a private reply. $post = $this->entityfactory->get_post_from_stdClass( (object) array_merge((array) $this->postrecord, ['parent' => 5, 'privatereplyto' => $user->id]) ); // Can't reply to a a private reply. $this->assertFalse($capabilitymanager->can_reply_to_post($user, $discussion, $post)); } /** * Test for \mod_forum\local\managers\capability::can_reply_to_post() involving Q & A forums. */ public function test_can_reply_to_post_in_qanda_forum(): void { global $CFG; $this->resetAfterTest(); // Set max editing time to 10 seconds. $CFG->maxeditingtime = 10; $qandaforum = $this->create_forum(['type' => 'qanda']); $datagenerator = $this->getDataGenerator(); $capabilitymanager = $this->managerfactory->get_capability_manager($qandaforum); // Student 1. $student1 = $datagenerator->create_user(['firstname' => 'S1']); $datagenerator->enrol_user($student1->id, $this->course->id, 'student'); // Confirm Student 1 can reply to the question. $this->assertTrue($capabilitymanager->can_reply_to_post($student1, $this->discussion, $this->post)); // Student 2. $student2 = $datagenerator->create_user(['firstname' => 'S2']); $datagenerator->enrol_user($student2->id, $this->course->id, 'student'); // Confirm Student 2 can reply to the question. $this->assertTrue($capabilitymanager->can_reply_to_post($student2, $this->discussion, $this->post)); // Reply to the question as student 1. $now = time(); $options = ['parent' => $this->post->get_id(), 'created' => $now - 100]; $student1post = $this->helper_post_to_discussion($this->forumrecord, $this->discussionrecord, $student1, $options); $student1postentity = $this->entityfactory->get_post_from_stdClass($student1post); // Confirm Student 2 cannot reply student 1's answer yet. $this->assertFalse($capabilitymanager->can_reply_to_post($student2, $this->discussion, $student1postentity)); // Reply to the question as student 2. $this->helper_post_to_discussion($this->forumrecord, $this->discussionrecord, $student2, $options); // Reinitialise capability manager first to ensure we don't return cached values. $capabilitymanager = $this->managerfactory->get_capability_manager($qandaforum); // Confirm Student 2 can now reply to student 1's answer. $this->assertTrue($capabilitymanager->can_reply_to_post($student2, $this->discussion, $student1postentity)); } /** * Ensure that can_reply_privately_to_post works as expected. * * @covers ::can_reply_privately_to_post */ public function test_can_reply_privately_to_post(): void { $this->resetAfterTest(); $forum = $this->create_forum(); $discussion = $this->discussion; $user = $this->user; $post = $this->post; $capabilitymanager = $this->managerfactory->get_capability_manager($forum); // Without the capability, and with a standard post, it is not possible to reply privately. $this->prevent_capability('mod/forum:postprivatereply'); $this->assertFalse($capabilitymanager->can_reply_privately_to_post($this->user, $post)); // With the capability, and a standard post, it is possible to reply privately. $this->give_capability('mod/forum:postprivatereply'); $this->assertTrue($capabilitymanager->can_reply_privately_to_post($this->user, $post)); // Make the post a private reply. $post = $this->entityfactory->get_post_from_stdClass( (object) array_merge((array) $this->postrecord, ['parent' => 5, 'privatereplyto' => $user->id]) ); // Can't ever reply to a a private reply. $this->assertFalse($capabilitymanager->can_reply_privately_to_post($user, $post)); } /** * Ensure that can_view_post works as expected. * * @covers ::can_view_post */ public function test_can_view_post(): void { $this->resetAfterTest(); $forum = $this->create_forum(); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $user = $this->user; $otheruser = $this->getDataGenerator()->create_user(); $discussion = $this->discussion; $post = $this->post; $postproperties = ['parent' => $post->get_id(), 'userid' => $otheruser->id, 'privatereplyto' => $otheruser->id]; $privatepost = $this->entityfactory->get_post_from_stdClass( (object) array_merge((array) $this->postrecord, $postproperties) ); $this->prevent_capability('mod/forum:readprivatereplies'); $this->assertFalse($capabilitymanager->can_view_post($user, $discussion, $privatepost)); } /** * Ensure that can_view_post_shell considers private replies correctly. * * @covers ::can_view_post_shell */ public function test_can_view_post_shell(): void { $this->resetAfterTest(); $forum = $this->create_forum(); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $user = $this->user; $otheruser = $this->getDataGenerator()->create_user(); $discussion = $this->discussion; $post = $this->post; $postproperties = ['parent' => $post->get_id(), 'userid' => $user->id, 'privatereplyto' => $user->id]; $privatepostfrommetome = $this->entityfactory->get_post_from_stdClass( (object) array_merge((array) $this->postrecord, $postproperties) ); $postproperties = ['parent' => $post->get_id(), 'userid' => $user->id, 'privatereplyto' => $otheruser->id]; $privatepostfrommetoother = $this->entityfactory->get_post_from_stdClass( (object) array_merge((array) $this->postrecord, $postproperties) ); $postproperties = ['parent' => $post->get_id(), 'userid' => $otheruser->id, 'privatereplyto' => $user->id]; $privatepostfromothertome = $this->entityfactory->get_post_from_stdClass( (object) array_merge((array) $this->postrecord, $postproperties) ); $postproperties = ['parent' => $post->get_id(), 'userid' => $otheruser->id, 'privatereplyto' => $otheruser->id]; $privatepostfromothertoother = $this->entityfactory->get_post_from_stdClass( (object) array_merge((array) $this->postrecord, $postproperties) ); // Can always view public replies, and private replies by me or to me. $this->prevent_capability('mod/forum:readprivatereplies'); $this->assertTrue($capabilitymanager->can_view_post_shell($this->user, $post)); $this->assertTrue($capabilitymanager->can_view_post_shell($this->user, $privatepostfrommetome)); $this->assertTrue($capabilitymanager->can_view_post_shell($this->user, $privatepostfrommetoother)); $this->assertTrue($capabilitymanager->can_view_post_shell($this->user, $privatepostfromothertome)); $this->assertFalse($capabilitymanager->can_view_post_shell($this->user, $privatepostfromothertoother)); $this->give_capability('mod/forum:readprivatereplies'); $this->assertTrue($capabilitymanager->can_view_post_shell($this->user, $post)); $this->assertTrue($capabilitymanager->can_view_post_shell($this->user, $privatepostfrommetome)); $this->assertTrue($capabilitymanager->can_view_post_shell($this->user, $privatepostfrommetoother)); $this->assertTrue($capabilitymanager->can_view_post_shell($this->user, $privatepostfromothertome)); $this->assertTrue($capabilitymanager->can_view_post_shell($this->user, $privatepostfromothertoother)); } /** * Test can_export_post. * * @covers ::can_export_post */ public function test_can_export_post(): void { global $CFG; $this->resetAfterTest(); $forum = $this->create_forum(); $post = $this->post; $user = $this->user; $otheruser = $this->getDataGenerator()->create_user(); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->getDataGenerator()->enrol_user($otheruser->id, $this->course->id, 'teacher'); $CFG->enableportfolios = true; $this->give_capability('mod/forum:exportpost'); $this->assertTrue($capabilitymanager->can_export_post($otheruser, $post)); $CFG->enableportfolios = false; $this->assertFalse($capabilitymanager->can_export_post($otheruser, $post)); $CFG->enableportfolios = true; $this->prevent_capability('mod/forum:exportpost'); // Can't export another user's post without the exportpost capavility. $this->assertFalse($capabilitymanager->can_export_post($otheruser, $post)); $this->give_capability('mod/forum:exportownpost'); // Can export own post with the exportownpost capability. $this->assertTrue($capabilitymanager->can_export_post($user, $post)); $this->prevent_capability('mod/forum:exportownpost'); $this->assertFalse($capabilitymanager->can_export_post($user, $post)); } /** * Test can_view_participants. * * @covers ::can_view_participants */ public function test_can_view_participants(): void { $this->resetAfterTest(); $discussion = $this->discussion; $user = $this->user; $otheruser = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($otheruser->id, $this->course->id, 'teacher'); $this->prevent_capability('moodle/course:viewparticipants'); $this->prevent_capability('moodle/course:enrolreview'); $this->prevent_capability('mod/forum:viewqandawithoutposting'); $forum = $this->create_forum(); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->assertFalse($capabilitymanager->can_view_participants($otheruser, $discussion)); $this->give_capability('moodle/course:viewparticipants'); $this->assertTrue($capabilitymanager->can_view_participants($otheruser, $discussion)); $this->prevent_capability('moodle/course:viewparticipants'); $this->give_capability('moodle/course:enrolreview'); $this->assertTrue($capabilitymanager->can_view_participants($otheruser, $discussion)); $forum = $this->create_forum(['type' => 'qanda']); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); // Q and A forum requires the user to post before they can view it. $this->prevent_capability('mod/forum:viewqandawithoutposting'); $this->assertFalse($capabilitymanager->can_view_participants($otheruser, $discussion)); // This user has posted. $this->assertTrue($capabilitymanager->can_view_participants($user, $discussion)); } /** * Test can_view_hidden_posts. * * @covers ::can_view_hidden_posts */ public function test_can_view_hidden_posts(): void { $this->resetAfterTest(); $forum = $this->create_forum(); $user = $this->user; $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->prevent_capability('mod/forum:viewhiddentimedposts'); $this->assertFalse($capabilitymanager->can_view_hidden_posts($user)); $this->give_capability('mod/forum:viewhiddentimedposts'); $this->assertTrue($capabilitymanager->can_view_hidden_posts($user)); } /** * Test can_manage_forum. * * @covers ::can_manage_forum */ public function test_can_manage_forum(): void { $this->resetAfterTest(); $forum = $this->create_forum(); $user = $this->user; $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->prevent_capability('moodle/course:manageactivities'); $this->assertFalse($capabilitymanager->can_manage_forum($user)); $this->give_capability('moodle/course:manageactivities'); $this->assertTrue($capabilitymanager->can_manage_forum($user)); } /** * Test can_manage_tags. * * @covers ::can_manage_tags */ public function test_can_manage_tags(): void { global $DB; $this->resetAfterTest(); $forum = $this->create_forum(); $user = $this->user; $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $context = \context_system::instance(); $roleid = $DB->get_field('role', 'id', ['shortname' => 'user'], MUST_EXIST); assign_capability('moodle/tag:manage', CAP_PREVENT, $roleid, $context->id, true); $this->assertFalse($capabilitymanager->can_manage_tags($user)); assign_capability('moodle/tag:manage', CAP_ALLOW, $roleid, $context->id, true); $this->assertTrue($capabilitymanager->can_manage_tags($user)); } /** * Ensure that the can_view_any_private_reply works as expected. * * @covers ::can_view_any_private_reply */ public function test_can_view_any_private_reply(): void { $this->resetAfterTest(); $forum = $this->create_forum(); $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $this->give_capability('mod/forum:readprivatereplies'); $this->assertTrue($capabilitymanager->can_view_any_private_reply($this->user)); $this->prevent_capability('mod/forum:readprivatereplies'); $this->assertFalse($capabilitymanager->can_view_any_private_reply($this->user)); } /** * Test delete a post with ratings. */ public function test_validate_delete_post_with_ratings(): void { 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; $record->created = $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // Add rating. $post = $DB->get_record('forum_posts', array('discussion' => $discussion->id)); $post->totalscore = 80; $DB->update_record('forum_posts', $post); $vaultfactory = container::get_vault_factory(); $forumvault = $vaultfactory->get_forum_vault(); $discussionvault = $vaultfactory->get_discussion_vault(); $postvault = $vaultfactory->get_post_vault(); $postentity = $postvault->get_from_id($post->id); $discussionentity = $discussionvault->get_from_id($postentity->get_discussion_id()); $forumentity = $forumvault->get_from_id($discussionentity->get_forum_id()); $capabilitymanager = $this->managerfactory->get_capability_manager($forumentity); $this->setUser($user); $this->expectExceptionMessage(get_string('couldnotdeleteratings', 'rating')); $capabilitymanager->validate_delete_post($user, $discussionentity, $postentity, false); } /** * Test delete a post with replies. */ public function test_validate_delete_post_with_replies(): void { 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; $record->created = $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; $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $vaultfactory = container::get_vault_factory(); $forumvault = $vaultfactory->get_forum_vault(); $discussionvault = $vaultfactory->get_discussion_vault(); $postvault = $vaultfactory->get_post_vault(); $postentity = $postvault->get_from_id($parentpost->id); $discussionentity = $discussionvault->get_from_id($postentity->get_discussion_id()); $forumentity = $forumvault->get_from_id($discussionentity->get_forum_id()); $capabilitymanager = $this->managerfactory->get_capability_manager($forumentity); $this->setUser($user); // Get reply count. $replycount = $postvault->get_reply_count_for_post_id_in_discussion_id( $user, $postentity->get_id(), $discussionentity->get_id(), true); $this->expectExceptionMessage(get_string('couldnotdeletereplies', 'forum')); $capabilitymanager->validate_delete_post($user, $discussionentity, $postentity, $replycount); } } PK-B\bE"tests/backup/restore_date_test.phpnu[. namespace mod_forum\backup; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->libdir . "/phpunit/classes/restore_date_testcase.php"); require_once($CFG->dirroot . '/rating/lib.php'); /** * Restore date tests. * * @package mod_forum * @copyright 2017 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class restore_date_test extends \restore_date_testcase { /** * Test restore dates. */ public function test_restore_dates(): void { global $DB, $USER; $gg = $this->getDataGenerator()->get_plugin_generator('mod_forum'); $record = ['assesstimefinish' => 100, 'assesstimestart' => 100, 'ratingtime' => 1, 'assessed' => 2, 'scale' => 1]; list($course, $forum) = $this->create_course_and_module('forum', $record); // Forum Discussions/posts/ratings. $timestamp = 996699; $diff = $this->get_diff(); $record = new \stdClass(); $record->course = $course->id; $record->userid = $USER->id; $record->forum = $forum->id; $record->timestart = $record->timeend = $record->timemodified = $timestamp; $discussion = $gg->create_discussion($record); $record = new \stdClass(); $record->discussion = $discussion->id; $record->parent = $discussion->firstpost; $record->userid = $USER->id; $record->created = $record->modified = $timestamp; $post = $gg->create_post($record); // Time modified is changed internally. $DB->set_field('forum_discussions', 'timemodified', $timestamp); // Ratings. $ratingoptions = new \stdClass; $ratingoptions->context = \context_module::instance($forum->cmid); $ratingoptions->ratingarea = 'post'; $ratingoptions->component = 'mod_forum'; $ratingoptions->itemid = $post->id; $ratingoptions->scaleid = 2; $ratingoptions->userid = $USER->id; $rating = new \rating($ratingoptions); $rating->update_rating(2); $rating = $DB->get_record('rating', ['itemid' => $post->id]); // Do backup and restore. $newcourseid = $this->backup_and_restore($course); $newforum = $DB->get_record('forum', ['course' => $newcourseid]); $this->assertFieldsNotRolledForward($forum, $newforum, ['timemodified']); $props = ['assesstimefinish', 'assesstimestart']; $this->assertFieldsRolledForward($forum, $newforum, $props); $newdiscussion = $DB->get_record('forum_discussions', ['forum' => $newforum->id]); $newposts = $DB->get_records('forum_posts', ['discussion' => $newdiscussion->id]); $newcm = $DB->get_record('course_modules', ['course' => $newcourseid, 'instance' => $newforum->id]); // Forum discussion time checks. $this->assertEquals($timestamp + $diff, $newdiscussion->timestart); $this->assertEquals($timestamp + $diff, $newdiscussion->timeend); $this->assertEquals($timestamp, $newdiscussion->timemodified); // Posts test. foreach ($newposts as $post) { $this->assertEquals($timestamp, $post->created); $this->assertEquals($timestamp, $post->modified); } // Rating test. $newrating = $DB->get_record('rating', ['contextid' => \context_module::instance($newcm->id)->id]); $this->assertEquals($rating->timecreated, $newrating->timecreated); $this->assertEquals($rating->timemodified, $newrating->timemodified); } } PK-B\ d00tests/generator/lib.phpnu[. /** * Forum module data generator class * * @package mod_forum * @category test * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_forum_generator extends testing_module_generator { /** * @var int keep track of how many forum discussions have been created. */ protected $forumdiscussioncount = 0; /** * @var int keep track of how many forum posts have been created. */ protected $forumpostcount = 0; /** * @var int keep track of how many forum subscriptions have been created. */ protected $forumsubscriptionscount = 0; /** * Get the clock implementation to use when generating data. * * @return \core\clock */ protected function get_clock(): \core\clock { return \core\di::get(\core\clock::class); } /** * To be called from data reset code only, * do not use in tests. * @return void */ public function reset() { $this->forumdiscussioncount = 0; $this->forumpostcount = 0; $this->forumsubscriptionscount = 0; parent::reset(); } public function create_instance($record = null, ?array $options = null) { global $CFG; require_once($CFG->dirroot.'/mod/forum/lib.php'); $record = (object)(array)$record; if (!isset($record->type)) { $record->type = 'general'; } if (!isset($record->assessed)) { $record->assessed = 0; } if (!isset($record->scale)) { $record->scale = 0; } if (!isset($record->forcesubscribe)) { $record->forcesubscribe = FORUM_CHOOSESUBSCRIBE; } if (!isset($record->grade_forum)) { $record->grade_forum = 0; } return parent::create_instance($record, (array)$options); } /** * Function to create a dummy subscription. * * @param array|stdClass $record * @return stdClass the subscription object */ public function create_subscription($record = null) { global $DB; // Increment the forum subscription count. $this->forumsubscriptionscount++; $record = (array)$record; if (!isset($record['course'])) { throw new coding_exception('course must be present in phpunit_util::create_subscription() $record'); } if (!isset($record['forum'])) { throw new coding_exception('forum must be present in phpunit_util::create_subscription() $record'); } if (!isset($record['userid'])) { throw new coding_exception('userid must be present in phpunit_util::create_subscription() $record'); } $record = (object)$record; // Add the subscription. $record->id = $DB->insert_record('forum_subscriptions', $record); return $record; } /** * Function to create a dummy discussion. * * @param array|stdClass $record * @return stdClass the discussion object */ public function create_discussion($record = null) { global $DB; // Increment the forum discussion count. $this->forumdiscussioncount++; $record = (array) $record; if (!isset($record['course'])) { throw new coding_exception('course must be present in phpunit_util::create_discussion() $record'); } if (!isset($record['forum'])) { throw new coding_exception('forum must be present in phpunit_util::create_discussion() $record'); } if (!isset($record['userid'])) { throw new coding_exception('userid must be present in phpunit_util::create_discussion() $record'); } if (!isset($record['name'])) { $record['name'] = "Discussion " . $this->forumdiscussioncount; } if (!isset($record['subject'])) { $record['subject'] = "Subject for discussion " . $this->forumdiscussioncount; } if (!isset($record['message'])) { $record['message'] = html_writer::tag('p', 'Message for discussion ' . $this->forumdiscussioncount); } if (!isset($record['messageformat'])) { $record['messageformat'] = editors_get_preferred_format(); } if (!isset($record['messagetrust'])) { $record['messagetrust'] = ""; } if (!isset($record['assessed'])) { $record['assessed'] = '1'; } if (!isset($record['groupid'])) { $record['groupid'] = "-1"; } if (!isset($record['timestart'])) { $record['timestart'] = "0"; } if (!isset($record['timeend'])) { $record['timeend'] = "0"; } if (!isset($record['mailnow'])) { $record['mailnow'] = "0"; } if (!isset($record['timecreated'])) { $record['timecreated'] = $this->get_clock()->now()->getTimestamp(); } if (isset($record['timemodified'])) { $timemodified = $record['timemodified']; } else { $timemodified = $record['timecreated']; } if (!isset($record['pinned'])) { $record['pinned'] = FORUM_DISCUSSION_UNPINNED; } if (!isset($record['timelocked'])) { $record['timelocked'] = 0; } if (isset($record['mailed'])) { $mailed = $record['mailed']; } $record = (object) $record; // Add the discussion. $record->id = forum_add_discussion($record, null, null, $record->userid); $post = $DB->get_record('forum_posts', array('discussion' => $record->id)); if (isset($timemodified) || isset($mailed)) { if (isset($mailed)) { $post->mailed = $mailed; } if (isset($timemodified)) { // Enforce the time modified. $record->timemodified = $timemodified; $post->modified = $post->created = $timemodified; $DB->update_record('forum_discussions', $record); } $DB->update_record('forum_posts', $post); } if (property_exists($record, 'tags')) { $cm = get_coursemodule_from_instance('forum', $record->forum); $tags = is_array($record->tags) ? $record->tags : preg_split('/,/', $record->tags); core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, context_module::instance($cm->id), $tags); } return $record; } /** * Function to create a dummy post. * * @param array|stdClass $record * @return stdClass the post object */ public function create_post($record = null) { global $DB; // Increment the forum post count. $this->forumpostcount++; // Variable to store time. $time = time(); $record = (array) $record; if (!isset($record['discussion'])) { throw new coding_exception('discussion must be present in phpunit_util::create_post() $record'); } if (!isset($record['userid'])) { throw new coding_exception('userid must be present in phpunit_util::create_post() $record'); } if (!isset($record['parent'])) { $record['parent'] = 0; } if (!isset($record['subject'])) { $record['subject'] = 'Forum post subject ' . $this->forumpostcount; } if (!isset($record['message'])) { $record['message'] = html_writer::tag('p', 'Forum message post ' . $this->forumpostcount); } if (!isset($record['created'])) { // If we are using the system clock, then revert to the time + count approach. // Unfortunately a lot of Forum code relies on things not happening at the same time. // See MDL-80838 for more information on this issue. if ($this->get_clock() instanceof \core\system_clock) { $record['created'] = $time; } else { $record['created'] = $this->get_clock()->now()->getTimestamp(); } } if (!isset($record['modified'])) { $record['modified'] = $record['created']; } if (!isset($record['mailed'])) { $record['mailed'] = 0; } if (!isset($record['messageformat'])) { $record['messageformat'] = 0; } if (!isset($record['messagetrust'])) { $record['messagetrust'] = 0; } if (!isset($record['attachment'])) { $record['attachment'] = ""; } if (!isset($record['totalscore'])) { $record['totalscore'] = 0; } if (!isset($record['mailnow'])) { $record['mailnow'] = 0; } if (!isset($record['deleted'])) { $record['deleted'] = 0; } if (!isset($record['privatereplyto'])) { $record['privatereplyto'] = 0; } $record = (object) $record; \mod_forum\local\entities\post::add_message_counts($record); // Add the post. $record->id = $DB->insert_record('forum_posts', $record); if (property_exists($record, 'tags')) { $discussion = $DB->get_record('forum_discussions', ['id' => $record->discussion]); $cm = get_coursemodule_from_instance('forum', $discussion->forum); $tags = is_array($record->tags) ? $record->tags : preg_split('/,/', $record->tags); core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $record->id, context_module::instance($cm->id), $tags); } // Update the last post. forum_discussion_update_last_post($record->discussion); return $record; } public function create_content($instance, $record = array()) { global $USER, $DB; $record = (array)$record + array( 'forum' => $instance->id, 'userid' => $USER->id, 'course' => $instance->course ); if (empty($record['discussion']) && empty($record['parent'])) { // Create discussion. $discussion = $this->create_discussion($record); $post = $DB->get_record('forum_posts', array('id' => $discussion->firstpost)); } else { // Create post. if (empty($record['parent'])) { $record['parent'] = $DB->get_field('forum_discussions', 'firstpost', array('id' => $record['discussion']), MUST_EXIST); } else if (empty($record['discussion'])) { $record['discussion'] = $DB->get_field('forum_posts', 'discussion', array('id' => $record['parent']), MUST_EXIST); } $post = $this->create_post($record); } return $post; } /** * Extracted from exporter/post.php * * Get the HTML to display as a subheading in a post. * * @param stdClass $exportedauthor The exported author object * @param int $timecreated The post time created timestamp if it's to be displayed * @return string */ public function get_author_subheading_html(stdClass $exportedauthor, int $timecreated): string { $fullname = $exportedauthor->fullname; $profileurl = $exportedauthor->urls['profile'] ?? null; $name = $profileurl ? "{$fullname}" : $fullname; $date = userdate_htmltime($timecreated, get_string('strftimedaydatetime', 'core_langconfig')); return get_string('bynameondate', 'mod_forum', ['name' => $name, 'date' => $date]); } } PK-B\q  -tests/generator/behat_mod_forum_generator.phpnu[. /** * Behat data generator for mod_forum. * * @package mod_forum * @category test * @copyright 2021 Noel De Martin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_mod_forum_generator extends behat_generator_base { /** * Get a list of the entities that Behat can create using the generator step. * * @return array */ protected function get_creatable_entities(): array { return [ 'discussions' => [ 'singular' => 'discussion', 'datagenerator' => 'discussion', 'required' => ['forum'], 'switchids' => ['forum' => 'forumid', 'user' => 'userid', 'group' => 'groupid'], ], 'posts' => [ 'singular' => 'post', 'datagenerator' => 'post', 'required' => [], 'switchids' => ['forum' => 'forumid', 'user' => 'userid'], ], ]; } /** * Get the forum id using an activity idnumber or name. * * @param string $idnumberorname The forum activity idnumber or name. * @return int The forum id */ protected function get_forum_id(string $idnumberorname): int { return $this->get_cm_by_activity_name('forum', $idnumberorname)->instance; } /** * Gets the group id from it's idnumber. It allows using 'All participants' as idnumber. * * @throws Exception * @param string $idnumber * @return int */ protected function get_group_id($idnumber): int { if ($idnumber === 'All participants') { return -1; } return parent::get_group_id($idnumber); } /** * Preprocess discussion data. * * @param array $data Raw data. * @return array Processed data. */ protected function preprocess_discussion(array $data) { global $DB, $USER; $forum = $DB->get_record('forum', ['id' => $data['forumid']]); unset($data['course']); unset($data['forumid']); return array_merge([ 'course' => $forum->course, 'forum' => $forum->id, 'userid' => $USER->id, ], $data); } /** * Preprocess post data. * * @param array $data Raw data. * @return array Processed data. */ protected function preprocess_post(array $data) { global $DB, $USER; // Get discussion from name. $discussionfilters = array_filter([ 'name' => $data['discussion'] ?? null, 'forum' => $data['forumid'] ?? null, ]); if (!empty($discussionfilters)) { if (!$discussionid = $DB->get_field('forum_discussions', 'id', $discussionfilters)) { throw new Exception('The specified discussion with name "' . $data['name'] . '" could not be found.'); } $data['discussion'] = $discussionid; unset($data['forumid']); } // Get discussion from parent. $parentfilters = array_filter([ 'subject' => $data['parentsubject'] ?? null, ]); if (!empty($parentfilters)) { if (isset($discussionid)) { $parentfilters['discussion'] = $discussionid; } if (!$parent = $DB->get_record('forum_posts', $parentfilters)) { $parentdescription = implode(' and ', array_filter([ isset($parentfilters['subject']) ? 'subject "' . $parentfilters['subject'] . '"' : null, ])); throw new Exception('The specified post with ' . $parentdescription . ' could not be found.'); } $data['parent'] = $parent->id; $data['discussion'] = $parent->discussion; unset($data['parentsubject']); } // Return processed data. if (!isset($data['discussion'])) { throw new Exception('It was not possible to find a discussion to create a post, '. 'please specify discussion or parentsubject.'); } return array_merge(['userid' => $USER->id], $data); } } PK-B\   tests/subscriptions_test.phpnu[. namespace mod_forum; use mod_forum_tests_generator_trait; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once(__DIR__ . '/generator_trait.php'); require_once("{$CFG->dirroot}/mod/forum/lib.php"); /** * The module forums tests * * @package mod_forum * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class subscriptions_test extends \advanced_testcase { // Include the mod_forum test helpers. // This includes functions to create forums, users, discussions, and posts. use mod_forum_tests_generator_trait; /** * Test setUp. */ public function setUp(): void { global $DB; parent::setUp(); // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); \mod_forum\subscriptions::reset_discussion_cache(); } /** * Test tearDown. */ public function tearDown(): void { // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); \mod_forum\subscriptions::reset_discussion_cache(); parent::tearDown(); } /** * Test subscription modes modifications. * * @covers \mod_forum\event\subscription_mode_updated */ public function test_subscription_modes(): void { global $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id); $forum = $this->getDataGenerator()->create_module('forum', $options); $context = \context_module::instance($forum->cmid); // Create a user enrolled in the course as a student. list($user) = $this->helper_create_users($course, 1); // Must be logged in as the current user. $this->setUser($user); $sink = $this->redirectEvents(); // Capturing the event. \mod_forum\subscriptions::set_subscription_mode($forum, FORUM_FORCESUBSCRIBE); $forum = $DB->get_record('forum', array('id' => $forum->id)); $this->assertEquals(FORUM_FORCESUBSCRIBE, \mod_forum\subscriptions::get_subscription_mode($forum)); $this->assertTrue(\mod_forum\subscriptions::is_forcesubscribed($forum)); $this->assertFalse(\mod_forum\subscriptions::is_subscribable($forum)); $this->assertFalse(\mod_forum\subscriptions::subscription_disabled($forum)); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\mod_forum\event\subscription_mode_updated', $event); $this->assertEquals($context, $event->get_context()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); $sink = $this->redirectEvents(); // Capturing the event. \mod_forum\subscriptions::set_subscription_mode($forum, FORUM_DISALLOWSUBSCRIBE); $forum = $DB->get_record('forum', array('id' => $forum->id)); $this->assertEquals(FORUM_DISALLOWSUBSCRIBE, \mod_forum\subscriptions::get_subscription_mode($forum)); $this->assertTrue(\mod_forum\subscriptions::subscription_disabled($forum)); $this->assertFalse(\mod_forum\subscriptions::is_subscribable($forum)); $this->assertFalse(\mod_forum\subscriptions::is_forcesubscribed($forum)); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\mod_forum\event\subscription_mode_updated', $event); $this->assertEquals($context, $event->get_context()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); $sink = $this->redirectEvents(); // Capturing the event. \mod_forum\subscriptions::set_subscription_mode($forum, FORUM_INITIALSUBSCRIBE); $forum = $DB->get_record('forum', array('id' => $forum->id)); $this->assertEquals(FORUM_INITIALSUBSCRIBE, \mod_forum\subscriptions::get_subscription_mode($forum)); $this->assertTrue(\mod_forum\subscriptions::is_subscribable($forum)); $this->assertFalse(\mod_forum\subscriptions::subscription_disabled($forum)); $this->assertFalse(\mod_forum\subscriptions::is_forcesubscribed($forum)); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\mod_forum\event\subscription_mode_updated', $event); $this->assertEquals($context, $event->get_context()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); $sink = $this->redirectEvents(); // Capturing the event. \mod_forum\subscriptions::set_subscription_mode($forum, FORUM_CHOOSESUBSCRIBE); $forum = $DB->get_record('forum', array('id' => $forum->id)); $this->assertEquals(FORUM_CHOOSESUBSCRIBE, \mod_forum\subscriptions::get_subscription_mode($forum)); $this->assertTrue(\mod_forum\subscriptions::is_subscribable($forum)); $this->assertFalse(\mod_forum\subscriptions::subscription_disabled($forum)); $this->assertFalse(\mod_forum\subscriptions::is_forcesubscribed($forum)); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\mod_forum\event\subscription_mode_updated', $event); $this->assertEquals($context, $event->get_context()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Test fetching unsubscribable forums. */ public function test_unsubscribable_forums(): void { global $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); // Create a user enrolled in the course as a student. list($user) = $this->helper_create_users($course, 1); // Must be logged in as the current user. $this->setUser($user); // Without any subscriptions, there should be nothing returned. $result = \mod_forum\subscriptions::get_unsubscribable_forums(); $this->assertEquals(0, count($result)); // Create the forums. $options = array('course' => $course->id, 'forcesubscribe' => FORUM_FORCESUBSCRIBE); $forceforum = $this->getDataGenerator()->create_module('forum', $options); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_DISALLOWSUBSCRIBE); $disallowforum = $this->getDataGenerator()->create_module('forum', $options); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $chooseforum = $this->getDataGenerator()->create_module('forum', $options); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE); $initialforum = $this->getDataGenerator()->create_module('forum', $options); // At present the user is only subscribed to the initial forum. $result = \mod_forum\subscriptions::get_unsubscribable_forums(); $this->assertEquals(1, count($result)); // Ensure that the user is enrolled in all of the forums except force subscribed. \mod_forum\subscriptions::subscribe_user($user->id, $disallowforum); \mod_forum\subscriptions::subscribe_user($user->id, $chooseforum); $result = \mod_forum\subscriptions::get_unsubscribable_forums(); $this->assertEquals(3, count($result)); // Hide the forums. set_coursemodule_visible($forceforum->cmid, 0); set_coursemodule_visible($disallowforum->cmid, 0); set_coursemodule_visible($chooseforum->cmid, 0); set_coursemodule_visible($initialforum->cmid, 0); $result = \mod_forum\subscriptions::get_unsubscribable_forums(); $this->assertEquals(0, count($result)); // Add the moodle/course:viewhiddenactivities capability to the student user. $roleids = $DB->get_records_menu('role', null, '', 'shortname, id'); $context = \context_course::instance($course->id); assign_capability('moodle/course:viewhiddenactivities', CAP_ALLOW, $roleids['student'], $context); // All of the unsubscribable forums should now be listed. $result = \mod_forum\subscriptions::get_unsubscribable_forums(); $this->assertEquals(3, count($result)); } /** * Test that toggling the forum-level subscription for a different user does not affect their discussion-level * subscriptions. */ public function test_forum_subscribe_toggle_as_other(): void { global $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create a user enrolled in the course as a student. list($author) = $this->helper_create_users($course, 1); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); // Check that the user is currently not subscribed to the forum. $this->assertFalse(\mod_forum\subscriptions::is_subscribed($author->id, $forum)); // Check that the user is unsubscribed from the discussion too. $this->assertFalse(\mod_forum\subscriptions::is_subscribed($author->id, $forum, $discussion->id)); // Check that we have no records in either of the subscription tables. $this->assertEquals(0, $DB->count_records('forum_subscriptions', array( 'userid' => $author->id, 'forum' => $forum->id, ))); $this->assertEquals(0, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); // Subscribing to the forum should create a record in the subscriptions table, but not the forum discussion // subscriptions table. \mod_forum\subscriptions::subscribe_user($author->id, $forum); $this->assertEquals(1, $DB->count_records('forum_subscriptions', array( 'userid' => $author->id, 'forum' => $forum->id, ))); $this->assertEquals(0, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); // Unsubscribing should remove the record from the forum subscriptions table, and not modify the forum // discussion subscriptions table. \mod_forum\subscriptions::unsubscribe_user($author->id, $forum); $this->assertEquals(0, $DB->count_records('forum_subscriptions', array( 'userid' => $author->id, 'forum' => $forum->id, ))); $this->assertEquals(0, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); // Enroling the user in the discussion should add one record to the forum discussion table without modifying the // form subscriptions. \mod_forum\subscriptions::subscribe_user_to_discussion($author->id, $discussion); $this->assertEquals(0, $DB->count_records('forum_subscriptions', array( 'userid' => $author->id, 'forum' => $forum->id, ))); $this->assertEquals(1, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); // Unsubscribing should remove the record from the forum subscriptions table, and not modify the forum // discussion subscriptions table. \mod_forum\subscriptions::unsubscribe_user_from_discussion($author->id, $discussion); $this->assertEquals(0, $DB->count_records('forum_subscriptions', array( 'userid' => $author->id, 'forum' => $forum->id, ))); $this->assertEquals(0, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); // Re-subscribe to the discussion so that we can check the effect of forum-level subscriptions. \mod_forum\subscriptions::subscribe_user_to_discussion($author->id, $discussion); $this->assertEquals(0, $DB->count_records('forum_subscriptions', array( 'userid' => $author->id, 'forum' => $forum->id, ))); $this->assertEquals(1, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); // Subscribing to the forum should have no effect on the forum discussion subscriptions table if the user did // not request the change themself. \mod_forum\subscriptions::subscribe_user($author->id, $forum); $this->assertEquals(1, $DB->count_records('forum_subscriptions', array( 'userid' => $author->id, 'forum' => $forum->id, ))); $this->assertEquals(1, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); // Unsubscribing from the forum should have no effect on the forum discussion subscriptions table if the user // did not request the change themself. \mod_forum\subscriptions::unsubscribe_user($author->id, $forum); $this->assertEquals(0, $DB->count_records('forum_subscriptions', array( 'userid' => $author->id, 'forum' => $forum->id, ))); $this->assertEquals(1, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); // Subscribing to the forum should remove the per-discussion subscription preference if the user requested the // change themself. \mod_forum\subscriptions::subscribe_user($author->id, $forum, null, true); $this->assertEquals(1, $DB->count_records('forum_subscriptions', array( 'userid' => $author->id, 'forum' => $forum->id, ))); $this->assertEquals(0, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); // Now unsubscribe from the current discussion whilst being subscribed to the forum as a whole. \mod_forum\subscriptions::unsubscribe_user_from_discussion($author->id, $discussion); $this->assertEquals(1, $DB->count_records('forum_subscriptions', array( 'userid' => $author->id, 'forum' => $forum->id, ))); $this->assertEquals(1, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); // Unsubscribing from the forum should remove the per-discussion subscription preference if the user requested the // change themself. \mod_forum\subscriptions::unsubscribe_user($author->id, $forum, null, true); $this->assertEquals(0, $DB->count_records('forum_subscriptions', array( 'userid' => $author->id, 'forum' => $forum->id, ))); $this->assertEquals(0, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); // Subscribe to the discussion. \mod_forum\subscriptions::subscribe_user_to_discussion($author->id, $discussion); $this->assertEquals(0, $DB->count_records('forum_subscriptions', array( 'userid' => $author->id, 'forum' => $forum->id, ))); $this->assertEquals(1, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); // Subscribe to the forum without removing the discussion preferences. \mod_forum\subscriptions::subscribe_user($author->id, $forum); $this->assertEquals(1, $DB->count_records('forum_subscriptions', array( 'userid' => $author->id, 'forum' => $forum->id, ))); $this->assertEquals(1, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); // Unsubscribing from the discussion should result in a change. \mod_forum\subscriptions::unsubscribe_user_from_discussion($author->id, $discussion); $this->assertEquals(1, $DB->count_records('forum_subscriptions', array( 'userid' => $author->id, 'forum' => $forum->id, ))); $this->assertEquals(1, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); } /** * Test that a user unsubscribed from a forum is not subscribed to it's discussions by default. */ public function test_forum_discussion_subscription_forum_unsubscribed(): void { $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create users enrolled in the course as students. list($author) = $this->helper_create_users($course, 1); // Check that the user is currently not subscribed to the forum. $this->assertFalse(\mod_forum\subscriptions::is_subscribed($author->id, $forum)); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); // Check that the user is unsubscribed from the discussion too. $this->assertFalse(\mod_forum\subscriptions::is_subscribed($author->id, $forum, $discussion->id)); } /** * Test that the act of subscribing to a forum subscribes the user to it's discussions by default. */ public function test_forum_discussion_subscription_forum_subscribed(): void { $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create users enrolled in the course as students. list($author) = $this->helper_create_users($course, 1); // Enrol the user in the forum. // If a subscription was added, we get the record ID. $this->assertIsInt(\mod_forum\subscriptions::subscribe_user($author->id, $forum)); // If we already have a subscription when subscribing the user, we get a boolean (true). $this->assertTrue(\mod_forum\subscriptions::subscribe_user($author->id, $forum)); // Check that the user is currently subscribed to the forum. $this->assertTrue(\mod_forum\subscriptions::is_subscribed($author->id, $forum)); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); // Check that the user is subscribed to the discussion too. $this->assertTrue(\mod_forum\subscriptions::is_subscribed($author->id, $forum, $discussion->id)); } /** * Test that a user unsubscribed from a forum can be subscribed to a discussion. */ public function test_forum_discussion_subscription_forum_unsubscribed_discussion_subscribed(): void { $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create a user enrolled in the course as a student. list($author) = $this->helper_create_users($course, 1); // Check that the user is currently not subscribed to the forum. $this->assertFalse(\mod_forum\subscriptions::is_subscribed($author->id, $forum)); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); // Attempting to unsubscribe from the discussion should not make a change. $this->assertFalse(\mod_forum\subscriptions::unsubscribe_user_from_discussion($author->id, $discussion)); // Then subscribe them to the discussion. $this->assertTrue(\mod_forum\subscriptions::subscribe_user_to_discussion($author->id, $discussion)); // Check that the user is still unsubscribed from the forum. $this->assertFalse(\mod_forum\subscriptions::is_subscribed($author->id, $forum)); // But subscribed to the discussion. $this->assertTrue(\mod_forum\subscriptions::is_subscribed($author->id, $forum, $discussion->id)); } /** * Test that a user subscribed to a forum can be unsubscribed from a discussion. */ public function test_forum_discussion_subscription_forum_subscribed_discussion_unsubscribed(): void { $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create two users enrolled in the course as students. list($author) = $this->helper_create_users($course, 2); // Enrol the student in the forum. \mod_forum\subscriptions::subscribe_user($author->id, $forum); // Check that the user is currently subscribed to the forum. $this->assertTrue(\mod_forum\subscriptions::is_subscribed($author->id, $forum)); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); // Then unsubscribe them from the discussion. \mod_forum\subscriptions::unsubscribe_user_from_discussion($author->id, $discussion); // Check that the user is still subscribed to the forum. $this->assertTrue(\mod_forum\subscriptions::is_subscribed($author->id, $forum)); // But unsubscribed from the discussion. $this->assertFalse(\mod_forum\subscriptions::is_subscribed($author->id, $forum, $discussion->id)); } /** * Test the effect of toggling the discussion subscription status when subscribed to the forum. */ public function test_forum_discussion_toggle_forum_subscribed(): void { global $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create two users enrolled in the course as students. list($author) = $this->helper_create_users($course, 2); // Enrol the student in the forum. \mod_forum\subscriptions::subscribe_user($author->id, $forum); // Check that the user is currently subscribed to the forum. $this->assertTrue(\mod_forum\subscriptions::is_subscribed($author->id, $forum)); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); // Check that the user is initially subscribed to that discussion. $this->assertTrue(\mod_forum\subscriptions::is_subscribed($author->id, $forum, $discussion->id)); // An attempt to subscribe again should result in a falsey return to indicate that no change was made. $this->assertFalse(\mod_forum\subscriptions::subscribe_user_to_discussion($author->id, $discussion)); // And there should be no discussion subscriptions (and one forum subscription). $this->assertEquals(0, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); $this->assertEquals(1, $DB->count_records('forum_subscriptions', array( 'userid' => $author->id, 'forum' => $forum->id, ))); // Then unsubscribe them from the discussion. \mod_forum\subscriptions::unsubscribe_user_from_discussion($author->id, $discussion); // Check that the user is still subscribed to the forum. $this->assertTrue(\mod_forum\subscriptions::is_subscribed($author->id, $forum)); // An attempt to unsubscribe again should result in a falsey return to indicate that no change was made. $this->assertFalse(\mod_forum\subscriptions::unsubscribe_user_from_discussion($author->id, $discussion)); // And there should be a discussion subscriptions (and one forum subscription). $this->assertEquals(1, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); $this->assertEquals(1, $DB->count_records('forum_subscriptions', array( 'userid' => $author->id, 'forum' => $forum->id, ))); // But unsubscribed from the discussion. $this->assertFalse(\mod_forum\subscriptions::is_subscribed($author->id, $forum, $discussion->id)); // There should be a record in the discussion subscription tracking table. $this->assertEquals(1, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); // And one in the forum subscription tracking table. $this->assertEquals(1, $DB->count_records('forum_subscriptions', array( 'userid' => $author->id, 'forum' => $forum->id, ))); // Now subscribe the user again to the discussion. \mod_forum\subscriptions::subscribe_user_to_discussion($author->id, $discussion); // Check that the user is still subscribed to the forum. $this->assertTrue(\mod_forum\subscriptions::is_subscribed($author->id, $forum)); // And is subscribed to the discussion again. $this->assertTrue(\mod_forum\subscriptions::is_subscribed($author->id, $forum, $discussion->id)); // There should be no record in the discussion subscription tracking table. $this->assertEquals(0, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); // And one in the forum subscription tracking table. $this->assertEquals(1, $DB->count_records('forum_subscriptions', array( 'userid' => $author->id, 'forum' => $forum->id, ))); // And unsubscribe again. \mod_forum\subscriptions::unsubscribe_user_from_discussion($author->id, $discussion); // Check that the user is still subscribed to the forum. $this->assertTrue(\mod_forum\subscriptions::is_subscribed($author->id, $forum)); // But unsubscribed from the discussion. $this->assertFalse(\mod_forum\subscriptions::is_subscribed($author->id, $forum, $discussion->id)); // There should be a record in the discussion subscription tracking table. $this->assertEquals(1, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); // And one in the forum subscription tracking table. $this->assertEquals(1, $DB->count_records('forum_subscriptions', array( 'userid' => $author->id, 'forum' => $forum->id, ))); // And subscribe the user again to the discussion. \mod_forum\subscriptions::subscribe_user_to_discussion($author->id, $discussion); // Check that the user is still subscribed to the forum. $this->assertTrue(\mod_forum\subscriptions::is_subscribed($author->id, $forum)); $this->assertTrue(\mod_forum\subscriptions::is_subscribed($author->id, $forum)); // And is subscribed to the discussion again. $this->assertTrue(\mod_forum\subscriptions::is_subscribed($author->id, $forum, $discussion->id)); // There should be no record in the discussion subscription tracking table. $this->assertEquals(0, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); // And one in the forum subscription tracking table. $this->assertEquals(1, $DB->count_records('forum_subscriptions', array( 'userid' => $author->id, 'forum' => $forum->id, ))); // And unsubscribe again. \mod_forum\subscriptions::unsubscribe_user_from_discussion($author->id, $discussion); // Check that the user is still subscribed to the forum. $this->assertTrue(\mod_forum\subscriptions::is_subscribed($author->id, $forum)); // But unsubscribed from the discussion. $this->assertFalse(\mod_forum\subscriptions::is_subscribed($author->id, $forum, $discussion->id)); // There should be a record in the discussion subscription tracking table. $this->assertEquals(1, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); // And one in the forum subscription tracking table. $this->assertEquals(1, $DB->count_records('forum_subscriptions', array( 'userid' => $author->id, 'forum' => $forum->id, ))); // Now unsubscribe the user from the forum. $this->assertTrue(\mod_forum\subscriptions::unsubscribe_user($author->id, $forum, null, true)); // This removes both the forum_subscriptions, and the forum_discussion_subs records. $this->assertEquals(0, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); $this->assertEquals(0, $DB->count_records('forum_subscriptions', array( 'userid' => $author->id, 'forum' => $forum->id, ))); // And should have reset the discussion cache value. $result = \mod_forum\subscriptions::fetch_discussion_subscription($forum->id, $author->id); $this->assertIsArray($result); $this->assertFalse(isset($result[$discussion->id])); } /** * Test the effect of toggling the discussion subscription status when unsubscribed from the forum. */ public function test_forum_discussion_toggle_forum_unsubscribed(): void { global $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create two users enrolled in the course as students. list($author) = $this->helper_create_users($course, 2); // Check that the user is currently unsubscribed to the forum. $this->assertFalse(\mod_forum\subscriptions::is_subscribed($author->id, $forum)); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); // Check that the user is initially unsubscribed to that discussion. $this->assertFalse(\mod_forum\subscriptions::is_subscribed($author->id, $forum, $discussion->id)); // Then subscribe them to the discussion. $this->assertTrue(\mod_forum\subscriptions::subscribe_user_to_discussion($author->id, $discussion)); // An attempt to subscribe again should result in a falsey return to indicate that no change was made. $this->assertFalse(\mod_forum\subscriptions::subscribe_user_to_discussion($author->id, $discussion)); // Check that the user is still unsubscribed from the forum. $this->assertFalse(\mod_forum\subscriptions::is_subscribed($author->id, $forum)); // But subscribed to the discussion. $this->assertTrue(\mod_forum\subscriptions::is_subscribed($author->id, $forum, $discussion->id)); // There should be a record in the discussion subscription tracking table. $this->assertEquals(1, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); // Now unsubscribe the user again from the discussion. \mod_forum\subscriptions::unsubscribe_user_from_discussion($author->id, $discussion); // Check that the user is still unsubscribed from the forum. $this->assertFalse(\mod_forum\subscriptions::is_subscribed($author->id, $forum)); // And is unsubscribed from the discussion again. $this->assertFalse(\mod_forum\subscriptions::is_subscribed($author->id, $forum, $discussion->id)); // There should be no record in the discussion subscription tracking table. $this->assertEquals(0, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); // And subscribe the user again to the discussion. \mod_forum\subscriptions::subscribe_user_to_discussion($author->id, $discussion); // Check that the user is still unsubscribed from the forum. $this->assertFalse(\mod_forum\subscriptions::is_subscribed($author->id, $forum)); // And is subscribed to the discussion again. $this->assertTrue(\mod_forum\subscriptions::is_subscribed($author->id, $forum, $discussion->id)); // There should be a record in the discussion subscription tracking table. $this->assertEquals(1, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); // And unsubscribe again. \mod_forum\subscriptions::unsubscribe_user_from_discussion($author->id, $discussion); // Check that the user is still unsubscribed from the forum. $this->assertFalse(\mod_forum\subscriptions::is_subscribed($author->id, $forum)); // But unsubscribed from the discussion. $this->assertFalse(\mod_forum\subscriptions::is_subscribed($author->id, $forum, $discussion->id)); // There should be no record in the discussion subscription tracking table. $this->assertEquals(0, $DB->count_records('forum_discussion_subs', array( 'userid' => $author->id, 'discussion' => $discussion->id, ))); } /** * Test that the correct users are returned when fetching subscribed users from a forum where users can choose to * subscribe and unsubscribe. */ public function test_fetch_subscribed_users_subscriptions(): void { global $DB, $CFG; $this->resetAfterTest(true); // Create a course, with a forum. where users are initially subscribed. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create some user enrolled in the course as a student. $usercount = 5; $users = $this->helper_create_users($course, $usercount); // All users should be subscribed. $subscribers = \mod_forum\subscriptions::fetch_subscribed_users($forum); $this->assertEquals($usercount, count($subscribers)); // Subscribe the guest user too to the forum - they should never be returned by this function. $this->getDataGenerator()->enrol_user($CFG->siteguest, $course->id); $subscribers = \mod_forum\subscriptions::fetch_subscribed_users($forum); $this->assertEquals($usercount, count($subscribers)); // Unsubscribe 2 users. $unsubscribedcount = 2; for ($i = 0; $i < $unsubscribedcount; $i++) { \mod_forum\subscriptions::unsubscribe_user($users[$i]->id, $forum); } // The subscription count should now take into account those users who have been unsubscribed. $subscribers = \mod_forum\subscriptions::fetch_subscribed_users($forum); $this->assertEquals($usercount - $unsubscribedcount, count($subscribers)); } /** * Test that the correct users are returned hwen fetching subscribed users from a forum where users are forcibly * subscribed. */ public function test_fetch_subscribed_users_forced(): void { global $DB; $this->resetAfterTest(true); // Create a course, with a forum. where users are initially subscribed. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_FORCESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create some user enrolled in the course as a student. $usercount = 5; $users = $this->helper_create_users($course, $usercount); // All users should be subscribed. $subscribers = \mod_forum\subscriptions::fetch_subscribed_users($forum); $this->assertEquals($usercount, count($subscribers)); } /** * Test that unusual combinations of discussion subscriptions do not affect the subscribed user list. */ public function test_fetch_subscribed_users_discussion_subscriptions(): void { global $DB; $this->resetAfterTest(true); // Create a course, with a forum. where users are initially subscribed. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create some user enrolled in the course as a student. $usercount = 5; $users = $this->helper_create_users($course, $usercount); list($discussion, $post) = $this->helper_post_to_forum($forum, $users[0]); // All users should be subscribed. $subscribers = \mod_forum\subscriptions::fetch_subscribed_users($forum); $this->assertEquals($usercount, count($subscribers)); $subscribers = \mod_forum\subscriptions::fetch_subscribed_users($forum, 0, null, null, true); $this->assertEquals($usercount, count($subscribers)); \mod_forum\subscriptions::unsubscribe_user_from_discussion($users[0]->id, $discussion); // All users should be subscribed. $subscribers = \mod_forum\subscriptions::fetch_subscribed_users($forum); $this->assertEquals($usercount, count($subscribers)); // All users should be subscribed. $subscribers = \mod_forum\subscriptions::fetch_subscribed_users($forum, 0, null, null, true); $this->assertEquals($usercount, count($subscribers)); // Manually insert an extra subscription for one of the users. $record = new \stdClass(); $record->userid = $users[2]->id; $record->forum = $forum->id; $record->discussion = $discussion->id; $record->preference = time(); $DB->insert_record('forum_discussion_subs', $record); // The discussion count should not have changed. $subscribers = \mod_forum\subscriptions::fetch_subscribed_users($forum); $this->assertEquals($usercount, count($subscribers)); $subscribers = \mod_forum\subscriptions::fetch_subscribed_users($forum, 0, null, null, true); $this->assertEquals($usercount, count($subscribers)); // Unsubscribe 2 users. $unsubscribedcount = 2; for ($i = 0; $i < $unsubscribedcount; $i++) { \mod_forum\subscriptions::unsubscribe_user($users[$i]->id, $forum); } // The subscription count should now take into account those users who have been unsubscribed. $subscribers = \mod_forum\subscriptions::fetch_subscribed_users($forum); $this->assertEquals($usercount - $unsubscribedcount, count($subscribers)); $subscribers = \mod_forum\subscriptions::fetch_subscribed_users($forum, 0, null, null, true); $this->assertEquals($usercount - $unsubscribedcount, count($subscribers)); // Now subscribe one of those users back to the discussion. $subscribeddiscussionusers = 1; for ($i = 0; $i < $subscribeddiscussionusers; $i++) { \mod_forum\subscriptions::subscribe_user_to_discussion($users[$i]->id, $discussion); } $subscribers = \mod_forum\subscriptions::fetch_subscribed_users($forum); $this->assertEquals($usercount - $unsubscribedcount, count($subscribers)); $subscribers = \mod_forum\subscriptions::fetch_subscribed_users($forum, 0, null, null, true); $this->assertEquals($usercount - $unsubscribedcount + $subscribeddiscussionusers, count($subscribers)); } /** * Test whether a user is force-subscribed to a forum. */ public function test_force_subscribed_to_forum(): void { global $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_FORCESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create a user enrolled in the course as a student. $roleids = $DB->get_records_menu('role', null, '', 'shortname, id'); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleids['student']); // Check that the user is currently subscribed to the forum. $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum)); // Remove the allowforcesubscribe capability from the user. $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); assign_capability('mod/forum:allowforcesubscribe', CAP_PROHIBIT, $roleids['student'], $context); $this->assertFalse(has_capability('mod/forum:allowforcesubscribe', $context, $user->id)); // Check that the user is no longer subscribed to the forum. $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum)); } /** * Test that the subscription cache can be pre-filled. */ public function test_subscription_cache_prefill(): void { global $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create some users. $users = $this->helper_create_users($course, 20); // Reset the subscription cache. \mod_forum\subscriptions::reset_forum_cache(); // Filling the subscription cache should use a query. $startcount = $DB->perf_get_reads(); $this->assertNull(\mod_forum\subscriptions::fill_subscription_cache($forum->id)); $postfillcount = $DB->perf_get_reads(); $this->assertNotEquals($postfillcount, $startcount); // Now fetch some subscriptions from that forum - these should use // the cache and not perform additional queries. foreach ($users as $user) { $this->assertTrue(\mod_forum\subscriptions::fetch_subscription_cache($forum->id, $user->id)); } $finalcount = $DB->perf_get_reads(); $this->assertEquals(0, $finalcount - $postfillcount); } /** * Test that the subscription cache can filled user-at-a-time. */ public function test_subscription_cache_fill(): void { global $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create some users. $users = $this->helper_create_users($course, 20); // Reset the subscription cache. \mod_forum\subscriptions::reset_forum_cache(); // Filling the subscription cache should only use a single query. $startcount = $DB->perf_get_reads(); // Fetch some subscriptions from that forum - these should not use the cache and will perform additional queries. foreach ($users as $user) { $this->assertTrue(\mod_forum\subscriptions::fetch_subscription_cache($forum->id, $user->id)); } $finalcount = $DB->perf_get_reads(); $this->assertEquals(20, $finalcount - $startcount); } /** * Test that the discussion subscription cache can filled course-at-a-time. */ public function test_discussion_subscription_cache_fill_for_course(): void { global $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); // Create the forums. $options = array('course' => $course->id, 'forcesubscribe' => FORUM_DISALLOWSUBSCRIBE); $disallowforum = $this->getDataGenerator()->create_module('forum', $options); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $chooseforum = $this->getDataGenerator()->create_module('forum', $options); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE); $initialforum = $this->getDataGenerator()->create_module('forum', $options); // Create some users and keep a reference to the first user. $users = $this->helper_create_users($course, 20); $user = reset($users); // Reset the subscription caches. \mod_forum\subscriptions::reset_forum_cache(); $startcount = $DB->perf_get_reads(); $result = \mod_forum\subscriptions::fill_subscription_cache_for_course($course->id, $user->id); $this->assertNull($result); $postfillcount = $DB->perf_get_reads(); $this->assertNotEquals($postfillcount, $startcount); $this->assertFalse(\mod_forum\subscriptions::fetch_subscription_cache($disallowforum->id, $user->id)); $this->assertFalse(\mod_forum\subscriptions::fetch_subscription_cache($chooseforum->id, $user->id)); $this->assertTrue(\mod_forum\subscriptions::fetch_subscription_cache($initialforum->id, $user->id)); $finalcount = $DB->perf_get_reads(); $this->assertEquals(0, $finalcount - $postfillcount); // Test for all users. foreach ($users as $user) { $result = \mod_forum\subscriptions::fill_subscription_cache_for_course($course->id, $user->id); $this->assertFalse(\mod_forum\subscriptions::fetch_subscription_cache($disallowforum->id, $user->id)); $this->assertFalse(\mod_forum\subscriptions::fetch_subscription_cache($chooseforum->id, $user->id)); $this->assertTrue(\mod_forum\subscriptions::fetch_subscription_cache($initialforum->id, $user->id)); } $finalcount = $DB->perf_get_reads(); $this->assertNotEquals($finalcount, $postfillcount); } /** * Test that the discussion subscription cache can be forcibly updated for a user. */ public function test_discussion_subscription_cache_prefill(): void { global $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_FORCESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create some users. $users = $this->helper_create_users($course, 20); // Post some discussions to the forum. $discussions = array(); $author = $users[0]; $userwithnosubs = $users[1]; for ($i = 0; $i < 20; $i++) { list($discussion, $post) = $this->helper_post_to_forum($forum, $author); $discussions[] = $discussion; } // Unsubscribe half the users from the half the discussions. $forumcount = 0; $usercount = 0; $userwithsubs = null; foreach ($discussions as $data) { // Unsubscribe user from all discussions. \mod_forum\subscriptions::unsubscribe_user_from_discussion($userwithnosubs->id, $data); if ($forumcount % 2) { continue; } foreach ($users as $user) { if ($usercount % 2) { $userwithsubs = $user; continue; } \mod_forum\subscriptions::unsubscribe_user_from_discussion($user->id, $data); $usercount++; } $forumcount++; } // Reset the subscription caches. \mod_forum\subscriptions::reset_forum_cache(); \mod_forum\subscriptions::reset_discussion_cache(); // A user with no subscriptions should only be fetched once. $this->assertNull(\mod_forum\subscriptions::fill_discussion_subscription_cache($forum->id, $userwithnosubs->id)); $startcount = $DB->perf_get_reads(); $this->assertNull(\mod_forum\subscriptions::fill_discussion_subscription_cache($forum->id, $userwithnosubs->id)); $this->assertEquals($startcount, $DB->perf_get_reads()); // Confirm subsequent calls properly tries to fetch subs. $this->assertNull(\mod_forum\subscriptions::fill_discussion_subscription_cache($forum->id, $userwithsubs->id)); $this->assertNotEquals($startcount, $DB->perf_get_reads()); // Another read should be performed to get all subscriptions for the forum. $startcount = $DB->perf_get_reads(); $this->assertNull(\mod_forum\subscriptions::fill_discussion_subscription_cache($forum->id)); $this->assertNotEquals($startcount, $DB->perf_get_reads()); // Reset the subscription caches. \mod_forum\subscriptions::reset_forum_cache(); \mod_forum\subscriptions::reset_discussion_cache(); // Filling the discussion subscription cache should only use a single query. $startcount = $DB->perf_get_reads(); $this->assertNull(\mod_forum\subscriptions::fill_discussion_subscription_cache($forum->id)); $postfillcount = $DB->perf_get_reads(); $this->assertNotEquals($postfillcount, $startcount); // Now fetch some subscriptions from that forum - these should use // the cache and not perform additional queries. foreach ($users as $user) { $result = \mod_forum\subscriptions::fetch_discussion_subscription($forum->id, $user->id); $this->assertIsArray($result); } $finalcount = $DB->perf_get_reads(); $this->assertEquals(0, $finalcount - $postfillcount); } /** * Test that the discussion subscription cache can filled user-at-a-time. */ public function test_discussion_subscription_cache_fill(): void { global $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create some users. $users = $this->helper_create_users($course, 20); // Post some discussions to the forum. $discussions = array(); $author = $users[0]; for ($i = 0; $i < 20; $i++) { list($discussion, $post) = $this->helper_post_to_forum($forum, $author); $discussions[] = $discussion; } // Unsubscribe half the users from the half the discussions. $forumcount = 0; $usercount = 0; foreach ($discussions as $data) { if ($forumcount % 2) { continue; } foreach ($users as $user) { if ($usercount % 2) { continue; } \mod_forum\subscriptions::unsubscribe_user_from_discussion($user->id, $discussion); $usercount++; } $forumcount++; } // Reset the subscription caches. \mod_forum\subscriptions::reset_forum_cache(); \mod_forum\subscriptions::reset_discussion_cache(); $startcount = $DB->perf_get_reads(); // Now fetch some subscriptions from that forum - these should use // the cache and not perform additional queries. foreach ($users as $user) { $result = \mod_forum\subscriptions::fetch_discussion_subscription($forum->id, $user->id); $this->assertIsArray($result); } $finalcount = $DB->perf_get_reads(); $this->assertNotEquals($finalcount, $startcount); } /** * Test that after toggling the forum subscription as another user, * the discussion subscription functionality works as expected. */ public function test_forum_subscribe_toggle_as_other_repeat_subscriptions(): void { global $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create a user enrolled in the course as a student. list($user) = $this->helper_create_users($course, 1); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $user); // Confirm that the user is currently not subscribed to the forum. $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum)); // Confirm that the user is unsubscribed from the discussion too. $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion->id)); // Confirm that we have no records in either of the subscription tables. $this->assertEquals(0, $DB->count_records('forum_subscriptions', array( 'userid' => $user->id, 'forum' => $forum->id, ))); $this->assertEquals(0, $DB->count_records('forum_discussion_subs', array( 'userid' => $user->id, 'discussion' => $discussion->id, ))); // Subscribing to the forum should create a record in the subscriptions table, but not the forum discussion // subscriptions table. \mod_forum\subscriptions::subscribe_user($user->id, $forum); $this->assertEquals(1, $DB->count_records('forum_subscriptions', array( 'userid' => $user->id, 'forum' => $forum->id, ))); $this->assertEquals(0, $DB->count_records('forum_discussion_subs', array( 'userid' => $user->id, 'discussion' => $discussion->id, ))); // Now unsubscribe from the discussion. This should return true. $this->assertTrue(\mod_forum\subscriptions::unsubscribe_user_from_discussion($user->id, $discussion)); // Attempting to unsubscribe again should return false because no change was made. $this->assertFalse(\mod_forum\subscriptions::unsubscribe_user_from_discussion($user->id, $discussion)); // Subscribing to the discussion again should return truthfully as the subscription preference was removed. $this->assertTrue(\mod_forum\subscriptions::subscribe_user_to_discussion($user->id, $discussion)); // Attempting to subscribe again should return false because no change was made. $this->assertFalse(\mod_forum\subscriptions::subscribe_user_to_discussion($user->id, $discussion)); // Now unsubscribe from the discussion. This should return true once more. $this->assertTrue(\mod_forum\subscriptions::unsubscribe_user_from_discussion($user->id, $discussion)); // And unsubscribing from the forum but not as a request from the user should maintain their preference. \mod_forum\subscriptions::unsubscribe_user($user->id, $forum); $this->assertEquals(0, $DB->count_records('forum_subscriptions', array( 'userid' => $user->id, 'forum' => $forum->id, ))); $this->assertEquals(1, $DB->count_records('forum_discussion_subs', array( 'userid' => $user->id, 'discussion' => $discussion->id, ))); // Subscribing to the discussion should return truthfully because a change was made. $this->assertTrue(\mod_forum\subscriptions::subscribe_user_to_discussion($user->id, $discussion)); $this->assertEquals(0, $DB->count_records('forum_subscriptions', array( 'userid' => $user->id, 'forum' => $forum->id, ))); $this->assertEquals(1, $DB->count_records('forum_discussion_subs', array( 'userid' => $user->id, 'discussion' => $discussion->id, ))); } /** * Test that providing a context_module instance to is_subscribed does not result in additional lookups to retrieve * the context_module. */ public function test_is_subscribed_cm(): void { global $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_FORCESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create a user enrolled in the course as a student. list($user) = $this->helper_create_users($course, 1); // Retrieve the $cm now. $cm = get_fast_modinfo($forum->course)->instances['forum'][$forum->id]; // Reset get_fast_modinfo. get_fast_modinfo(0, 0, true); // Call is_subscribed without passing the $cmid - this should result in a lookup and filling of some of the // caches. This provides us with consistent data to start from. $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum)); $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum)); // Make a note of the number of DB calls. $basecount = $DB->perf_get_reads(); // Call is_subscribed - it should give return the correct result (False), and result in no additional queries. $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, null, $cm)); // The capability check does require some queries, so we don't test it directly. // We don't assert here because this is dependant upon linked code which could change at any time. $suppliedcmcount = $DB->perf_get_reads() - $basecount; // Call is_subscribed without passing the $cmid now - this should result in a lookup. get_fast_modinfo(0, 0, true); $basecount = $DB->perf_get_reads(); $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum)); $calculatedcmcount = $DB->perf_get_reads() - $basecount; // There should be more queries than when we performed the same check a moment ago. $this->assertGreaterThan($suppliedcmcount, $calculatedcmcount); } public static function is_subscribable_forums(): array { return [ [ 'forcesubscribe' => FORUM_DISALLOWSUBSCRIBE, ], [ 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE, ], [ 'forcesubscribe' => FORUM_INITIALSUBSCRIBE, ], [ 'forcesubscribe' => FORUM_FORCESUBSCRIBE, ], ]; } public static function is_subscribable_provider(): array { $data = []; foreach (self::is_subscribable_forums() as $forum) { $data[] = [$forum]; } return $data; } /** * @dataProvider is_subscribable_provider */ public function test_is_subscribable_logged_out($options): void { $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options['course'] = $course->id; $forum = $this->getDataGenerator()->create_module('forum', $options); $this->assertFalse(\mod_forum\subscriptions::is_subscribable($forum)); } /** * @dataProvider is_subscribable_provider */ public function test_is_subscribable_is_guest($options): void { global $DB; $this->resetAfterTest(true); $guest = $DB->get_record('user', array('username'=>'guest')); $this->setUser($guest); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options['course'] = $course->id; $forum = $this->getDataGenerator()->create_module('forum', $options); $this->assertFalse(\mod_forum\subscriptions::is_subscribable($forum)); } public static function is_subscribable_loggedin_provider(): array { return [ [ ['forcesubscribe' => FORUM_DISALLOWSUBSCRIBE], false, ], [ ['forcesubscribe' => FORUM_CHOOSESUBSCRIBE], true, ], [ ['forcesubscribe' => FORUM_INITIALSUBSCRIBE], true, ], [ ['forcesubscribe' => FORUM_FORCESUBSCRIBE], false, ], ]; } /** * @dataProvider is_subscribable_loggedin_provider */ public function test_is_subscribable_loggedin($options, $expect): void { $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options['course'] = $course->id; $forum = $this->getDataGenerator()->create_module('forum', $options); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $this->setUser($user); $this->assertEquals($expect, \mod_forum\subscriptions::is_subscribable($forum)); } public function test_get_user_default_subscription(): void { global $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); $options['course'] = $course->id; $forum = $this->getDataGenerator()->create_module('forum', $options); $cm = get_coursemodule_from_instance("forum", $forum->id, $course->id); // Create a user enrolled in the course as a student. list($author, $student) = $this->helper_create_users($course, 2, 'student'); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); // A guest user. $this->setUser(0); $this->assertFalse((boolean)\mod_forum\subscriptions::get_user_default_subscription($forum, $context, $cm, $discussion->id)); $this->assertFalse((boolean)\mod_forum\subscriptions::get_user_default_subscription($forum, $context, $cm, null)); // A user enrolled in the course. $this->setUser($author->id); $this->assertTrue((boolean)\mod_forum\subscriptions::get_user_default_subscription($forum, $context, $cm, $discussion->id)); $this->assertTrue((boolean)\mod_forum\subscriptions::get_user_default_subscription($forum, $context, $cm, null)); // Subscribption disabled. $this->setUser($student->id); \mod_forum\subscriptions::set_subscription_mode($forum, FORUM_DISALLOWSUBSCRIBE); $forum = $DB->get_record('forum', array('id' => $forum->id)); $this->assertFalse((boolean)\mod_forum\subscriptions::get_user_default_subscription($forum, $context, $cm, $discussion->id)); $this->assertFalse((boolean)\mod_forum\subscriptions::get_user_default_subscription($forum, $context, $cm, null)); \mod_forum\subscriptions::set_subscription_mode($forum, FORUM_FORCESUBSCRIBE); $forum = $DB->get_record('forum', array('id' => $forum->id)); $this->assertTrue((boolean)\mod_forum\subscriptions::get_user_default_subscription($forum, $context, $cm, $discussion->id)); $this->assertTrue((boolean)\mod_forum\subscriptions::get_user_default_subscription($forum, $context, $cm, null)); // Admin user. $this->setAdminUser(); $this->assertTrue((boolean)\mod_forum\subscriptions::get_user_default_subscription($forum, $context, $cm, $discussion->id)); $this->assertTrue((boolean)\mod_forum\subscriptions::get_user_default_subscription($forum, $context, $cm, null)); } } PK-B\J(Y(Ytests/privacy/provider_test.phpnu[. /** * Tests for the forum implementation of the Privacy Provider API. * * @package mod_forum * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\privacy; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once(__DIR__ . '/../generator_trait.php'); require_once($CFG->dirroot . '/rating/lib.php'); use mod_forum\privacy\provider; /** * Tests for the forum implementation of the Privacy Provider API. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends \core_privacy\tests\provider_testcase { // Include the privacy subcontext_info trait. // This includes the subcontext builders. use \mod_forum\privacy\subcontext_info; // Include the mod_forum test helpers. // This includes functions to create forums, users, discussions, and posts. use \mod_forum_tests_generator_trait; // Include the privacy helper trait for the ratings API. use \core_rating\phpunit\privacy_helper; // Include the privacy helper trait for the tag API. use \core_tag\tests\privacy_helper; /** * Test setUp. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(true); } /** * Helper to assert that the forum data is correct. * * @param object $expected The expected data in the forum. * @param object $actual The actual data in the forum. */ protected function assert_forum_data($expected, $actual) { // Exact matches. $this->assertEquals(format_string($expected->name, true), $actual->name); } /** * Helper to assert that the discussion data is correct. * * @param object $expected The expected data in the discussion. * @param object $actual The actual data in the discussion. */ protected function assert_discussion_data($expected, $actual) { // Exact matches. $this->assertEquals(format_string($expected->name, true), $actual->name); $this->assertEquals( \core_privacy\local\request\transform::yesno($expected->pinned), $actual->pinned ); $this->assertEquals( \core_privacy\local\request\transform::datetime($expected->timemodified), $actual->timemodified ); $this->assertEquals( \core_privacy\local\request\transform::datetime($expected->usermodified), $actual->usermodified ); } /** * Helper to assert that the post data is correct. * * @param object $expected The expected data in the post. * @param object $actual The actual data in the post. * @param \core_privacy\local\request\writer $writer The writer used */ protected function assert_post_data($expected, $actual, $writer) { // Exact matches. $this->assertEquals(format_string($expected->subject, true), $actual->subject); // The message should have been passed through the rewriter. // Note: The testable rewrite_pluginfile_urls function in the ignores all items except the text. $this->assertEquals( $writer->rewrite_pluginfile_urls([], '', '', '', $expected->message), $actual->message ); $this->assertEquals( \core_privacy\local\request\transform::datetime($expected->created), $actual->created ); $this->assertEquals( \core_privacy\local\request\transform::datetime($expected->modified), $actual->modified ); } /** * Test that a user who is enrolled in a course, but who has never * posted and has no other metadata stored will not have any link to * that context. */ public function test_user_has_never_posted(): void { // Create a course, with a forum, our user under test, another user, and a discussion + post from the other user. $course = $this->getDataGenerator()->create_course(); $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $course = $this->getDataGenerator()->create_course(); $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); list($user, $otheruser) = $this->helper_create_users($course, 2); list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); // Test that no contexts were retrieved. $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum'); $contexts = $contextlist->get_contextids(); $this->assertCount(0, $contexts); // Attempting to export data for this context should return nothing either. $this->export_context_data_for_user($user->id, $context, 'mod_forum'); $writer = \core_privacy\local\request\writer::with_context($context); // The provider should always export data for any context explicitly asked of it, but there should be no // metadata, files, or discussions. $this->assertEmpty($writer->get_data([get_string('discussions', 'mod_forum')])); $this->assertEmpty($writer->get_all_metadata([])); $this->assertEmpty($writer->get_files([])); } /** * Test that a user who is enrolled in a course, and who has never * posted and has subscribed to the forum will have relevant * information returned. */ public function test_user_has_never_posted_subscribed_to_forum(): void { global $DB; // Create a course, with a forum, our user under test, another user, and a discussion + post from the other user. $course = $this->getDataGenerator()->create_course(); $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $course = $this->getDataGenerator()->create_course(); $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); list($user, $otheruser) = $this->helper_create_users($course, 2); list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); // Subscribe the user to the forum. \mod_forum\subscriptions::subscribe_user($user->id, $forum); // Retrieve all contexts - only this context should be returned. $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum'); $this->assertCount(1, $contextlist); $this->assertEquals($context, $contextlist->current()); // Export all of the data for the context. $this->export_context_data_for_user($user->id, $context, 'mod_forum'); $writer = \core_privacy\local\request\writer::with_context($context); $this->assertTrue($writer->has_any_data()); $subcontext = $this->get_subcontext($forum); // There should be one item of metadata. $this->assertCount(1, $writer->get_all_metadata($subcontext)); // It should be the subscriptionpreference whose value is 1. $this->assertEquals(1, $writer->get_metadata($subcontext, 'subscriptionpreference')); // There should be data about the forum itself. $this->assertNotEmpty($writer->get_data($subcontext)); // Delete the data now. // Only the post by the user under test will be removed. $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( \core_user::get_user($user->id), 'mod_forum', [$context->id] ); $this->assertCount(1, $DB->get_records('forum_subscriptions', ['userid' => $user->id])); provider::delete_data_for_user($approvedcontextlist); $this->assertCount(0, $DB->get_records('forum_subscriptions', ['userid' => $user->id])); } /** * Test that a user who is enrolled in a course, and who has never * posted and has subscribed to the discussion will have relevant * information returned. */ public function test_user_has_never_posted_subscribed_to_discussion(): void { global $DB; // Create a course, with a forum, our user under test, another user, and a discussion + post from the other user. $course = $this->getDataGenerator()->create_course(); $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $course = $this->getDataGenerator()->create_course(); $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); list($user, $otheruser) = $this->helper_create_users($course, 2); // Post twice - only the second discussion should be included. $this->helper_post_to_forum($forum, $otheruser); list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); // Subscribe the user to the discussion. \mod_forum\subscriptions::subscribe_user_to_discussion($user->id, $discussion); // Retrieve all contexts - only this context should be returned. $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum'); $this->assertCount(1, $contextlist); $this->assertEquals($context, $contextlist->current()); // Export all of the data for the context. $this->export_context_data_for_user($user->id, $context, 'mod_forum'); $writer = \core_privacy\local\request\writer::with_context($context); $this->assertTrue($writer->has_any_data()); // There should be nothing in the forum. The user is not subscribed there. $forumsubcontext = $this->get_subcontext($forum); $this->assertCount(0, $writer->get_all_metadata($forumsubcontext)); $this->assert_forum_data($forum, $writer->get_data($forumsubcontext)); // There should be metadata in the discussion. $discsubcontext = $this->get_subcontext($forum, $discussion); $this->assertCount(1, $writer->get_all_metadata($discsubcontext)); // It should be the subscriptionpreference whose value is an Integer. // (It's a timestamp, but it doesn't matter). $metadata = $writer->get_metadata($discsubcontext, 'subscriptionpreference'); $this->assertGreaterThan(1, $metadata); // For context we output the discussion content. $data = $writer->get_data($discsubcontext); $this->assertInstanceOf('stdClass', $data); $this->assert_discussion_data($discussion, $data); // Post content is not exported unless the user participated. $postsubcontext = $this->get_subcontext($forum, $discussion, $post); $this->assertCount(0, $writer->get_data($postsubcontext)); // Delete the data now. // Only the post by the user under test will be removed. $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( \core_user::get_user($user->id), 'mod_forum', [$context->id] ); $this->assertCount(1, $DB->get_records('forum_discussion_subs', ['userid' => $user->id])); provider::delete_data_for_user($approvedcontextlist); $this->assertCount(0, $DB->get_records('forum_discussion_subs', ['userid' => $user->id])); } /** * Test that a user who has posted their own discussion will have all * content returned. */ public function test_user_has_posted_own_discussion(): void { $course = $this->getDataGenerator()->create_course(); $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $course = $this->getDataGenerator()->create_course(); $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); list($user, $otheruser) = $this->helper_create_users($course, 2); // Post twice - only the second discussion should be included. list($discussion, $post) = $this->helper_post_to_forum($forum, $user); list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $otheruser); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); // Retrieve all contexts - only this context should be returned. $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum'); $this->assertCount(1, $contextlist); $this->assertEquals($context, $contextlist->current()); // Export all of the data for the context. $this->setUser($user); $this->export_context_data_for_user($user->id, $context, 'mod_forum'); $writer = \core_privacy\local\request\writer::with_context($context); $this->assertTrue($writer->has_any_data()); // The other discussion should not have been returned as we did not post in it. $this->assertEmpty($writer->get_data($this->get_subcontext($forum, $otherdiscussion))); $this->assert_discussion_data($discussion, $writer->get_data($this->get_subcontext($forum, $discussion))); $this->assert_post_data($post, $writer->get_data($this->get_subcontext($forum, $discussion, $post)), $writer); } /** * Test that a user who has posted a reply to another users discussion will have all content returned, and * appropriate content removed. */ public function test_user_has_posted_reply(): void { global $DB; // Create several courses and forums. We only insert data into the final one. $course = $this->getDataGenerator()->create_course(); $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $course = $this->getDataGenerator()->create_course(); $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); list($user, $otheruser) = $this->helper_create_users($course, 2); // Post twice - only the second discussion should be included. list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser); list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $otheruser); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); // Post a reply to the other person's post. $reply = $this->helper_reply_to_post($post, $user); // Testing as user $user. $this->setUser($user); // Retrieve all contexts - only this context should be returned. $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum'); $this->assertCount(1, $contextlist); $this->assertEquals($context, $contextlist->current()); // Export all of the data for the context. $this->export_context_data_for_user($user->id, $context, 'mod_forum'); $writer = \core_privacy\local\request\writer::with_context($context); $this->assertTrue($writer->has_any_data()); // Refresh the discussions. $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]); $otherdiscussion = $DB->get_record('forum_discussions', ['id' => $otherdiscussion->id]); // The other discussion should not have been returned as we did not post in it. $this->assertEmpty($writer->get_data($this->get_subcontext($forum, $otherdiscussion))); // Our discussion should have been returned as we did post in it. $data = $writer->get_data($this->get_subcontext($forum, $discussion)); $this->assertNotEmpty($data); $this->assert_discussion_data($discussion, $data); // The reply will be included. $this->assert_post_data($reply, $writer->get_data($this->get_subcontext($forum, $discussion, $reply)), $writer); // Delete the data now. // Only the post by the user under test will be removed. $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( \core_user::get_user($user->id), 'mod_forum', [$context->id] ); provider::delete_data_for_user($approvedcontextlist); $reply = $DB->get_record('forum_posts', ['id' => $reply->id]); $this->assertEmpty($reply->subject); $this->assertEmpty($reply->message); $this->assertEquals(1, $reply->deleted); $post = $DB->get_record('forum_posts', ['id' => $post->id]); $this->assertNotEmpty($post->subject); $this->assertNotEmpty($post->message); $this->assertEquals(0, $post->deleted); } /** * Test private reply in a range of scenarios. */ public function test_user_private_reply(): void { global $DB; $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); [$student, $otherstudent] = $this->helper_create_users($course, 2, 'student'); [$teacher, $otherteacher] = $this->helper_create_users($course, 2, 'teacher'); [$discussion, $post] = $this->helper_post_to_forum($forum, $student); $reply = $this->helper_reply_to_post($post, $teacher, [ 'privatereplyto' => $student->id, ]); // Testing as user $student. $this->setUser($student); // Retrieve all contexts - only this context should be returned. $contextlist = $this->get_contexts_for_userid($student->id, 'mod_forum'); $this->assertCount(1, $contextlist); $this->assertEquals($context, $contextlist->current()); // Export all of the data for the context. $this->export_context_data_for_user($student->id, $context, 'mod_forum'); $writer = \core_privacy\local\request\writer::with_context($context); $this->assertTrue($writer->has_any_data()); // The initial post and reply will be included. $this->assert_post_data($post, $writer->get_data($this->get_subcontext($forum, $discussion, $post)), $writer); $this->assert_post_data($reply, $writer->get_data($this->get_subcontext($forum, $discussion, $reply)), $writer); // Testing as user $teacher. \core_privacy\local\request\writer::reset(); $this->setUser($teacher); // Retrieve all contexts - only this context should be returned. $contextlist = $this->get_contexts_for_userid($teacher->id, 'mod_forum'); $this->assertCount(1, $contextlist); $this->assertEquals($context, $contextlist->current()); // Export all of the data for the context. $this->export_context_data_for_user($teacher->id, $context, 'mod_forum'); $writer = \core_privacy\local\request\writer::with_context($context); $this->assertTrue($writer->has_any_data()); // The reply will be included. $this->assert_post_data($post, $writer->get_data($this->get_subcontext($forum, $discussion, $post)), $writer); $this->assert_post_data($reply, $writer->get_data($this->get_subcontext($forum, $discussion, $reply)), $writer); // Testing as user $otherteacher. // The user was not involved in any of the conversation. \core_privacy\local\request\writer::reset(); $this->setUser($otherteacher); // Retrieve all contexts - only this context should be returned. $contextlist = $this->get_contexts_for_userid($otherteacher->id, 'mod_forum'); $this->assertCount(0, $contextlist); // Export all of the data for the context. $this->export_context_data_for_user($otherteacher->id, $context, 'mod_forum'); $writer = \core_privacy\local\request\writer::with_context($context); // The user has none of the discussion. $this->assertEmpty($writer->get_data($this->get_subcontext($forum, $discussion))); // Testing as user $otherstudent. // The user was not involved in any of the conversation. \core_privacy\local\request\writer::reset(); $this->setUser($otherstudent); // Retrieve all contexts - only this context should be returned. $contextlist = $this->get_contexts_for_userid($otherstudent->id, 'mod_forum'); $this->assertCount(0, $contextlist); // Export all of the data for the context. $this->export_context_data_for_user($otherstudent->id, $context, 'mod_forum'); $writer = \core_privacy\local\request\writer::with_context($context); // The user has none of the discussion. $this->assertEmpty($writer->get_data($this->get_subcontext($forum, $discussion))); } /** * Test that the rating of another users content will have only the * rater's information returned. */ public function test_user_has_rated_others(): void { global $DB; $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, 'scale' => 100, ]); list($user, $otheruser) = $this->helper_create_users($course, 2); list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); // Rate the other users content. $rm = new \rating_manager(); $ratingoptions = new \stdClass; $ratingoptions->context = $context; $ratingoptions->component = 'mod_forum'; $ratingoptions->ratingarea = 'post'; $ratingoptions->itemid = $post->id; $ratingoptions->scaleid = $forum->scale; $ratingoptions->userid = $user->id; $rating = new \rating($ratingoptions); $rating->update_rating(75); // Run as the user under test. $this->setUser($user); // Retrieve all contexts - only this context should be returned. $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum'); $this->assertCount(1, $contextlist); $this->assertEquals($context, $contextlist->current()); // Export all of the data for the context. $this->export_context_data_for_user($user->id, $context, 'mod_forum'); $writer = \core_privacy\local\request\writer::with_context($context); $this->assertTrue($writer->has_any_data()); // The discussion should not have been returned as we did not post in it. $this->assertEmpty($writer->get_data($this->get_subcontext($forum, $discussion))); $this->assert_all_own_ratings_on_context( $user->id, $context, $this->get_subcontext($forum, $discussion, $post), 'mod_forum', 'post', $post->id ); // The original post will not be included. $this->assert_post_data($post, $writer->get_data($this->get_subcontext($forum, $discussion, $post)), $writer); // Delete the data of the user who rated the other user. // The rating should not be deleted as it the rating is considered grading data. $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( \core_user::get_user($user->id), 'mod_forum', [$context->id] ); provider::delete_data_for_user($approvedcontextlist); // Ratings should remain as they are of another user's content. $this->assertCount(1, $DB->get_records('rating', ['itemid' => $post->id])); } /** * Test that ratings of a users own content will all be returned. */ public function test_user_has_been_rated(): void { global $DB; $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, 'scale' => 100, ]); list($user, $otheruser, $anotheruser) = $this->helper_create_users($course, 3); list($discussion, $post) = $this->helper_post_to_forum($forum, $user); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); // Other users rate my content. $rm = new \rating_manager(); $ratingoptions = new \stdClass; $ratingoptions->context = $context; $ratingoptions->component = 'mod_forum'; $ratingoptions->ratingarea = 'post'; $ratingoptions->itemid = $post->id; $ratingoptions->scaleid = $forum->scale; $ratingoptions->userid = $otheruser->id; $rating = new \rating($ratingoptions); $rating->update_rating(75); $ratingoptions->userid = $anotheruser->id; $rating = new \rating($ratingoptions); $rating->update_rating(75); // Run as the user under test. $this->setUser($user); // Retrieve all contexts - only this context should be returned. $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum'); $this->assertCount(1, $contextlist); $this->assertEquals($context, $contextlist->current()); // Export all of the data for the context. $this->export_context_data_for_user($user->id, $context, 'mod_forum'); $writer = \core_privacy\local\request\writer::with_context($context); $this->assertTrue($writer->has_any_data()); $this->assert_all_ratings_on_context( $context, $this->get_subcontext($forum, $discussion, $post), 'mod_forum', 'post', $post->id ); // Delete the data of the user who was rated. // The rating should now be deleted. $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( \core_user::get_user($user->id), 'mod_forum', [$context->id] ); provider::delete_data_for_user($approvedcontextlist); // Ratings should remain as they are of another user's content. $this->assertCount(0, $DB->get_records('rating', ['itemid' => $post->id])); } /** * Test that per-user daily digest settings are included correctly. */ public function test_user_forum_digest(): void { global $DB; $course = $this->getDataGenerator()->create_course(); $forum0 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm0 = get_coursemodule_from_instance('forum', $forum0->id); $context0 = \context_module::instance($cm0->id); $forum1 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm1 = get_coursemodule_from_instance('forum', $forum1->id); $context1 = \context_module::instance($cm1->id); $forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm2 = get_coursemodule_from_instance('forum', $forum2->id); $context2 = \context_module::instance($cm2->id); $forum3 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm3 = get_coursemodule_from_instance('forum', $forum3->id); $context3 = \context_module::instance($cm3->id); list($user) = $this->helper_create_users($course, 1); // Set a digest value for each forum. forum_set_user_maildigest($forum0, 0, $user); forum_set_user_maildigest($forum1, 1, $user); forum_set_user_maildigest($forum2, 2, $user); // Run as the user under test. $this->setUser($user); // Retrieve all contexts - three contexts should be returned - the fourth should not be included. $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum'); $this->assertCount(3, $contextlist); $contextids = [ $context0->id, $context1->id, $context2->id, ]; sort($contextids); $contextlistids = $contextlist->get_contextids(); sort($contextlistids); $this->assertEquals($contextids, $contextlistids); // Check export data for each context. $this->export_context_data_for_user($user->id, $context0, 'mod_forum'); $this->assertEquals(0, \core_privacy\local\request\writer::with_context($context0)->get_metadata([], 'digestpreference')); $this->export_context_data_for_user($user->id, $context1, 'mod_forum'); $this->assertEquals(1, \core_privacy\local\request\writer::with_context($context1)->get_metadata([], 'digestpreference')); $this->export_context_data_for_user($user->id, $context2, 'mod_forum'); $this->assertEquals(2, \core_privacy\local\request\writer::with_context($context2)->get_metadata([], 'digestpreference')); // Delete the data for one of the users in one of the forums. $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( \core_user::get_user($user->id), 'mod_forum', [$context1->id] ); $this->assertEquals(0, $DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum0->id])); $this->assertEquals(1, $DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum1->id])); $this->assertEquals(2, $DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum2->id])); provider::delete_data_for_user($approvedcontextlist); $this->assertEquals(0, $DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum0->id])); $this->assertFalse($DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum1->id])); $this->assertEquals(2, $DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum2->id])); } /** * Test that the per-user, per-forum user tracking data is exported. */ public function test_user_tracking_data(): void { global $DB; $course = $this->getDataGenerator()->create_course(); $forumoff = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cmoff = get_coursemodule_from_instance('forum', $forumoff->id); $contextoff = \context_module::instance($cmoff->id); $forumon = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cmon = get_coursemodule_from_instance('forum', $forumon->id); $contexton = \context_module::instance($cmon->id); list($user) = $this->helper_create_users($course, 1); // Set user tracking data. forum_tp_stop_tracking($forumoff->id, $user->id); forum_tp_start_tracking($forumon->id, $user->id); // Run as the user under test. $this->setUser($user); // Retrieve all contexts - only the forum tracking reads should be included. $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum'); $this->assertCount(1, $contextlist); $this->assertEquals($contextoff, $contextlist->current()); // Check export data for each context. $this->export_context_data_for_user($user->id, $contextoff, 'mod_forum'); $this->assertEquals(0, \core_privacy\local\request\writer::with_context($contextoff)->get_metadata([], 'trackreadpreference')); // Delete the data for one of the users in the 'on' forum. $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( \core_user::get_user($user->id), 'mod_forum', [$contexton->id] ); $this->assertTrue($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumoff->id])); $this->assertFalse($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumon->id])); provider::delete_data_for_user($approvedcontextlist); $this->assertTrue($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumoff->id])); $this->assertFalse($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumon->id])); // Delete the data for one of the users in the 'off' forum. $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( \core_user::get_user($user->id), 'mod_forum', [$contextoff->id] ); provider::delete_data_for_user($approvedcontextlist); $this->assertFalse($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumoff->id])); $this->assertFalse($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumon->id])); } /** * Test that the posts which a user has read are returned correctly. */ public function test_user_read_posts(): void { global $DB; $course = $this->getDataGenerator()->create_course(); $forum1 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm1 = get_coursemodule_from_instance('forum', $forum1->id); $context1 = \context_module::instance($cm1->id); $forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm2 = get_coursemodule_from_instance('forum', $forum2->id); $context2 = \context_module::instance($cm2->id); $forum3 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm3 = get_coursemodule_from_instance('forum', $forum3->id); $context3 = \context_module::instance($cm3->id); $forum4 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm4 = get_coursemodule_from_instance('forum', $forum4->id); $context4 = \context_module::instance($cm4->id); list($author, $user) = $this->helper_create_users($course, 2); list($f1d1, $f1p1) = $this->helper_post_to_forum($forum1, $author); $f1p1reply = $this->helper_post_to_discussion($forum1, $f1d1, $author); $f1d1 = $DB->get_record('forum_discussions', ['id' => $f1d1->id]); list($f1d2, $f1p2) = $this->helper_post_to_forum($forum1, $author); list($f2d1, $f2p1) = $this->helper_post_to_forum($forum2, $author); $f2p1reply = $this->helper_post_to_discussion($forum2, $f2d1, $author); $f2d1 = $DB->get_record('forum_discussions', ['id' => $f2d1->id]); list($f2d2, $f2p2) = $this->helper_post_to_forum($forum2, $author); list($f3d1, $f3p1) = $this->helper_post_to_forum($forum3, $author); $f3p1reply = $this->helper_post_to_discussion($forum3, $f3d1, $author); $f3d1 = $DB->get_record('forum_discussions', ['id' => $f3d1->id]); list($f3d2, $f3p2) = $this->helper_post_to_forum($forum3, $author); list($f4d1, $f4p1) = $this->helper_post_to_forum($forum4, $author); $f4p1reply = $this->helper_post_to_discussion($forum4, $f4d1, $author); $f4d1 = $DB->get_record('forum_discussions', ['id' => $f4d1->id]); list($f4d2, $f4p2) = $this->helper_post_to_forum($forum4, $author); // Insert read info. // User has read post1, but not the reply or second post in forum1. forum_tp_add_read_record($user->id, $f1p1->id); // User has read post1 and its reply, but not the second post in forum2. forum_tp_add_read_record($user->id, $f2p1->id); forum_tp_add_read_record($user->id, $f2p1reply->id); // User has read post2 in forum3. forum_tp_add_read_record($user->id, $f3p2->id); // Nothing has been read in forum4. // Run as the user under test. $this->setUser($user); // Retrieve all contexts - should be three - forum4 has no data. $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum'); $this->assertCount(3, $contextlist); $contextids = [ $context1->id, $context2->id, $context3->id, ]; sort($contextids); $contextlistids = $contextlist->get_contextids(); sort($contextlistids); $this->assertEquals($contextids, $contextlistids); // Forum 1. $this->export_context_data_for_user($user->id, $context1, 'mod_forum'); $writer = \core_privacy\local\request\writer::with_context($context1); // User has read f1p1. $readdata = $writer->get_metadata( $this->get_subcontext($forum1, $f1d1, $f1p1), 'postread' ); $this->assertNotEmpty($readdata); $this->assertTrue(isset($readdata->firstread)); $this->assertTrue(isset($readdata->lastread)); // User has not f1p1reply. $readdata = $writer->get_metadata( $this->get_subcontext($forum1, $f1d1, $f1p1reply), 'postread' ); $this->assertEmpty($readdata); // User has not f1p2. $readdata = $writer->get_metadata( $this->get_subcontext($forum1, $f1d2, $f1p2), 'postread' ); $this->assertEmpty($readdata); // Forum 2. $this->export_context_data_for_user($user->id, $context2, 'mod_forum'); $writer = \core_privacy\local\request\writer::with_context($context2); // User has read f2p1. $readdata = $writer->get_metadata( $this->get_subcontext($forum2, $f2d1, $f2p1), 'postread' ); $this->assertNotEmpty($readdata); $this->assertTrue(isset($readdata->firstread)); $this->assertTrue(isset($readdata->lastread)); // User has read f2p1reply. $readdata = $writer->get_metadata( $this->get_subcontext($forum2, $f2d1, $f2p1reply), 'postread' ); $this->assertNotEmpty($readdata); $this->assertTrue(isset($readdata->firstread)); $this->assertTrue(isset($readdata->lastread)); // User has not read f2p2. $readdata = $writer->get_metadata( $this->get_subcontext($forum2, $f2d2, $f2p2), 'postread' ); $this->assertEmpty($readdata); // Forum 3. $this->export_context_data_for_user($user->id, $context3, 'mod_forum'); $writer = \core_privacy\local\request\writer::with_context($context3); // User has not read f3p1. $readdata = $writer->get_metadata( $this->get_subcontext($forum3, $f3d1, $f3p1), 'postread' ); $this->assertEmpty($readdata); // User has not read f3p1reply. $readdata = $writer->get_metadata( $this->get_subcontext($forum3, $f3d1, $f3p1reply), 'postread' ); $this->assertEmpty($readdata); // User has read f3p2. $readdata = $writer->get_metadata( $this->get_subcontext($forum3, $f3d2, $f3p2), 'postread' ); $this->assertNotEmpty($readdata); $this->assertTrue(isset($readdata->firstread)); $this->assertTrue(isset($readdata->lastread)); // Delete all data for one of the users in one of the forums. $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( \core_user::get_user($user->id), 'mod_forum', [$context3->id] ); $this->assertTrue($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum1->id])); $this->assertTrue($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum2->id])); $this->assertTrue($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum3->id])); provider::delete_data_for_user($approvedcontextlist); $this->assertTrue($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum1->id])); $this->assertTrue($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum2->id])); $this->assertFalse($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum3->id])); } /** * Test that posts with attachments have their attachments correctly exported. */ public function test_post_attachment_inclusion(): void { global $DB; $fs = get_file_storage(); $course = $this->getDataGenerator()->create_course(); list($author, $otheruser) = $this->helper_create_users($course, 2); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, 'scale' => 100, ]); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); // Create a new discussion + post in the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]); // Add a number of replies. $reply = $this->helper_reply_to_post($post, $author); $reply = $this->helper_reply_to_post($post, $author); $reply = $this->helper_reply_to_post($reply, $author); $posts[$reply->id] = $reply; // Add a fake inline image to the original post. $createdfile = $fs->create_file_from_string([ 'contextid' => $context->id, 'component' => 'mod_forum', 'filearea' => 'post', 'itemid' => $post->id, 'filepath' => '/', 'filename' => 'example.jpg', ], 'image contents (not really)'); // Tag the post and the final reply. \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']); \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $reply->id, $context, ['example', 'differenttag']); // Create a second discussion + post in the forum without tags. list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $author); $otherdiscussion = $DB->get_record('forum_discussions', ['id' => $otherdiscussion->id]); // Add a number of replies. $reply = $this->helper_reply_to_post($otherpost, $author); $reply = $this->helper_reply_to_post($otherpost, $author); // Run as the user under test. $this->setUser($author); // Retrieve all contexts - should be one. $contextlist = $this->get_contexts_for_userid($author->id, 'mod_forum'); $this->assertCount(1, $contextlist); $this->export_context_data_for_user($author->id, $context, 'mod_forum'); $writer = \core_privacy\local\request\writer::with_context($context); // The inline file should be on the first forum post. $subcontext = $this->get_subcontext($forum, $discussion, $post); $foundfiles = $writer->get_files($subcontext); $this->assertCount(1, $foundfiles); $this->assertEquals($createdfile, reset($foundfiles)); } /** * Test that posts which include tags have those tags exported. */ public function test_post_tags(): void { global $DB; $course = $this->getDataGenerator()->create_course(); list($author, $otheruser) = $this->helper_create_users($course, 2); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, 'scale' => 100, ]); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); // Create a new discussion + post in the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]); // Add a number of replies. $reply = $this->helper_reply_to_post($post, $author); $reply = $this->helper_reply_to_post($post, $author); $reply = $this->helper_reply_to_post($reply, $author); $posts[$reply->id] = $reply; // Tag the post and the final reply. \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']); \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $reply->id, $context, ['example', 'differenttag']); // Create a second discussion + post in the forum without tags. list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $author); $otherdiscussion = $DB->get_record('forum_discussions', ['id' => $otherdiscussion->id]); // Add a number of replies. $reply = $this->helper_reply_to_post($otherpost, $author); $reply = $this->helper_reply_to_post($otherpost, $author); // Run as the user under test. $this->setUser($author); // Retrieve all contexts - should be two. $contextlist = $this->get_contexts_for_userid($author->id, 'mod_forum'); $this->assertCount(1, $contextlist); $this->export_all_data_for_user($author->id, 'mod_forum'); $writer = \core_privacy\local\request\writer::with_context($context); $this->assert_all_tags_match_on_context( $author->id, $context, $this->get_subcontext($forum, $discussion, $post), 'mod_forum', 'forum_posts', $post->id ); } /** * Ensure that all user data is deleted from a context. */ public function test_all_users_deleted_from_context(): void { global $DB; $fs = get_file_storage(); $course = $this->getDataGenerator()->create_course(); $users = $this->helper_create_users($course, 5); $forums = []; $contexts = []; for ($i = 0; $i < 2; $i++) { $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, 'scale' => 100, ]); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); $forums[$forum->id] = $forum; $contexts[$forum->id] = $context; } $discussions = []; $posts = []; foreach ($users as $user) { foreach ($forums as $forum) { $context = $contexts[$forum->id]; // Create a new discussion + post in the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $user); $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]); $discussions[$discussion->id] = $discussion; // Add a number of replies. $posts[$post->id] = $post; $reply = $this->helper_reply_to_post($post, $user); $posts[$reply->id] = $reply; $reply = $this->helper_reply_to_post($post, $user); $posts[$reply->id] = $reply; $reply = $this->helper_reply_to_post($reply, $user); $posts[$reply->id] = $reply; // Add a fake inline image to the original post. $fs->create_file_from_string([ 'contextid' => $context->id, 'component' => 'mod_forum', 'filearea' => 'post', 'itemid' => $post->id, 'filepath' => '/', 'filename' => 'example.jpg', ], 'image contents (not really)'); // And an attachment. $fs->create_file_from_string([ 'contextid' => $context->id, 'component' => 'mod_forum', 'filearea' => 'attachment', 'itemid' => $post->id, 'filepath' => '/', 'filename' => 'example.jpg', ], 'image contents (not really)'); } } // Mark all posts as read by user. $user = reset($users); $ratedposts = []; foreach ($posts as $post) { $discussion = $discussions[$post->discussion]; $forum = $forums[$discussion->forum]; $context = $contexts[$forum->id]; // Mark the post as being read by user. forum_tp_add_read_record($user->id, $post->id); // Tag the post. \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']); // Rate the other users content. if ($post->userid != $user->id) { $ratedposts[$post->id] = $post; $rm = new \rating_manager(); $ratingoptions = (object) [ 'context' => $context, 'component' => 'mod_forum', 'ratingarea' => 'post', 'itemid' => $post->id, 'scaleid' => $forum->scale, 'userid' => $user->id, ]; $rating = new \rating($ratingoptions); $rating->update_rating(75); } } // Run as the user under test. $this->setUser($user); // Retrieve all contexts - should be two. $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum'); $this->assertCount(2, $contextlist); // These are the contexts we expect. $contextids = array_map(function($context) { return $context->id; }, $contexts); sort($contextids); $contextlistids = $contextlist->get_contextids(); sort($contextlistids); $this->assertEquals($contextids, $contextlistids); // Delete for the first forum. $forum = reset($forums); $context = $contexts[$forum->id]; provider::delete_data_for_all_users_in_context($context); // Determine what should have been deleted. $discussionsinforum = array_filter($discussions, function($discussion) use ($forum) { return $discussion->forum == $forum->id; }); $postsinforum = array_filter($posts, function($post) use ($discussionsinforum) { return isset($discussionsinforum[$post->discussion]); }); // All forum discussions and posts should have been deleted in this forum. $this->assertCount(0, $DB->get_records('forum_discussions', ['forum' => $forum->id])); list ($insql, $inparams) = $DB->get_in_or_equal(array_keys($discussionsinforum)); $this->assertCount(0, $DB->get_records_select('forum_posts', "discussion {$insql}", $inparams)); // All uploaded files relating to this context should have been deleted (post content). foreach ($postsinforum as $post) { $this->assertEmpty($fs->get_area_files($context->id, 'mod_forum', 'post', $post->id)); $this->assertEmpty($fs->get_area_files($context->id, 'mod_forum', 'attachment', $post->id)); } // All ratings should have been deleted. $rm = new \rating_manager(); foreach ($postsinforum as $post) { $ratings = $rm->get_all_ratings_for_item((object) [ 'context' => $context, 'component' => 'mod_forum', 'ratingarea' => 'post', 'itemid' => $post->id, ]); $this->assertEmpty($ratings); } // All tags should have been deleted. $posttags = \core_tag_tag::get_items_tags('mod_forum', 'forum_posts', array_keys($postsinforum)); foreach ($posttags as $tags) { $this->assertEmpty($tags); } // Check the other forum too. It should remain intact. $forum = next($forums); $context = $contexts[$forum->id]; // Grab the list of discussions and posts in the forum. $discussionsinforum = array_filter($discussions, function($discussion) use ($forum) { return $discussion->forum == $forum->id; }); $postsinforum = array_filter($posts, function($post) use ($discussionsinforum) { return isset($discussionsinforum[$post->discussion]); }); // Forum discussions and posts should not have been deleted in this forum. $this->assertGreaterThan(0, $DB->count_records('forum_discussions', ['forum' => $forum->id])); list ($insql, $inparams) = $DB->get_in_or_equal(array_keys($discussionsinforum)); $this->assertGreaterThan(0, $DB->count_records_select('forum_posts', "discussion {$insql}", $inparams)); // Uploaded files relating to this context should remain. foreach ($postsinforum as $post) { if ($post->parent == 0) { $this->assertNotEmpty($fs->get_area_files($context->id, 'mod_forum', 'post', $post->id)); } } // Ratings should not have been deleted. $rm = new \rating_manager(); foreach ($postsinforum as $post) { if (!isset($ratedposts[$post->id])) { continue; } $ratings = $rm->get_all_ratings_for_item((object) [ 'context' => $context, 'component' => 'mod_forum', 'ratingarea' => 'post', 'itemid' => $post->id, ]); $this->assertNotEmpty($ratings); } // All tags should remain. $posttags = \core_tag_tag::get_items_tags('mod_forum', 'forum_posts', array_keys($postsinforum)); foreach ($posttags as $tags) { $this->assertNotEmpty($tags); } } /** * Ensure that all user data is deleted for a specific context. */ public function test_delete_data_for_user(): void { global $DB; $fs = get_file_storage(); $course = $this->getDataGenerator()->create_course(); $users = $this->helper_create_users($course, 5); $forums = []; $contexts = []; for ($i = 0; $i < 2; $i++) { $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, 'scale' => 100, ]); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); $forums[$forum->id] = $forum; $contexts[$forum->id] = $context; } $discussions = []; $posts = []; $postsbyforum = []; foreach ($users as $user) { $postsbyforum[$user->id] = []; foreach ($forums as $forum) { $context = $contexts[$forum->id]; // Create a new discussion + post in the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $user); $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]); $discussions[$discussion->id] = $discussion; $postsbyforum[$user->id][$context->id] = []; // Add a number of replies. $posts[$post->id] = $post; $thisforumposts[$post->id] = $post; $postsbyforum[$user->id][$context->id][$post->id] = $post; $reply = $this->helper_reply_to_post($post, $user); $posts[$reply->id] = $reply; $postsbyforum[$user->id][$context->id][$reply->id] = $reply; $reply = $this->helper_reply_to_post($post, $user); $posts[$reply->id] = $reply; $postsbyforum[$user->id][$context->id][$reply->id] = $reply; $reply = $this->helper_reply_to_post($reply, $user); $posts[$reply->id] = $reply; $postsbyforum[$user->id][$context->id][$reply->id] = $reply; // Add a fake inline image to the original post. $fs->create_file_from_string([ 'contextid' => $context->id, 'component' => 'mod_forum', 'filearea' => 'post', 'itemid' => $post->id, 'filepath' => '/', 'filename' => 'example.jpg', ], 'image contents (not really)'); // And a fake attachment. $fs->create_file_from_string([ 'contextid' => $context->id, 'component' => 'mod_forum', 'filearea' => 'attachment', 'itemid' => $post->id, 'filepath' => '/', 'filename' => 'example.jpg', ], 'image contents (not really)'); } } // Mark all posts as read by user1. $user1 = reset($users); foreach ($posts as $post) { $discussion = $discussions[$post->discussion]; $forum = $forums[$discussion->forum]; $context = $contexts[$forum->id]; // Mark the post as being read by user1. forum_tp_add_read_record($user1->id, $post->id); } // Rate and tag all posts. $ratedposts = []; foreach ($users as $user) { foreach ($posts as $post) { $discussion = $discussions[$post->discussion]; $forum = $forums[$discussion->forum]; $context = $contexts[$forum->id]; // Tag the post. \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']); // Rate the other users content. if ($post->userid != $user->id) { $ratedposts[$post->id] = $post; $rm = new \rating_manager(); $ratingoptions = (object) [ 'context' => $context, 'component' => 'mod_forum', 'ratingarea' => 'post', 'itemid' => $post->id, 'scaleid' => $forum->scale, 'userid' => $user->id, ]; $rating = new \rating($ratingoptions); $rating->update_rating(75); } } } // Delete for one of the forums for the first user. $firstcontext = reset($contexts); $deletedpostids = []; $otherpostids = []; foreach ($postsbyforum as $user => $contexts) { foreach ($contexts as $thiscontextid => $theseposts) { $thesepostids = array_map(function($post) { return $post->id; }, $theseposts); if ($user == $user1->id && $thiscontextid == $firstcontext->id) { // This post is in the deleted context and by the target user. $deletedpostids = array_merge($deletedpostids, $thesepostids); } else { // This post is by another user, or in a non-target context. $otherpostids = array_merge($otherpostids, $thesepostids); } } } list($postinsql, $postinparams) = $DB->get_in_or_equal($deletedpostids, SQL_PARAMS_NAMED); list($otherpostinsql, $otherpostinparams) = $DB->get_in_or_equal($otherpostids, SQL_PARAMS_NAMED); $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( \core_user::get_user($user1->id), 'mod_forum', [$firstcontext->id] ); provider::delete_data_for_user($approvedcontextlist); // All posts should remain. $this->assertCount(40, $DB->get_records('forum_posts')); // There should be 8 posts belonging to user1. $this->assertCount(8, $DB->get_records('forum_posts', [ 'userid' => $user1->id, ])); // Four of those posts should have been marked as deleted. // That means that the deleted flag is set, and both the subject and message are empty. $this->assertCount(4, $DB->get_records_select('forum_posts', "userid = :userid AND deleted = :deleted" . " AND " . $DB->sql_compare_text('subject') . " = " . $DB->sql_compare_text(':subject') . " AND " . $DB->sql_compare_text('message') . " = " . $DB->sql_compare_text(':message') , [ 'userid' => $user1->id, 'deleted' => 1, 'subject' => '', 'message' => '', ])); // Only user1's posts should have been marked this way. $this->assertCount(4, $DB->get_records('forum_posts', [ 'deleted' => 1, ])); $this->assertCount(4, $DB->get_records_select('forum_posts', $DB->sql_compare_text('subject') . " = " . $DB->sql_compare_text(':subject'), [ 'subject' => '', ])); $this->assertCount(4, $DB->get_records_select('forum_posts', $DB->sql_compare_text('message') . " = " . $DB->sql_compare_text(':message'), [ 'message' => '', ])); // Only the posts in the first discussion should have been marked this way. $this->assertCount(4, $DB->get_records_select('forum_posts', "deleted = :deleted AND id {$postinsql}", array_merge($postinparams, [ 'deleted' => 1, ]) )); // Ratings should have been removed from the affected posts. $this->assertCount(0, $DB->get_records_select('rating', "itemid {$postinsql}", $postinparams)); // Ratings should remain on posts in the other context, and posts not belonging to the affected user. $this->assertCount(144, $DB->get_records_select('rating', "itemid {$otherpostinsql}", $otherpostinparams)); // Ratings should remain where the user has rated another person's post. $this->assertCount(32, $DB->get_records('rating', ['userid' => $user1->id])); // Tags for the affected posts should be removed. $this->assertCount(0, $DB->get_records_select('tag_instance', "itemid {$postinsql}", $postinparams)); // Tags should remain for the other posts by this user, and all posts by other users. $this->assertCount(72, $DB->get_records_select('tag_instance', "itemid {$otherpostinsql}", $otherpostinparams)); // Files for the affected posts should be removed. // 5 users * 2 forums * 1 file in each forum // Original total: 10 // One post with file removed. $componentsql = "component = 'mod_forum' AND "; $this->assertCount(0, $DB->get_records_select('files', "{$componentsql} itemid {$postinsql}", $postinparams)); // Files for the other posts should remain. $this->assertCount(18, $DB->get_records_select('files', "{$componentsql} filename <> '.' AND itemid {$otherpostinsql}", $otherpostinparams)); } /** * Ensure that user data for specific users is deleted from a specified context. */ public function test_delete_data_for_users(): void { global $DB; $fs = get_file_storage(); $course = $this->getDataGenerator()->create_course(); $users = $this->helper_create_users($course, 5); $forums = []; $contexts = []; for ($i = 0; $i < 2; $i++) { $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, 'scale' => 100, ]); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); $forums[$forum->id] = $forum; $contexts[$forum->id] = $context; } $discussions = []; $posts = []; $postsbyforum = []; foreach ($users as $user) { $postsbyforum[$user->id] = []; foreach ($forums as $forum) { $context = $contexts[$forum->id]; // Create a new discussion + post in the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $user); $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]); $discussions[$discussion->id] = $discussion; $postsbyforum[$user->id][$context->id] = []; // Add a number of replies. $posts[$post->id] = $post; $thisforumposts[$post->id] = $post; $postsbyforum[$user->id][$context->id][$post->id] = $post; $reply = $this->helper_reply_to_post($post, $user); $posts[$reply->id] = $reply; $postsbyforum[$user->id][$context->id][$reply->id] = $reply; $reply = $this->helper_reply_to_post($post, $user); $posts[$reply->id] = $reply; $postsbyforum[$user->id][$context->id][$reply->id] = $reply; $reply = $this->helper_reply_to_post($reply, $user); $posts[$reply->id] = $reply; $postsbyforum[$user->id][$context->id][$reply->id] = $reply; // Add a fake inline image to the original post. $fs->create_file_from_string([ 'contextid' => $context->id, 'component' => 'mod_forum', 'filearea' => 'post', 'itemid' => $post->id, 'filepath' => '/', 'filename' => 'example.jpg', ], 'image contents (not really)'); // And a fake attachment. $fs->create_file_from_string([ 'contextid' => $context->id, 'component' => 'mod_forum', 'filearea' => 'attachment', 'itemid' => $post->id, 'filepath' => '/', 'filename' => 'example.jpg', ], 'image contents (not really)'); } } // Mark all posts as read by user1. $user1 = reset($users); foreach ($posts as $post) { $discussion = $discussions[$post->discussion]; $forum = $forums[$discussion->forum]; $context = $contexts[$forum->id]; // Mark the post as being read by user1. forum_tp_add_read_record($user1->id, $post->id); } // Rate and tag all posts. $ratedposts = []; foreach ($users as $user) { foreach ($posts as $post) { $discussion = $discussions[$post->discussion]; $forum = $forums[$discussion->forum]; $context = $contexts[$forum->id]; // Tag the post. \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']); // Rate the other users content. if ($post->userid != $user->id) { $ratedposts[$post->id] = $post; $rm = new \rating_manager(); $ratingoptions = (object) [ 'context' => $context, 'component' => 'mod_forum', 'ratingarea' => 'post', 'itemid' => $post->id, 'scaleid' => $forum->scale, 'userid' => $user->id, ]; $rating = new \rating($ratingoptions); $rating->update_rating(75); } } } // Delete for one of the forums for the first user. $firstcontext = reset($contexts); $deletedpostids = []; $otherpostids = []; foreach ($postsbyforum as $user => $contexts) { foreach ($contexts as $thiscontextid => $theseposts) { $thesepostids = array_map(function($post) { return $post->id; }, $theseposts); if ($user == $user1->id && $thiscontextid == $firstcontext->id) { // This post is in the deleted context and by the target user. $deletedpostids = array_merge($deletedpostids, $thesepostids); } else { // This post is by another user, or in a non-target context. $otherpostids = array_merge($otherpostids, $thesepostids); } } } list($postinsql, $postinparams) = $DB->get_in_or_equal($deletedpostids, SQL_PARAMS_NAMED); list($otherpostinsql, $otherpostinparams) = $DB->get_in_or_equal($otherpostids, SQL_PARAMS_NAMED); $approveduserlist = new \core_privacy\local\request\approved_userlist($firstcontext, 'mod_forum', [$user1->id]); provider::delete_data_for_users($approveduserlist); // All posts should remain. $this->assertCount(40, $DB->get_records('forum_posts')); // There should be 8 posts belonging to user1. $this->assertCount(8, $DB->get_records('forum_posts', [ 'userid' => $user1->id, ])); // Four of those posts should have been marked as deleted. // That means that the deleted flag is set, and both the subject and message are empty. $this->assertCount(4, $DB->get_records_select('forum_posts', "userid = :userid AND deleted = :deleted" . " AND " . $DB->sql_compare_text('subject') . " = " . $DB->sql_compare_text(':subject') . " AND " . $DB->sql_compare_text('message') . " = " . $DB->sql_compare_text(':message') , [ 'userid' => $user1->id, 'deleted' => 1, 'subject' => '', 'message' => '', ])); // Only user1's posts should have been marked this way. $this->assertCount(4, $DB->get_records('forum_posts', [ 'deleted' => 1, ])); $this->assertCount(4, $DB->get_records_select('forum_posts', $DB->sql_compare_text('subject') . " = " . $DB->sql_compare_text(':subject'), [ 'subject' => '', ])); $this->assertCount(4, $DB->get_records_select('forum_posts', $DB->sql_compare_text('message') . " = " . $DB->sql_compare_text(':message'), [ 'message' => '', ])); // Only the posts in the first discussion should have been marked this way. $this->assertCount(4, $DB->get_records_select('forum_posts', "deleted = :deleted AND id {$postinsql}", array_merge($postinparams, [ 'deleted' => 1, ]) )); // Ratings should have been removed from the affected posts. $this->assertCount(0, $DB->get_records_select('rating', "itemid {$postinsql}", $postinparams)); // Ratings should remain on posts in the other context, and posts not belonging to the affected user. $this->assertCount(144, $DB->get_records_select('rating', "itemid {$otherpostinsql}", $otherpostinparams)); // Ratings should remain where the user has rated another person's post. $this->assertCount(32, $DB->get_records('rating', ['userid' => $user1->id])); // Tags for the affected posts should be removed. $this->assertCount(0, $DB->get_records_select('tag_instance', "itemid {$postinsql}", $postinparams)); // Tags should remain for the other posts by this user, and all posts by other users. $this->assertCount(72, $DB->get_records_select('tag_instance', "itemid {$otherpostinsql}", $otherpostinparams)); // Files for the affected posts should be removed. // 5 users * 2 forums * 1 file in each forum // Original total: 10 // One post with file removed. $componentsql = "component = 'mod_forum' AND "; $this->assertCount(0, $DB->get_records_select('files', "{$componentsql} itemid {$postinsql}", $postinparams)); // Files for the other posts should remain. $this->assertCount(18, $DB->get_records_select('files', "{$componentsql} filename <> '.' AND itemid {$otherpostinsql}", $otherpostinparams)); } /** * Ensure that the discussion author is listed as a user in the context. */ public function test_get_users_in_context_post_author(): void { global $DB; $component = 'mod_forum'; $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); list($author, $user) = $this->helper_create_users($course, 2); list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author); $userlist = new \core_privacy\local\request\userlist($context, $component); \mod_forum\privacy\provider::get_users_in_context($userlist); // There should only be one user in the list. $this->assertCount(1, $userlist); $this->assertEquals([$author->id], $userlist->get_userids()); } /** * Ensure that all post authors are included as a user in the context. */ public function test_get_users_in_context_post_authors(): void { global $DB; $component = 'mod_forum'; $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); list($author, $user, $other) = $this->helper_create_users($course, 3); list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author); $fp1reply = $this->helper_post_to_discussion($forum, $fd1, $user); $fd1 = $DB->get_record('forum_discussions', ['id' => $fd1->id]); $userlist = new \core_privacy\local\request\userlist($context, $component); \mod_forum\privacy\provider::get_users_in_context($userlist); // Two users - author and replier. $this->assertCount(2, $userlist); $expected = [$author->id, $user->id]; sort($expected); $actual = $userlist->get_userids(); sort($actual); $this->assertEquals($expected, $actual); } /** * Ensure that all post raters are included as a user in the context. */ public function test_get_users_in_context_post_ratings(): void { global $DB; $component = 'mod_forum'; $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); list($author, $user, $other) = $this->helper_create_users($course, 3); list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author); // Rate the other users content. $rm = new \rating_manager(); $ratingoptions = (object) [ 'context' => $context, 'component' => 'mod_forum', 'ratingarea' => 'post', 'itemid' => $fp1->id, 'scaleid' => $forum->scale, 'userid' => $user->id, ]; $rating = new \rating($ratingoptions); $rating->update_rating(75); $fp1reply = $this->helper_post_to_discussion($forum, $fd1, $author); $fd1 = $DB->get_record('forum_discussions', ['id' => $fd1->id]); $userlist = new \core_privacy\local\request\userlist($context, $component); \mod_forum\privacy\provider::get_users_in_context($userlist); // Two users - author and rater. $this->assertCount(2, $userlist); $expected = [$author->id, $user->id]; sort($expected); $actual = $userlist->get_userids(); sort($actual); $this->assertEquals($expected, $actual); } /** * Ensure that all users with a digest preference are included as a user in the context. */ public function test_get_users_in_context_digest_preference(): void { global $DB; $component = 'mod_forum'; $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); $otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $othercm = get_coursemodule_from_instance('forum', $otherforum->id); $othercontext = \context_module::instance($othercm->id); list($user, $otheruser) = $this->helper_create_users($course, 2); // Add digest subscriptions. forum_set_user_maildigest($forum, 0, $user); forum_set_user_maildigest($otherforum, 0, $otheruser); $userlist = new \core_privacy\local\request\userlist($context, $component); \mod_forum\privacy\provider::get_users_in_context($userlist); // One user - the one with a digest preference. $this->assertCount(1, $userlist); $expected = [$user->id]; sort($expected); $actual = $userlist->get_userids(); sort($actual); $this->assertEquals($expected, $actual); } /** * Ensure that all users with a forum subscription preference included as a user in the context. */ public function test_get_users_in_context_with_subscription(): void { global $DB; $component = 'mod_forum'; $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); $otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $othercm = get_coursemodule_from_instance('forum', $otherforum->id); $othercontext = \context_module::instance($othercm->id); list($user, $otheruser) = $this->helper_create_users($course, 2); // Subscribe the user to the forum. \mod_forum\subscriptions::subscribe_user($user->id, $forum); $userlist = new \core_privacy\local\request\userlist($context, $component); \mod_forum\privacy\provider::get_users_in_context($userlist); // One user - the one with a digest preference. $this->assertCount(1, $userlist); $expected = [$user->id]; sort($expected); $actual = $userlist->get_userids(); sort($actual); $this->assertEquals($expected, $actual); } /** * Ensure that all users with a per-discussion subscription preference included as a user in the context. */ public function test_get_users_in_context_with_discussion_subscription(): void { global $DB; $component = 'mod_forum'; $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); $otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $othercm = get_coursemodule_from_instance('forum', $otherforum->id); $othercontext = \context_module::instance($othercm->id); list($author, $user, $otheruser) = $this->helper_create_users($course, 3); // Post in both of the forums. list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author); list($ofd1, $ofp1) = $this->helper_post_to_forum($otherforum, $author); // Subscribe the user to the discussions. \mod_forum\subscriptions::subscribe_user_to_discussion($user->id, $fd1); \mod_forum\subscriptions::subscribe_user_to_discussion($otheruser->id, $ofd1); $userlist = new \core_privacy\local\request\userlist($context, $component); \mod_forum\privacy\provider::get_users_in_context($userlist); // Two users - the author, and the one who subscribed. $this->assertCount(2, $userlist); $expected = [$author->id, $user->id]; sort($expected); $actual = $userlist->get_userids(); sort($actual); $this->assertEquals($expected, $actual); } /** * Ensure that all users with read tracking are included as a user in the context. */ public function test_get_users_in_context_with_read_post_tracking(): void { global $DB; $component = 'mod_forum'; $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); $otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $othercm = get_coursemodule_from_instance('forum', $otherforum->id); $othercontext = \context_module::instance($othercm->id); list($author, $user, $otheruser) = $this->helper_create_users($course, 3); // Post in both of the forums. list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author); list($ofd1, $ofp1) = $this->helper_post_to_forum($otherforum, $author); // Add read information for those users. forum_tp_add_read_record($user->id, $fp1->id); forum_tp_add_read_record($otheruser->id, $ofp1->id); $userlist = new \core_privacy\local\request\userlist($context, $component); \mod_forum\privacy\provider::get_users_in_context($userlist); // Two user - the author, and the one who has read the post. $this->assertCount(2, $userlist); $expected = [$author->id, $user->id]; sort($expected); $actual = $userlist->get_userids(); sort($actual); $this->assertEquals($expected, $actual); } /** * Ensure that all users with tracking preferences are included as a user in the context. */ public function test_get_users_in_context_with_tracking_preferences(): void { global $DB; $component = 'mod_forum'; $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); $otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $othercm = get_coursemodule_from_instance('forum', $otherforum->id); $othercontext = \context_module::instance($othercm->id); list($author, $user, $otheruser) = $this->helper_create_users($course, 3); // Forum tracking is opt-out. // Stop tracking the read posts. forum_tp_stop_tracking($forum->id, $user->id); forum_tp_stop_tracking($otherforum->id, $otheruser->id); $userlist = new \core_privacy\local\request\userlist($context, $component); \mod_forum\privacy\provider::get_users_in_context($userlist); // One user - the one who is tracking that forum. $this->assertCount(1, $userlist); $expected = [$user->id]; sort($expected); $actual = $userlist->get_userids(); sort($actual); $this->assertEquals($expected, $actual); } /** * Test exporting plugin user preferences */ public function test_export_user_preferences(): void { $this->setAdminUser(); // Create a user with some forum preferences. $user = $this->getDataGenerator()->create_user([ 'maildigest' => 2, 'autosubscribe' => 1, 'trackforums' => 0, ]); set_user_preference('markasreadonnotification', 0, $user); set_user_preference('forum_discussionlistsortorder', \mod_forum\local\vaults\discussion_list::SORTORDER_STARTER_ASC, $user); // Export test users preferences. provider::export_user_preferences($user->id); $writer = \core_privacy\local\request\writer::with_context(\context_system::instance()); $this->assertTrue($writer->has_any_data()); $preferences = (array) $writer->get_user_preferences('mod_forum'); $this->assertEquals((object) [ 'value' => 2, 'description' => get_string('emaildigestsubjects'), ], $preferences['maildigest']); $this->assertEquals((object) [ 'value' => 1, 'description' => get_string('autosubscribeyes'), ], $preferences['autosubscribe']); $this->assertEquals((object) [ 'value' => 0, 'description' => get_string('trackforumsno'), ], $preferences['trackforums']); $this->assertEquals((object) [ 'value' => 0, 'description' => get_string('markasreadonnotificationno', 'mod_forum'), ], $preferences['markasreadonnotification']); $this->assertEquals((object) [ 'value' => \mod_forum\local\vaults\discussion_list::SORTORDER_STARTER_ASC, 'description' => get_string('discussionlistsortbystarterasc', 'mod_forum'), ], $preferences['forum_discussionlistsortorder']); } } PK-B\4"tests/entities_discussion_test.phpnu[. namespace mod_forum; use mod_forum\local\entities\discussion as discussion_entity; use mod_forum\local\entities\post as post_entity; /** * The discussion entity tests. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class entities_discussion_test extends \advanced_testcase { /** * Test the entity returns expected values. */ public function test_entity(): void { $this->resetAfterTest(); // In the past to ensure the time started is true. $time = time() + 10; $discussion = new discussion_entity( 1, 2, 3, 'test discussion', 4, 5, 6, false, $time, $time, 0, 0, false, 0 ); $firstpost = new post_entity( 4, 1, 0, 1, time(), time(), true, 'post subject', 'post message', 1, true, false, 0, false, false, false, null, null ); $notfirstpost = new post_entity( 1, 1, 0, 1, time(), time(), true, 'post subject', 'post message', 1, true, false, 0, false, false, false, null, null ); $this->assertEquals(1, $discussion->get_id()); $this->assertEquals(2, $discussion->get_course_id()); $this->assertEquals(3, $discussion->get_forum_id()); $this->assertEquals('test discussion', $discussion->get_name()); $this->assertEquals(4, $discussion->get_first_post_id()); $this->assertEquals(5, $discussion->get_user_id()); $this->assertEquals(6, $discussion->get_group_id()); $this->assertEquals(false, $discussion->is_assessed()); $this->assertEquals($time, $discussion->get_time_modified()); $this->assertEquals($time, $discussion->get_user_modified()); $this->assertEquals(0, $discussion->get_time_start()); $this->assertEquals(0, $discussion->get_time_end()); $this->assertEquals(false, $discussion->is_pinned()); $this->assertEquals(true, $discussion->is_first_post($firstpost)); $this->assertEquals(false, $discussion->is_first_post($notfirstpost)); $this->assertEquals(true, $discussion->has_started()); $this->assertEquals(true, $discussion->has_group()); } /** * Test the display period settings for discussions. * This covers each individual date function as well as the combination of the 2. * * @dataProvider diplay_period_options_provider * @param string $testdescription A basic description of the base assertions. * @param int $startoffset Start time offset with current time in seconds. * @param int $endoffset End time offset with current time in seconds. * @param bool $timestartresult Expected result from the has_started function * @param bool $timeendresult Expected result from the has_ended function * @param bool $isvisible Expected result from the is_timed_discussion_visible function */ public function test_display_period_settings($testdescription, $startoffset, $endoffset, $timestartresult, $timeendresult, $isvisible): void { global $CFG; $this->resetAfterTest(); $basetime = time(); $starttime = $startoffset != 0 ? $basetime + $startoffset : 0; $endtime = $endoffset != 0 ? $basetime + $endoffset : 0; $discussion = new discussion_entity( 1, 2, 3, 'test discussion', 4, 5, 6, false, $basetime, $basetime, $starttime, $endtime, false, 0 ); $CFG->forum_enabletimedposts = true; $this->assertEquals($timestartresult, $discussion->has_started(), $testdescription); $this->assertEquals($timeendresult, $discussion->has_ended(), $testdescription); $this->assertEquals($isvisible, $discussion->is_timed_discussion_visible(), $testdescription); } /** * Data provider for test_display_period_settings(). * * @return array start/end time offsets and the expected results. */ public static function diplay_period_options_provider(): array { return array( ["No dates set", 0, 0, true, false, true], ["Only started date in the future", 100, 0, false, false, false], ["Only started date in the past", -100, 0, true, false, true], ["Only end date in the future", 0, 100, true, false, true], ["Only end date in the past", 0, -100, true, true, false], ["Start date in the past, end date in the future", -100, 100, true, false, true], ["Both dates in the past", -100, -50, true, true, false], ["Both dates in the future", 100, 150, false, false, false], ); } } PK-B\ww%tests/vaults_discussion_list_test.phpnu[. namespace mod_forum; use mod_forum_external; use mod_forum_tests_generator_trait; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/forum/externallib.php'); require_once(__DIR__ . '/generator_trait.php'); /** * The discussion_list vault tests. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class vaults_discussion_list_test extends \advanced_testcase { // Make use of the test generator trait. use mod_forum_tests_generator_trait; /** @var \mod_forum\local\vaults\discussion_list */ private $vault; /** * Set up function for tests. */ public function setUp(): void { parent::setUp(); $vaultfactory = \mod_forum\local\container::get_vault_factory(); $this->vault = $vaultfactory->get_discussions_in_forum_vault(); } /** * Test get_from_id. */ public function test_get_from_id(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $vault = $this->vault; $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); [$discussion, $post] = $this->helper_post_to_forum($forum, $user); $discussionlist = $vault->get_from_id($discussion->id); $this->assertEquals($discussion->id, $discussionlist->get_discussion()->get_id()); $this->assertEquals($post->id, $discussionlist->get_first_post()->get_id()); $this->assertEquals($user->id, $discussionlist->get_first_post_author()->get_id()); $this->assertEquals($user->id, $discussionlist->get_latest_post_author()->get_id()); } /** * Test get_from_forum_id. */ public function test_get_from_forum_id(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $vault = $this->vault; $user = $datagenerator->create_user(); self::setUser($user); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); $this->getDataGenerator()->enrol_user($user->id, $course->id, null, 'manual'); $this->assertEquals([], $vault->get_from_forum_id($forum->id, true, $user->id, null, 0, 0)); $now = time(); [$discussion1, $post1] = $this->helper_post_to_forum($forum, $user, ['timestart' => $now - 10, 'timemodified' => 1]); [$discussion2, $post2] = $this->helper_post_to_forum($forum, $user, ['timestart' => $now - 9, 'timemodified' => 2]); [$hiddendiscussion, $post3] = $this->helper_post_to_forum($forum, $user, ['timestart' => $now + 10, 'timemodified' => 3]); [$discussion3, $post4] = $this->helper_post_to_forum($forum, $user, ['timestart' => $now - 8, 'timemodified' => 4]); // By default orders the discussions by last post. $summaries = array_values($vault->get_from_forum_id($forum->id, false, null, null, 0, 0)); $this->assertCount(3, $summaries); $this->assertEquals($discussion3->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($discussion1->id, $summaries[2]->get_discussion()->get_id()); $summaries = array_values($vault->get_from_forum_id($forum->id, true, null, null, 0, 0)); $this->assertCount(4, $summaries); $this->assertEquals($hiddendiscussion->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($discussion3->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($discussion1->id, $summaries[3]->get_discussion()->get_id()); $summaries = array_values($vault->get_from_forum_id($forum->id, false, $user->id, null, 0, 0)); $this->assertCount(4, $summaries); $this->assertEquals($hiddendiscussion->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($discussion3->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($discussion1->id, $summaries[3]->get_discussion()->get_id()); $summaries = array_values($vault->get_from_forum_id($forum->id, true, null, null, 1, 0)); $this->assertCount(1, $summaries); $this->assertEquals($hiddendiscussion->id, $summaries[0]->get_discussion()->get_id()); $summaries = array_values($vault->get_from_forum_id($forum->id, true, null, null, 1, 1)); $this->assertCount(1, $summaries); $this->assertEquals($discussion3->id, $summaries[0]->get_discussion()->get_id()); $summaries = array_values($vault->get_from_forum_id($forum->id, true, null, null, 1, 2)); $this->assertCount(1, $summaries); $this->assertEquals($discussion2->id, $summaries[0]->get_discussion()->get_id()); // Create 2 replies for $post1. $this->helper_reply_to_post($post1, $user); $this->helper_reply_to_post($post1, $user); // Create 3 replies for $post2. $this->helper_reply_to_post($post2, $user); $this->helper_reply_to_post($post2, $user); $this->helper_reply_to_post($post2, $user); // Create 1 reply for $post3. $this->helper_reply_to_post($post3, $user); // Sort discussions by last post DESC. $summaries = array_values($vault->get_from_forum_id($forum->id, true, null, $vault::SORTORDER_LASTPOST_DESC, 0, 0)); $this->assertCount(4, $summaries); $this->assertEquals($hiddendiscussion->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($discussion1->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($discussion3->id, $summaries[3]->get_discussion()->get_id()); // Sort discussions by last post ASC. $summaries = array_values($vault->get_from_forum_id($forum->id, true, null, $vault::SORTORDER_LASTPOST_ASC, 0, 0)); $this->assertCount(4, $summaries); $this->assertEquals($discussion3->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($discussion1->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($hiddendiscussion->id, $summaries[3]->get_discussion()->get_id()); // Sort discussions by replies DESC. $summaries = array_values($vault->get_from_forum_id($forum->id, true, null, $vault::SORTORDER_REPLIES_DESC, 0, 0)); $this->assertCount(4, $summaries); $this->assertEquals($discussion2->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($discussion1->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($hiddendiscussion->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($discussion3->id, $summaries[3]->get_discussion()->get_id()); // Sort discussions by replies ASC. $summaries = array_values($vault->get_from_forum_id($forum->id, true, null, $vault::SORTORDER_REPLIES_ASC, 0, 0)); $this->assertCount(4, $summaries); $this->assertEquals($discussion3->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($hiddendiscussion->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($discussion1->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[3]->get_discussion()->get_id()); // Sort discussions by discussion created DESC. $summaries = array_values($vault->get_from_forum_id($forum->id, true, null, $vault::SORTORDER_CREATED_DESC, 0, 0)); $this->assertCount(4, $summaries); $this->assertEquals($discussion3->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($hiddendiscussion->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($discussion1->id, $summaries[3]->get_discussion()->get_id()); // Sort discussions by discussion created ASC. $summaries = array_values($vault->get_from_forum_id($forum->id, true, null, $vault::SORTORDER_CREATED_ASC, 0, 0)); $this->assertCount(4, $summaries); $this->assertEquals($discussion1->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($hiddendiscussion->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($discussion3->id, $summaries[3]->get_discussion()->get_id()); // Sort discussions when there is a pinned discussion. $this->pin_discussion($discussion1); $summaries = array_values($vault->get_from_forum_id($forum->id, true, null, $vault::SORTORDER_LASTPOST_ASC, 0, 0)); $this->assertCount(4, $summaries); $this->assertEquals($discussion1->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($discussion3->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($hiddendiscussion->id, $summaries[3]->get_discussion()->get_id()); $summaries = array_values($vault->get_from_forum_id($forum->id, true, null, $vault::SORTORDER_LASTPOST_DESC, 0, 0)); $this->assertCount(4, $summaries); $this->assertEquals($discussion1->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($hiddendiscussion->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($discussion3->id, $summaries[3]->get_discussion()->get_id()); $summaries = array_values($vault->get_from_forum_id($forum->id, true, null, $vault::SORTORDER_REPLIES_DESC, 0, 0)); $this->assertCount(4, $summaries); $this->assertEquals($discussion1->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($hiddendiscussion->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($discussion3->id, $summaries[3]->get_discussion()->get_id()); // Sort discussions where there is a pinned discussion and several starred discussions. $this->star_discussion($discussion3, 1); $this->star_discussion($hiddendiscussion, 1); $summaries = array_values($vault->get_from_forum_id($forum->id, true, $user->id, $vault::SORTORDER_REPLIES_DESC, 0, 0)); $this->assertCount(4, $summaries); $this->assertEquals($discussion1->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($hiddendiscussion->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($discussion3->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[3]->get_discussion()->get_id()); $summaries = array_values($vault->get_from_forum_id($forum->id, true, $user->id, $vault::SORTORDER_REPLIES_ASC, 0, 0)); $this->assertCount(4, $summaries); $this->assertEquals($discussion1->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($discussion3->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($hiddendiscussion->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[3]->get_discussion()->get_id()); } /** * Test get_from_forum_id_and_group_id. */ public function test_get_from_forum_id_and_group_id(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $vault = $this->vault; $user = $datagenerator->create_user(); self::setUser($user); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); $this->getDataGenerator()->enrol_user($user->id, $course->id, null, 'manual'); $this->assertEquals([], $vault->get_from_forum_id_and_group_id($forum->id, [1, 2, 3], true, $user->id, null, 0, 0)); $now = time(); [$discussion1, $post1] = $this->helper_post_to_forum($forum, $user, ['timestart' => $now - 10, 'timemodified' => $now + 1]); [$discussion2, $post2] = $this->helper_post_to_forum($forum, $user, ['timestart' => $now - 9, 'timemodified' => $now + 2]); [$hiddendiscussion, $post3] = $this->helper_post_to_forum( $forum, $user, ['timestart' => $now + 10, 'timemodified' => $now + 3] ); [$groupdiscussion1, $post4] = $this->helper_post_to_forum( $forum, $user, ['timestart' => $now - 8, 'timemodified' => $now + 4, 'groupid' => 1] ); [$groupdiscussion2, $post5] = $this->helper_post_to_forum( $forum, $user, ['timestart' => $now - 7, 'timemodified' => $now + 5, 'groupid' => 2] ); [$hiddengroupdiscussion, $post6] = $this->helper_post_to_forum( $forum, $user, ['timestart' => $now + 11, 'timemodified' => $now + 6, 'groupid' => 3] ); $summaries = array_values($vault->get_from_forum_id_and_group_id($forum->id, [1, 2, 3], true, null, null, 0, 0)); $this->assertCount(6, $summaries); $this->assertEquals($hiddengroupdiscussion->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($hiddendiscussion->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($groupdiscussion2->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($groupdiscussion1->id, $summaries[3]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[4]->get_discussion()->get_id()); $this->assertEquals($discussion1->id, $summaries[5]->get_discussion()->get_id()); $summaries = array_values($vault->get_from_forum_id_and_group_id($forum->id, [1, 2, 3], false, $user->id, null, 0, 0)); $this->assertCount(6, $summaries); $this->assertEquals($hiddengroupdiscussion->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($hiddendiscussion->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($groupdiscussion2->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($groupdiscussion1->id, $summaries[3]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[4]->get_discussion()->get_id()); $this->assertEquals($discussion1->id, $summaries[5]->get_discussion()->get_id()); $summaries = array_values($vault->get_from_forum_id_and_group_id($forum->id, [1, 2, 3], true, null, null, 1, 0)); $this->assertCount(1, $summaries); $this->assertEquals($hiddengroupdiscussion->id, $summaries[0]->get_discussion()->get_id()); $summaries = array_values($vault->get_from_forum_id_and_group_id($forum->id, [1, 2, 3], true, null, null, 1, 1)); $this->assertCount(1, $summaries); $this->assertEquals($hiddendiscussion->id, $summaries[0]->get_discussion()->get_id()); $summaries = array_values($vault->get_from_forum_id_and_group_id($forum->id, [1, 2, 3], true, null, null, 1, 2)); $this->assertCount(1, $summaries); $this->assertEquals($groupdiscussion2->id, $summaries[0]->get_discussion()->get_id()); $summaries = array_values($vault->get_from_forum_id_and_group_id($forum->id, [1, 2, 3], false, null, null, 0, 0)); $this->assertCount(4, $summaries); $this->assertEquals($groupdiscussion2->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($groupdiscussion1->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($discussion1->id, $summaries[3]->get_discussion()->get_id()); $summaries = array_values($vault->get_from_forum_id_and_group_id($forum->id, [], true, null, null, 0, 0)); $this->assertCount(3, $summaries); $this->assertEquals($hiddendiscussion->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($discussion1->id, $summaries[2]->get_discussion()->get_id()); // Add 4 replies to $post1. $this->helper_reply_to_post($post1, $user); $this->helper_reply_to_post($post1, $user); $this->helper_reply_to_post($post1, $user); $this->helper_reply_to_post($post1, $user); // Add 5 replies to $post2. $this->helper_reply_to_post($post2, $user); $this->helper_reply_to_post($post2, $user); $this->helper_reply_to_post($post2, $user); $this->helper_reply_to_post($post2, $user); $this->helper_reply_to_post($post2, $user); // Add 3 replies to $post3. $this->helper_reply_to_post($post3, $user); $this->helper_reply_to_post($post3, $user); $this->helper_reply_to_post($post3, $user); // Add 2 replies to $post4. $this->helper_reply_to_post($post4, $user); $this->helper_reply_to_post($post4, $user); // Add 1 reply to $post5. $this->helper_reply_to_post($post5, $user); // Sort discussions by last post DESC. $summaries = array_values($vault->get_from_forum_id_and_group_id($forum->id, [1, 2, 3], true, $user->id, $vault::SORTORDER_LASTPOST_DESC, 0, 0)); $this->assertCount(6, $summaries); $this->assertEquals($hiddengroupdiscussion->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($hiddendiscussion->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($groupdiscussion2->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($groupdiscussion1->id, $summaries[3]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[4]->get_discussion()->get_id()); $this->assertEquals($discussion1->id, $summaries[5]->get_discussion()->get_id()); // Sort discussions by last post ASC. $summaries = array_values($vault->get_from_forum_id_and_group_id($forum->id, [1, 2, 3], true, $user->id, $vault::SORTORDER_LASTPOST_ASC, 0, 0)); $this->assertCount(6, $summaries); $this->assertEquals($discussion1->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($groupdiscussion1->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($groupdiscussion2->id, $summaries[3]->get_discussion()->get_id()); $this->assertEquals($hiddendiscussion->id, $summaries[4]->get_discussion()->get_id()); $this->assertEquals($hiddengroupdiscussion->id, $summaries[5]->get_discussion()->get_id()); // Sort discussions by replies DESC. $summaries = array_values($vault->get_from_forum_id_and_group_id($forum->id, [1, 2, 3], true, $user->id, $vault::SORTORDER_REPLIES_DESC, 0, 0)); $this->assertCount(6, $summaries); $this->assertEquals($discussion2->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($discussion1->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($hiddendiscussion->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($groupdiscussion1->id, $summaries[3]->get_discussion()->get_id()); $this->assertEquals($groupdiscussion2->id, $summaries[4]->get_discussion()->get_id()); $this->assertEquals($hiddengroupdiscussion->id, $summaries[5]->get_discussion()->get_id()); // Sort discussions by replies ASC. $summaries = array_values($vault->get_from_forum_id_and_group_id($forum->id, [1, 2, 3], true, $user->id, $vault::SORTORDER_REPLIES_ASC, 0, 0)); $this->assertCount(6, $summaries); $this->assertEquals($hiddengroupdiscussion->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($groupdiscussion2->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($groupdiscussion1->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($hiddendiscussion->id, $summaries[3]->get_discussion()->get_id()); $this->assertEquals($discussion1->id, $summaries[4]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[5]->get_discussion()->get_id()); // Sort discussions by discussion created DESC. $summaries = array_values($vault->get_from_forum_id_and_group_id($forum->id, [1, 2, 3], true, $user->id, $vault::SORTORDER_CREATED_DESC, 0, 0)); $this->assertCount(6, $summaries); $this->assertEquals($hiddengroupdiscussion->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($groupdiscussion2->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($groupdiscussion1->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($hiddendiscussion->id, $summaries[3]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[4]->get_discussion()->get_id()); $this->assertEquals($discussion1->id, $summaries[5]->get_discussion()->get_id()); // Sort discussions by discussion created ASC. $summaries = array_values($vault->get_from_forum_id_and_group_id($forum->id, [1, 2, 3], true, $user->id, $vault::SORTORDER_CREATED_ASC, 0, 0)); $this->assertCount(6, $summaries); $this->assertEquals($discussion1->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($hiddendiscussion->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($groupdiscussion1->id, $summaries[3]->get_discussion()->get_id()); $this->assertEquals($groupdiscussion2->id, $summaries[4]->get_discussion()->get_id()); $this->assertEquals($hiddengroupdiscussion->id, $summaries[5]->get_discussion()->get_id()); // Sort discussions when there is a pinned discussion. $this->pin_discussion($discussion1); $this->pin_discussion($hiddendiscussion); $summaries = array_values($vault->get_from_forum_id_and_group_id($forum->id, [1, 2, 3], true, null, $vault::SORTORDER_LASTPOST_DESC, 0, 0)); $this->assertCount(6, $summaries); $this->assertEquals($hiddendiscussion->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($discussion1->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($hiddengroupdiscussion->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($groupdiscussion2->id, $summaries[3]->get_discussion()->get_id()); $this->assertEquals($groupdiscussion1->id, $summaries[4]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[5]->get_discussion()->get_id()); $summaries = array_values($vault->get_from_forum_id_and_group_id($forum->id, [1, 2, 3], true, null, $vault::SORTORDER_LASTPOST_ASC, 0, 0)); $this->assertCount(6, $summaries); $this->assertEquals($discussion1->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($hiddendiscussion->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($groupdiscussion1->id, $summaries[3]->get_discussion()->get_id()); $this->assertEquals($groupdiscussion2->id, $summaries[4]->get_discussion()->get_id()); $this->assertEquals($hiddengroupdiscussion->id, $summaries[5]->get_discussion()->get_id()); $summaries = array_values($vault->get_from_forum_id_and_group_id($forum->id, [1, 2, 3], true, null, $vault::SORTORDER_REPLIES_DESC, 0, 0)); $this->assertCount(6, $summaries); $this->assertEquals($discussion1->id, $summaries[0]->get_discussion()->get_id()); $this->assertEquals($hiddendiscussion->id, $summaries[1]->get_discussion()->get_id()); $this->assertEquals($discussion2->id, $summaries[2]->get_discussion()->get_id()); $this->assertEquals($groupdiscussion1->id, $summaries[3]->get_discussion()->get_id()); $this->assertEquals($groupdiscussion2->id, $summaries[4]->get_discussion()->get_id()); $this->assertEquals($hiddengroupdiscussion->id, $summaries[5]->get_discussion()->get_id()); } /** * Test get_total_discussion_count_from_forum_id. */ public function test_get_total_discussion_count_from_forum_id(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $vault = $this->vault; $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); $this->assertEquals(0, $vault->get_total_discussion_count_from_forum_id($forum->id, true, null)); $now = time(); [$discussion1, $post1] = $this->helper_post_to_forum($forum, $user, ['timestart' => $now - 10, 'timemodified' => 1]); [$discussion2, $post2] = $this->helper_post_to_forum($forum, $user, ['timestart' => $now - 9, 'timemodified' => 2]); [$hiddendiscussion, $post3] = $this->helper_post_to_forum($forum, $user, ['timestart' => $now + 10, 'timemodified' => 3]); $this->assertEquals(2, $vault->get_total_discussion_count_from_forum_id($forum->id, false, null)); $this->assertEquals(3, $vault->get_total_discussion_count_from_forum_id($forum->id, true, null)); $this->assertEquals(3, $vault->get_total_discussion_count_from_forum_id($forum->id, false, $user->id)); } /** * Test get_total_discussion_count_from_forum_id_and_group_id. */ public function test_get_total_discussion_count_from_forum_id_and_group_id(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $vault = $this->vault; $user = $datagenerator->create_user(); self::setUser($user); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); $this->assertEquals([], $vault->get_from_forum_id($forum->id, true, null, null, 0, 0)); $now = time(); [$discussion1, $post1] = $this->helper_post_to_forum($forum, $user, ['timestart' => $now - 10, 'timemodified' => 1]); [$discussion2, $post2] = $this->helper_post_to_forum($forum, $user, ['timestart' => $now - 9, 'timemodified' => 2]); [$hiddendiscussion, $post3] = $this->helper_post_to_forum($forum, $user, ['timestart' => $now + 10, 'timemodified' => 3]); [$groupdiscussion1, $post4] = $this->helper_post_to_forum( $forum, $user, ['timestart' => $now - 8, 'timemodified' => 4, 'groupid' => 1] ); [$groupdiscussion2, $post5] = $this->helper_post_to_forum( $forum, $user, ['timestart' => $now - 7, 'timemodified' => 5, 'groupid' => 2] ); [$hiddengroupdiscussion, $post6] = $this->helper_post_to_forum( $forum, $user, ['timestart' => $now + 11, 'timemodified' => 6, 'groupid' => 3] ); $this->assertEquals(6, $vault->get_total_discussion_count_from_forum_id_and_group_id($forum->id, [1, 2, 3], true, null)); $this->assertEquals(6, $vault->get_total_discussion_count_from_forum_id_and_group_id( $forum->id, [1, 2, 3], false, $user->id )); $this->assertEquals(4, $vault->get_total_discussion_count_from_forum_id_and_group_id($forum->id, [1, 2, 3], false, null)); $this->assertEquals(3, $vault->get_total_discussion_count_from_forum_id_and_group_id($forum->id, [], true, null)); } /** * Pin a duscussion. * * @param \stdClass $discussion */ private function pin_discussion(\stdClass $discussion) { global $DB; $DB->update_record('forum_discussions', (object) array('id' => $discussion->id, 'pinned' => FORUM_DISCUSSION_PINNED)); } /** * Star a duscussion. * * @param \stdClass $discussion * @param bool $targetstate The new starred state of the discussion (0 => unstar, 1 => star) */ private function star_discussion(\stdClass $discussion, bool $targetstate) { mod_forum_external::toggle_favourite_state($discussion->id, $targetstate); } } PK-B\l tests/vaults_author_test.phpnu[. namespace mod_forum; /** * The author vault tests. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class vaults_author_test extends \advanced_testcase { /** * Test get_from_id. */ public function test_get_from_id(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $vaultfactory = \mod_forum\local\container::get_vault_factory(); $authorvault = $vaultfactory->get_author_vault(); $author = $authorvault->get_from_id($user->id); $this->assertEquals($user->id, $author->get_id()); } /** * Test get_context_ids_for_author_ids. */ public function test_get_context_ids_for_author_ids(): void { $this->resetAfterTest(); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); $userid1 = $user1->id; $userid2 = $user2->id; $userid3 = $user3->id; $fakeuserid = $user3->id + 1000; $vaultfactory = \mod_forum\local\container::get_vault_factory(); $authorvault = $vaultfactory->get_author_vault(); $user1context = \context_user::instance($user1->id); $user2context = \context_user::instance($user2->id); $user3context = \context_user::instance($user3->id); $user1contextid = $user1context->id; $user2contextid = $user2context->id; $user3contextid = $user3context->id; $fakeusercontextid = null; $userids = [$userid1, $userid2, $userid3, $fakeuserid]; $expected = [ $userid1 => $user1contextid, $userid2 => $user2contextid, $userid3 => $user3contextid, $fakeuserid => $fakeusercontextid ]; $this->assertEquals($expected, $authorvault->get_context_ids_for_author_ids($userids)); } } PK-B\w((tests/exporters_author_test.phpnu[. namespace mod_forum; use mod_forum\local\entities\author as author_entity; use mod_forum\local\exporters\author as author_exporter; /** * The author exporter tests. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class exporters_author_test extends \advanced_testcase { /** * Test the export function returns expected values. */ public function test_export_author(): void { global $PAGE; $this->resetAfterTest(); $renderer = $PAGE->get_renderer('core'); $datagenerator = $this->getDataGenerator(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); $coursemodule = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($coursemodule->id); $entityfactory = \mod_forum\local\container::get_entity_factory(); $forum = $entityfactory->get_forum_from_stdClass($forum, $context, $coursemodule, $course); $author = new author_entity( 1, 1, 'test', 'user', 'test user', 'test@example.com', false ); $exporter = new author_exporter($author, 1, [], true, [ 'urlfactory' => \mod_forum\local\container::get_url_factory(), 'context' => $context, 'forum' => $forum, ]); $exportedauthor = $exporter->export($renderer); $this->assertEquals(1, $exportedauthor->id); $this->assertEquals('test user', $exportedauthor->fullname); $this->assertEquals([], $exportedauthor->groups); $this->assertNotEquals(null, $exportedauthor->urls['profile']); $this->assertNotEquals(null, $exportedauthor->urls['profileimage']); } /** * Test the export function with groups. */ public function test_export_author_with_groups(): void { global $PAGE; $this->resetAfterTest(); $renderer = $PAGE->get_renderer('core'); $datagenerator = $this->getDataGenerator(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); $coursemodule = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($coursemodule->id); $entityfactory = \mod_forum\local\container::get_entity_factory(); $forum = $entityfactory->get_forum_from_stdClass($forum, $context, $coursemodule, $course); $author = new author_entity( 1, 1, 'test', 'user', 'test user', 'test@example.com', false ); $group = $datagenerator->create_group(['courseid' => $course->id]); $exporter = new author_exporter($author, 1, [$group], true, [ 'urlfactory' => \mod_forum\local\container::get_url_factory(), 'context' => $context, 'forum' => $forum, ]); $exportedauthor = $exporter->export($renderer); $this->assertCount(1, $exportedauthor->groups); $this->assertEquals($group->id, $exportedauthor->groups[0]['id']); } /** * Test the export function with no view capability. */ public function test_export_author_no_view_capability(): void { global $PAGE; $this->resetAfterTest(); $renderer = $PAGE->get_renderer('core'); $datagenerator = $this->getDataGenerator(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); $coursemodule = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($coursemodule->id); $entityfactory = \mod_forum\local\container::get_entity_factory(); $forum = $entityfactory->get_forum_from_stdClass($forum, $context, $coursemodule, $course); $author = new author_entity( 1, 1, 'test', 'user', 'test user', 'test@example.com', false ); $group = $datagenerator->create_group(['courseid' => $course->id]); $exporter = new author_exporter($author, 1, [$group], false, [ 'urlfactory' => \mod_forum\local\container::get_url_factory(), 'context' => $context, 'forum' => $forum, ]); $exportedauthor = $exporter->export($renderer); $this->assertEquals(null, $exportedauthor->id); $this->assertNotEquals('test user', $exportedauthor->fullname); $this->assertEquals([], $exportedauthor->groups); $this->assertEquals(null, $exportedauthor->urls['profile']); $this->assertEquals(null, $exportedauthor->urls['profileimage']); } } PK-B\f!!#tests/exporters_discussion_test.phpnu[. namespace mod_forum; use mod_forum\local\entities\discussion as discussion_entity; use mod_forum\local\exporters\discussion as discussion_exporter; /** * The discussion exporter tests. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class exporters_discussion_test extends \advanced_testcase { /** @var \mod_forum\local\builders\exported_posts */ private $builder; /** * Test set up function. */ public function setUp(): void { parent::setUp(); // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); $builderfactory = \mod_forum\local\container::get_builder_factory(); $this->builder = $builderfactory->get_exported_posts_builder(); } /** * Test tear down function. */ public function tearDown(): void { // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); parent::tearDown(); } /** * Test the export function returns expected values. */ public function test_export(): void { global $PAGE; $this->resetAfterTest(); $renderer = $PAGE->get_renderer('core'); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); $coursemodule = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($coursemodule->id); $entityfactory = \mod_forum\local\container::get_entity_factory(); $forum = $entityfactory->get_forum_from_stdClass($forum, $context, $coursemodule, $course); $group = $datagenerator->create_group(['courseid' => $course->id]); $now = time(); $discussion = new discussion_entity( 1, $course->id, $forum->get_id(), 'test discussion', 1, $user->id, $group->id, false, $now, $now, 0, 0, false, 0 ); $exporter = new discussion_exporter($discussion, [ 'legacydatamapperfactory' => \mod_forum\local\container::get_legacy_data_mapper_factory(), 'urlfactory' => \mod_forum\local\container::get_url_factory(), 'capabilitymanager' => (\mod_forum\local\container::get_manager_factory())->get_capability_manager($forum), 'context' => $context, 'forum' => $forum, 'user' => $user, 'groupsbyid' => [$group->id => $group], 'latestpostid' => 7 ]); $exporteddiscussion = $exporter->export($renderer); $this->assertEquals(1, $exporteddiscussion->id); $this->assertEquals($forum->get_id(), $exporteddiscussion->forumid); $this->assertEquals(false, $exporteddiscussion->pinned); $this->assertEquals('test discussion', $exporteddiscussion->name); $this->assertEquals($now, $exporteddiscussion->times['modified']); $this->assertEquals(0, $exporteddiscussion->times['start']); $this->assertEquals(0, $exporteddiscussion->times['end']); $this->assertEquals($group->name, $exporteddiscussion->group['name']); } } PK-B\<tests/lib_test.phpnu[. namespace mod_forum; use mod_forum_generator; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/forum/lib.php'); require_once($CFG->dirroot . '/mod/forum/locallib.php'); require_once($CFG->dirroot . '/rating/lib.php'); /** * The mod_forum lib.php tests. * * @package mod_forum * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class lib_test extends \advanced_testcase { public function setUp(): void { parent::setUp(); // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); } public function tearDown(): void { // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); parent::tearDown(); } public function test_forum_trigger_content_uploaded_event(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $context = \context_module::instance($forum->cmid); $this->setUser($user->id); $fakepost = (object) array('id' => 123, 'message' => 'Yay!', 'discussion' => 100); $cm = get_coursemodule_from_instance('forum', $forum->id); $fs = get_file_storage(); $dummy = (object) array( 'contextid' => $context->id, 'component' => 'mod_forum', 'filearea' => 'attachment', 'itemid' => $fakepost->id, 'filepath' => '/', 'filename' => 'myassignmnent.pdf' ); $fi = $fs->create_file_from_string($dummy, 'Content of ' . $dummy->filename); $data = new \stdClass(); $sink = $this->redirectEvents(); forum_trigger_content_uploaded_event($fakepost, $cm, 'some triggered from value'); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\mod_forum\event\assessable_uploaded', $event); $this->assertEquals($context->id, $event->contextid); $this->assertEquals($fakepost->id, $event->objectid); $this->assertEquals($fakepost->message, $event->other['content']); $this->assertEquals($fakepost->discussion, $event->other['discussionid']); $this->assertCount(1, $event->other['pathnamehashes']); $this->assertEquals($fi->get_pathnamehash(), $event->other['pathnamehashes'][0]); $expected = new \stdClass(); $expected->modulename = 'forum'; $expected->name = 'some triggered from value'; $expected->cmid = $forum->cmid; $expected->itemid = $fakepost->id; $expected->courseid = $course->id; $expected->userid = $user->id; $expected->content = $fakepost->message; $expected->pathnamehashes = array($fi->get_pathnamehash()); $this->assertEventContextNotUsed($event); } public function test_forum_get_courses_user_posted_in(): void { $this->resetAfterTest(); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $course3 = $this->getDataGenerator()->create_course(); // Create 3 forums, one in each course. $record = new \stdClass(); $record->course = $course1->id; $forum1 = $this->getDataGenerator()->create_module('forum', $record); $record = new \stdClass(); $record->course = $course2->id; $forum2 = $this->getDataGenerator()->create_module('forum', $record); $record = new \stdClass(); $record->course = $course3->id; $forum3 = $this->getDataGenerator()->create_module('forum', $record); // Add a second forum in course 1. $record = new \stdClass(); $record->course = $course1->id; $forum4 = $this->getDataGenerator()->create_module('forum', $record); // Add discussions to course 1 started by user1. $record = new \stdClass(); $record->course = $course1->id; $record->userid = $user1->id; $record->forum = $forum1->id; $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); $record = new \stdClass(); $record->course = $course1->id; $record->userid = $user1->id; $record->forum = $forum4->id; $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // Add discussions to course2 started by user1. $record = new \stdClass(); $record->course = $course2->id; $record->userid = $user1->id; $record->forum = $forum2->id; $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // Add discussions to course 3 started by user2. $record = new \stdClass(); $record->course = $course3->id; $record->userid = $user2->id; $record->forum = $forum3->id; $discussion3 = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // Add post to course 3 by user1. $record = new \stdClass(); $record->course = $course3->id; $record->userid = $user1->id; $record->forum = $forum3->id; $record->discussion = $discussion3->id; $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); // User 3 hasn't posted anything, so shouldn't get any results. $user3courses = forum_get_courses_user_posted_in($user3); $this->assertEmpty($user3courses); // User 2 has only posted in course3. $user2courses = forum_get_courses_user_posted_in($user2); $this->assertCount(1, $user2courses); $user2course = array_shift($user2courses); $this->assertEquals($course3->id, $user2course->id); $this->assertEquals($course3->shortname, $user2course->shortname); // User 1 has posted in all 3 courses. $user1courses = forum_get_courses_user_posted_in($user1); $this->assertCount(3, $user1courses); foreach ($user1courses as $course) { $this->assertContains($course->id, array($course1->id, $course2->id, $course3->id)); $this->assertContains($course->shortname, array($course1->shortname, $course2->shortname, $course3->shortname)); } // User 1 has only started a discussion in course 1 and 2 though. $user1courses = forum_get_courses_user_posted_in($user1, true); $this->assertCount(2, $user1courses); foreach ($user1courses as $course) { $this->assertContains($course->id, array($course1->id, $course2->id)); $this->assertContains($course->shortname, array($course1->shortname, $course2->shortname)); } } /** * Test the logic in the forum_tp_can_track_forums() function. */ public function test_forum_tp_can_track_forums(): void { global $CFG; $this->resetAfterTest(); $useron = $this->getDataGenerator()->create_user(array('trackforums' => 1)); $useroff = $this->getDataGenerator()->create_user(array('trackforums' => 0)); $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'trackingtype' => FORUM_TRACKING_OFF); // Off. $forumoff = $this->getDataGenerator()->create_module('forum', $options); $options = array('course' => $course->id, 'trackingtype' => FORUM_TRACKING_FORCED); // On. $forumforce = $this->getDataGenerator()->create_module('forum', $options); $options = array('course' => $course->id, 'trackingtype' => FORUM_TRACKING_OPTIONAL); // Optional. $forumoptional = $this->getDataGenerator()->create_module('forum', $options); // Allow force. $CFG->forum_allowforcedreadtracking = 1; // User on, forum off, should be off. $result = forum_tp_can_track_forums($forumoff, $useron); $this->assertEquals(false, $result); // User on, forum on, should be on. $result = forum_tp_can_track_forums($forumforce, $useron); $this->assertEquals(true, $result); // User on, forum optional, should be on. $result = forum_tp_can_track_forums($forumoptional, $useron); $this->assertEquals(true, $result); // User off, forum off, should be off. $result = forum_tp_can_track_forums($forumoff, $useroff); $this->assertEquals(false, $result); // User off, forum force, should be on. $result = forum_tp_can_track_forums($forumforce, $useroff); $this->assertEquals(true, $result); // User off, forum optional, should be off. $result = forum_tp_can_track_forums($forumoptional, $useroff); $this->assertEquals(false, $result); // Don't allow force. $CFG->forum_allowforcedreadtracking = 0; // User on, forum off, should be off. $result = forum_tp_can_track_forums($forumoff, $useron); $this->assertEquals(false, $result); // User on, forum on, should be on. $result = forum_tp_can_track_forums($forumforce, $useron); $this->assertEquals(true, $result); // User on, forum optional, should be on. $result = forum_tp_can_track_forums($forumoptional, $useron); $this->assertEquals(true, $result); // User off, forum off, should be off. $result = forum_tp_can_track_forums($forumoff, $useroff); $this->assertEquals(false, $result); // User off, forum force, should be off. $result = forum_tp_can_track_forums($forumforce, $useroff); $this->assertEquals(false, $result); // User off, forum optional, should be off. $result = forum_tp_can_track_forums($forumoptional, $useroff); $this->assertEquals(false, $result); } /** * Test the logic in the test_forum_tp_is_tracked() function. */ public function test_forum_tp_is_tracked(): void { global $CFG; $this->resetAfterTest(); $cache = \cache::make('mod_forum', 'forum_is_tracked'); $useron = $this->getDataGenerator()->create_user(array('trackforums' => 1)); $useroff = $this->getDataGenerator()->create_user(array('trackforums' => 0)); $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'trackingtype' => FORUM_TRACKING_OFF); // Off. $forumoff = $this->getDataGenerator()->create_module('forum', $options); $options = array('course' => $course->id, 'trackingtype' => FORUM_TRACKING_FORCED); // On. $forumforce = $this->getDataGenerator()->create_module('forum', $options); $options = array('course' => $course->id, 'trackingtype' => FORUM_TRACKING_OPTIONAL); // Optional. $forumoptional = $this->getDataGenerator()->create_module('forum', $options); // Allow force. $CFG->forum_allowforcedreadtracking = 1; // User on, forum off, should be off. $result = forum_tp_is_tracked($forumoff, $useron); $this->assertEquals(false, $result); // User on, forum force, should be on. $result = forum_tp_is_tracked($forumforce, $useron); $this->assertEquals(true, $result); // User on, forum optional, should be on. $result = forum_tp_is_tracked($forumoptional, $useron); $this->assertEquals(true, $result); // User off, forum off, should be off. $result = forum_tp_is_tracked($forumoff, $useroff); $this->assertEquals(false, $result); // User off, forum force, should be on. $result = forum_tp_is_tracked($forumforce, $useroff); $this->assertEquals(true, $result); // User off, forum optional, should be off. $result = forum_tp_is_tracked($forumoptional, $useroff); $this->assertEquals(false, $result); $cache->purge(); // Don't allow force. $CFG->forum_allowforcedreadtracking = 0; // User on, forum off, should be off. $result = forum_tp_is_tracked($forumoff, $useron); $this->assertEquals(false, $result); // User on, forum force, should be on. $result = forum_tp_is_tracked($forumforce, $useron); $this->assertEquals(true, $result); // User on, forum optional, should be on. $result = forum_tp_is_tracked($forumoptional, $useron); $this->assertEquals(true, $result); // User off, forum off, should be off. $result = forum_tp_is_tracked($forumoff, $useroff); $this->assertEquals(false, $result); // User off, forum force, should be off. $result = forum_tp_is_tracked($forumforce, $useroff); $this->assertEquals(false, $result); // User off, forum optional, should be off. $result = forum_tp_is_tracked($forumoptional, $useroff); $this->assertEquals(false, $result); // Stop tracking so we can test again. forum_tp_stop_tracking($forumforce->id, $useron->id); forum_tp_stop_tracking($forumoptional->id, $useron->id); forum_tp_stop_tracking($forumforce->id, $useroff->id); forum_tp_stop_tracking($forumoptional->id, $useroff->id); $cache->purge(); // Allow force. $CFG->forum_allowforcedreadtracking = 1; // User on, preference off, forum force, should be on. $result = forum_tp_is_tracked($forumforce, $useron); $this->assertEquals(true, $result); // User on, preference off, forum optional, should be on. $result = forum_tp_is_tracked($forumoptional, $useron); $this->assertEquals(false, $result); // User off, preference off, forum force, should be on. $result = forum_tp_is_tracked($forumforce, $useroff); $this->assertEquals(true, $result); // User off, preference off, forum optional, should be off. $result = forum_tp_is_tracked($forumoptional, $useroff); $this->assertEquals(false, $result); $cache->purge(); // Don't allow force. $CFG->forum_allowforcedreadtracking = 0; // User on, preference off, forum force, should be on. $result = forum_tp_is_tracked($forumforce, $useron); $this->assertEquals(false, $result); // User on, preference off, forum optional, should be on. $result = forum_tp_is_tracked($forumoptional, $useron); $this->assertEquals(false, $result); // User off, preference off, forum force, should be off. $result = forum_tp_is_tracked($forumforce, $useroff); $this->assertEquals(false, $result); // User off, preference off, forum optional, should be off. $result = forum_tp_is_tracked($forumoptional, $useroff); $this->assertEquals(false, $result); } /** * Test the logic in the forum_tp_get_course_unread_posts() function. */ public function test_forum_tp_get_course_unread_posts(): void { global $CFG; $this->resetAfterTest(); $useron = $this->getDataGenerator()->create_user(array('trackforums' => 1)); $useroff = $this->getDataGenerator()->create_user(array('trackforums' => 0)); $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'trackingtype' => FORUM_TRACKING_OFF); // Off. $forumoff = $this->getDataGenerator()->create_module('forum', $options); $options = array('course' => $course->id, 'trackingtype' => FORUM_TRACKING_FORCED); // On. $forumforce = $this->getDataGenerator()->create_module('forum', $options); $options = array('course' => $course->id, 'trackingtype' => FORUM_TRACKING_OPTIONAL); // Optional. $forumoptional = $this->getDataGenerator()->create_module('forum', $options); // Add discussions to the tracking off forum. $record = new \stdClass(); $record->course = $course->id; $record->userid = $useron->id; $record->forum = $forumoff->id; $discussionoff = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // Add discussions to the tracking forced forum. $record = new \stdClass(); $record->course = $course->id; $record->userid = $useron->id; $record->forum = $forumforce->id; $discussionforce = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // Add post to the tracking forced discussion. $record = new \stdClass(); $record->course = $course->id; $record->userid = $useroff->id; $record->forum = $forumforce->id; $record->discussion = $discussionforce->id; $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); // Add discussions to the tracking optional forum. $record = new \stdClass(); $record->course = $course->id; $record->userid = $useron->id; $record->forum = $forumoptional->id; $discussionoptional = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // Allow force. $CFG->forum_allowforcedreadtracking = 1; $result = forum_tp_get_course_unread_posts($useron->id, $course->id); $this->assertEquals(2, count($result)); $this->assertEquals(false, isset($result[$forumoff->id])); $this->assertEquals(true, isset($result[$forumforce->id])); $this->assertEquals(2, $result[$forumforce->id]->unread); $this->assertEquals(true, isset($result[$forumoptional->id])); $this->assertEquals(1, $result[$forumoptional->id]->unread); $result = forum_tp_get_course_unread_posts($useroff->id, $course->id); $this->assertEquals(1, count($result)); $this->assertEquals(false, isset($result[$forumoff->id])); $this->assertEquals(true, isset($result[$forumforce->id])); $this->assertEquals(2, $result[$forumforce->id]->unread); $this->assertEquals(false, isset($result[$forumoptional->id])); // Don't allow force. $CFG->forum_allowforcedreadtracking = 0; $result = forum_tp_get_course_unread_posts($useron->id, $course->id); $this->assertEquals(2, count($result)); $this->assertEquals(false, isset($result[$forumoff->id])); $this->assertEquals(true, isset($result[$forumforce->id])); $this->assertEquals(2, $result[$forumforce->id]->unread); $this->assertEquals(true, isset($result[$forumoptional->id])); $this->assertEquals(1, $result[$forumoptional->id]->unread); $result = forum_tp_get_course_unread_posts($useroff->id, $course->id); $this->assertEquals(0, count($result)); $this->assertEquals(false, isset($result[$forumoff->id])); $this->assertEquals(false, isset($result[$forumforce->id])); $this->assertEquals(false, isset($result[$forumoptional->id])); // Stop tracking so we can test again. forum_tp_stop_tracking($forumforce->id, $useron->id); forum_tp_stop_tracking($forumoptional->id, $useron->id); forum_tp_stop_tracking($forumforce->id, $useroff->id); forum_tp_stop_tracking($forumoptional->id, $useroff->id); // Allow force. $CFG->forum_allowforcedreadtracking = 1; $result = forum_tp_get_course_unread_posts($useron->id, $course->id); $this->assertEquals(1, count($result)); $this->assertEquals(false, isset($result[$forumoff->id])); $this->assertEquals(true, isset($result[$forumforce->id])); $this->assertEquals(2, $result[$forumforce->id]->unread); $this->assertEquals(false, isset($result[$forumoptional->id])); $result = forum_tp_get_course_unread_posts($useroff->id, $course->id); $this->assertEquals(1, count($result)); $this->assertEquals(false, isset($result[$forumoff->id])); $this->assertEquals(true, isset($result[$forumforce->id])); $this->assertEquals(2, $result[$forumforce->id]->unread); $this->assertEquals(false, isset($result[$forumoptional->id])); // Don't allow force. $CFG->forum_allowforcedreadtracking = 0; $result = forum_tp_get_course_unread_posts($useron->id, $course->id); $this->assertEquals(0, count($result)); $this->assertEquals(false, isset($result[$forumoff->id])); $this->assertEquals(false, isset($result[$forumforce->id])); $this->assertEquals(false, isset($result[$forumoptional->id])); $result = forum_tp_get_course_unread_posts($useroff->id, $course->id); $this->assertEquals(0, count($result)); $this->assertEquals(false, isset($result[$forumoff->id])); $this->assertEquals(false, isset($result[$forumforce->id])); $this->assertEquals(false, isset($result[$forumoptional->id])); } /** * Test the logic in the forum_tp_get_course_unread_posts() function when private replies are present. * * @covers ::forum_tp_get_course_unread_posts */ public function test_forum_tp_get_course_unread_posts_with_private_replies(): void { global $DB; $this->resetAfterTest(); $generator = $this->getDataGenerator(); // Create 3 students. $s1 = $generator->create_user(['trackforums' => 1]); $s2 = $generator->create_user(['trackforums' => 1]); $s3 = $generator->create_user(['trackforums' => 1]); // Editing teacher. $t1 = $generator->create_user(['trackforums' => 1]); // Non-editing teacher. $t2 = $generator->create_user(['trackforums' => 1]); // Create our course. $course = $generator->create_course(); // Enrol editing and non-editing teachers. $generator->enrol_user($t1->id, $course->id, 'editingteacher'); $generator->enrol_user($t2->id, $course->id, 'teacher'); // Create forums. $forum1 = $generator->create_module('forum', ['course' => $course->id]); $forum2 = $generator->create_module('forum', ['course' => $course->id]); $forumgenerator = $generator->get_plugin_generator('mod_forum'); // Prevent the non-editing teacher from reading private replies in forum 2. $teacherroleid = $DB->get_field('role', 'id', ['shortname' => 'teacher']); $forum2cm = get_coursemodule_from_instance('forum', $forum2->id); $forum2context = \context_module::instance($forum2cm->id); role_change_permission($teacherroleid, $forum2context, 'mod/forum:readprivatereplies', CAP_PREVENT); // Create discussion by s1. $discussiondata = (object)[ 'course' => $course->id, 'forum' => $forum1->id, 'userid' => $s1->id, ]; $discussion1 = $forumgenerator->create_discussion($discussiondata); // Create discussion by s2. $discussiondata->userid = $s2->id; $discussion2 = $forumgenerator->create_discussion($discussiondata); // Create discussion by s3. $discussiondata->userid = $s3->id; $discussion3 = $forumgenerator->create_discussion($discussiondata); // Post a normal reply to s1's discussion in forum 1 as the editing teacher. $replydata = (object)[ 'course' => $course->id, 'forum' => $forum1->id, 'discussion' => $discussion1->id, 'userid' => $t1->id, ]; $forumgenerator->create_post($replydata); // Post a normal reply to s2's discussion as the editing teacher. $replydata->discussion = $discussion2->id; $forumgenerator->create_post($replydata); // Post a normal reply to s3's discussion as the editing teacher. $replydata->discussion = $discussion3->id; $forumgenerator->create_post($replydata); // Post a private reply to s1's discussion in forum 1 as the editing teacher. $replydata->discussion = $discussion1->id; $replydata->userid = $t1->id; $replydata->privatereplyto = $s1->id; $forumgenerator->create_post($replydata); // Post another private reply to s1 as the teacher. $forumgenerator->create_post($replydata); // Post a private reply to s2's discussion as the editing teacher. $replydata->discussion = $discussion2->id; $replydata->privatereplyto = $s2->id; $forumgenerator->create_post($replydata); // Create discussion by s1 in forum 2. $discussiondata->forum = $forum2->id; $discussiondata->userid = $s1->id; $discussion21 = $forumgenerator->create_discussion($discussiondata); // Post a private reply to s1's discussion in forum 2 as the editing teacher. $replydata->discussion = $discussion21->id; $replydata->privatereplyto = $s1->id; $forumgenerator->create_post($replydata); // Let's count! // S1 should see 8 unread posts 3 discussions posts + 2 private replies + 3 normal replies. $result = forum_tp_get_course_unread_posts($s1->id, $course->id); $unreadcounts = $result[$forum1->id]; $this->assertEquals(8, $unreadcounts->unread); // S2 should see 7 unread posts 3 discussions posts + 1 private reply + 3 normal replies. $result = forum_tp_get_course_unread_posts($s2->id, $course->id); $unreadcounts = $result[$forum1->id]; $this->assertEquals(7, $unreadcounts->unread); // S3 should see 6 unread posts 3 discussions posts + 3 normal replies. No private replies. $result = forum_tp_get_course_unread_posts($s3->id, $course->id); $unreadcounts = $result[$forum1->id]; $this->assertEquals(6, $unreadcounts->unread); // The editing teacher should see 9 unread posts in forum 1: 3 discussions posts + 3 normal replies + 3 private replies. $result = forum_tp_get_course_unread_posts($t1->id, $course->id); $unreadcounts = $result[$forum1->id]; $this->assertEquals(9, $unreadcounts->unread); // Same with the non-editing teacher, since they can read private replies by default. $result = forum_tp_get_course_unread_posts($t2->id, $course->id); $unreadcounts = $result[$forum1->id]; $this->assertEquals(9, $unreadcounts->unread); // But for forum 2, the non-editing teacher should only see 1 unread which is s1's discussion post. $unreadcounts = $result[$forum2->id]; $this->assertEquals(1, $unreadcounts->unread); } /** * Test the logic in the forum_tp_count_forum_unread_posts() function when private replies are present but without * separate group mode. This should yield the same results returned by forum_tp_get_course_unread_posts(). * * @covers ::forum_tp_count_forum_unread_posts */ public function test_forum_tp_count_forum_unread_posts_with_private_replies(): void { global $DB; $this->resetAfterTest(); $generator = $this->getDataGenerator(); // Create 3 students. $s1 = $generator->create_user(['username' => 's1', 'trackforums' => 1]); $s2 = $generator->create_user(['username' => 's2', 'trackforums' => 1]); $s3 = $generator->create_user(['username' => 's3', 'trackforums' => 1]); // Editing teacher. $t1 = $generator->create_user(['username' => 't1', 'trackforums' => 1]); // Non-editing teacher. $t2 = $generator->create_user(['username' => 't2', 'trackforums' => 1]); // Create our course. $course = $generator->create_course(); // Enrol editing and non-editing teachers. $generator->enrol_user($t1->id, $course->id, 'editingteacher'); $generator->enrol_user($t2->id, $course->id, 'teacher'); // Create forums. $forum1 = $generator->create_module('forum', ['course' => $course->id]); $forum2 = $generator->create_module('forum', ['course' => $course->id]); $forumgenerator = $generator->get_plugin_generator('mod_forum'); // Prevent the non-editing teacher from reading private replies in forum 2. $teacherroleid = $DB->get_field('role', 'id', ['shortname' => 'teacher']); $forum2cm = get_coursemodule_from_instance('forum', $forum2->id); $forum2context = \context_module::instance($forum2cm->id); role_change_permission($teacherroleid, $forum2context, 'mod/forum:readprivatereplies', CAP_PREVENT); // Create discussion by s1. $discussiondata = (object)[ 'course' => $course->id, 'forum' => $forum1->id, 'userid' => $s1->id, ]; $discussion1 = $forumgenerator->create_discussion($discussiondata); // Create discussion by s2. $discussiondata->userid = $s2->id; $discussion2 = $forumgenerator->create_discussion($discussiondata); // Create discussion by s3. $discussiondata->userid = $s3->id; $discussion3 = $forumgenerator->create_discussion($discussiondata); // Post a normal reply to s1's discussion in forum 1 as the editing teacher. $replydata = (object)[ 'course' => $course->id, 'forum' => $forum1->id, 'discussion' => $discussion1->id, 'userid' => $t1->id, ]; $forumgenerator->create_post($replydata); // Post a normal reply to s2's discussion as the editing teacher. $replydata->discussion = $discussion2->id; $forumgenerator->create_post($replydata); // Post a normal reply to s3's discussion as the editing teacher. $replydata->discussion = $discussion3->id; $forumgenerator->create_post($replydata); // Post a private reply to s1's discussion in forum 1 as the editing teacher. $replydata->discussion = $discussion1->id; $replydata->userid = $t1->id; $replydata->privatereplyto = $s1->id; $forumgenerator->create_post($replydata); // Post another private reply to s1 as the teacher. $forumgenerator->create_post($replydata); // Post a private reply to s2's discussion as the editing teacher. $replydata->discussion = $discussion2->id; $replydata->privatereplyto = $s2->id; $forumgenerator->create_post($replydata); // Create discussion by s1 in forum 2. $discussiondata->forum = $forum2->id; $discussiondata->userid = $s1->id; $discussion11 = $forumgenerator->create_discussion($discussiondata); // Post a private reply to s1's discussion in forum 2 as the editing teacher. $replydata->discussion = $discussion11->id; $replydata->privatereplyto = $s1->id; $forumgenerator->create_post($replydata); // Let's count! // S1 should see 8 unread posts 3 discussions posts + 2 private replies + 3 normal replies. $this->setUser($s1); $forum1cm = get_coursemodule_from_instance('forum', $forum1->id); $result = forum_tp_count_forum_unread_posts($forum1cm, $course, true); $this->assertEquals(8, $result); // S2 should see 7 unread posts 3 discussions posts + 1 private reply + 3 normal replies. $this->setUser($s2); $result = forum_tp_count_forum_unread_posts($forum1cm, $course, true); $this->assertEquals(7, $result); // S3 should see 6 unread posts 3 discussions posts + 3 normal replies. No private replies. $this->setUser($s3); $result = forum_tp_count_forum_unread_posts($forum1cm, $course, true); $this->assertEquals(6, $result); // The editing teacher should see 9 unread posts in forum 1: 3 discussions posts + 3 normal replies + 3 private replies. $this->setUser($t1); $result = forum_tp_count_forum_unread_posts($forum1cm, $course, true); $this->assertEquals(9, $result); // Same with the non-editing teacher, since they can read private replies by default. $this->setUser($t2); $result = forum_tp_count_forum_unread_posts($forum1cm, $course, true); $this->assertEquals(9, $result); // But for forum 2, the non-editing teacher should only see 1 unread which is s1's discussion post. $result = forum_tp_count_forum_unread_posts($forum2cm, $course); $this->assertEquals(1, $result); } /** * Test the logic in the forum_tp_count_forum_unread_posts() function when private replies are present and group modes are set. * * @covers ::forum_tp_count_forum_unread_posts */ public function test_forum_tp_count_forum_unread_posts_with_private_replies_and_separate_groups(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); // Create 3 students. $s1 = $generator->create_user(['username' => 's1', 'trackforums' => 1]); $s2 = $generator->create_user(['username' => 's2', 'trackforums' => 1]); // Editing teacher. $t1 = $generator->create_user(['username' => 't1', 'trackforums' => 1]); // Create our course. $course = $generator->create_course(); // Enrol students, editing and non-editing teachers. $generator->enrol_user($s1->id, $course->id, 'student'); $generator->enrol_user($s2->id, $course->id, 'student'); $generator->enrol_user($t1->id, $course->id, 'editingteacher'); // Create groups. $g1 = $generator->create_group(['courseid' => $course->id]); $g2 = $generator->create_group(['courseid' => $course->id]); $generator->create_group_member(['groupid' => $g1->id, 'userid' => $s1->id]); $generator->create_group_member(['groupid' => $g2->id, 'userid' => $s2->id]); // Create forums. $forum1 = $generator->create_module('forum', ['course' => $course->id, 'groupmode' => SEPARATEGROUPS]); $forum2 = $generator->create_module('forum', ['course' => $course->id, 'groupmode' => VISIBLEGROUPS]); $forumgenerator = $generator->get_plugin_generator('mod_forum'); // Create discussion by s1. $discussiondata = (object)[ 'course' => $course->id, 'forum' => $forum1->id, 'userid' => $s1->id, 'groupid' => $g1->id, ]; $discussion1 = $forumgenerator->create_discussion($discussiondata); // Create discussion by s2. $discussiondata->userid = $s2->id; $discussiondata->groupid = $g2->id; $discussion2 = $forumgenerator->create_discussion($discussiondata); // Post a normal reply to s1's discussion in forum 1 as the editing teacher. $replydata = (object)[ 'course' => $course->id, 'forum' => $forum1->id, 'discussion' => $discussion1->id, 'userid' => $t1->id, ]; $forumgenerator->create_post($replydata); // Post a normal reply to s2's discussion as the editing teacher. $replydata->discussion = $discussion2->id; $forumgenerator->create_post($replydata); // Post a private reply to s1's discussion in forum 1 as the editing teacher. $replydata->discussion = $discussion1->id; $replydata->userid = $t1->id; $replydata->privatereplyto = $s1->id; $forumgenerator->create_post($replydata); // Post another private reply to s1 as the teacher. $forumgenerator->create_post($replydata); // Post a private reply to s2's discussion as the editing teacher. $replydata->discussion = $discussion2->id; $replydata->privatereplyto = $s2->id; $forumgenerator->create_post($replydata); // Create discussion by s1 in forum 2. $discussiondata->forum = $forum2->id; $discussiondata->userid = $s1->id; $discussiondata->groupid = $g1->id; $discussion21 = $forumgenerator->create_discussion($discussiondata); // Post a private reply to s1's discussion in forum 2 as the editing teacher. $replydata->discussion = $discussion21->id; $replydata->privatereplyto = $s1->id; $forumgenerator->create_post($replydata); // Let's count! // S1 should see 4 unread posts in forum 1 (1 discussions post + 2 private replies + 1 normal reply). $this->setUser($s1); $forum1cm = get_coursemodule_from_instance('forum', $forum1->id); $result = forum_tp_count_forum_unread_posts($forum1cm, $course, true); $this->assertEquals(4, $result); // S2 should see 3 unread posts in forum 1 (1 discussions post + 1 private reply + 1 normal reply). $this->setUser($s2); $result = forum_tp_count_forum_unread_posts($forum1cm, $course, true); $this->assertEquals(3, $result); // S2 should see 1 unread posts in forum 2 (visible groups, 1 discussion post from s1). $forum2cm = get_coursemodule_from_instance('forum', $forum2->id); $result = forum_tp_count_forum_unread_posts($forum2cm, $course, true); $this->assertEquals(1, $result); // The editing teacher should still see 7 unread posts (2 discussions posts + 2 normal replies + 3 private replies) // in forum 1 since they have the capability to view all groups by default. $this->setUser($t1); $result = forum_tp_count_forum_unread_posts($forum1cm, $course, true); $this->assertEquals(7, $result); } /** * Test the logic in the test_forum_tp_get_untracked_forums() function. */ public function test_forum_tp_get_untracked_forums(): void { global $CFG; $this->resetAfterTest(); $useron = $this->getDataGenerator()->create_user(array('trackforums' => 1)); $useroff = $this->getDataGenerator()->create_user(array('trackforums' => 0)); $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'trackingtype' => FORUM_TRACKING_OFF); // Off. $forumoff = $this->getDataGenerator()->create_module('forum', $options); $options = array('course' => $course->id, 'trackingtype' => FORUM_TRACKING_FORCED); // On. $forumforce = $this->getDataGenerator()->create_module('forum', $options); $options = array('course' => $course->id, 'trackingtype' => FORUM_TRACKING_OPTIONAL); // Optional. $forumoptional = $this->getDataGenerator()->create_module('forum', $options); // Allow force. $CFG->forum_allowforcedreadtracking = 1; // On user with force on. $result = forum_tp_get_untracked_forums($useron->id, $course->id); $this->assertEquals(1, count($result)); $this->assertEquals(true, isset($result[$forumoff->id])); // Off user with force on. $result = forum_tp_get_untracked_forums($useroff->id, $course->id); $this->assertEquals(2, count($result)); $this->assertEquals(true, isset($result[$forumoff->id])); $this->assertEquals(true, isset($result[$forumoptional->id])); // Don't allow force. $CFG->forum_allowforcedreadtracking = 0; // On user with force off. $result = forum_tp_get_untracked_forums($useron->id, $course->id); $this->assertEquals(1, count($result)); $this->assertEquals(true, isset($result[$forumoff->id])); // Off user with force off. $result = forum_tp_get_untracked_forums($useroff->id, $course->id); $this->assertEquals(3, count($result)); $this->assertEquals(true, isset($result[$forumoff->id])); $this->assertEquals(true, isset($result[$forumoptional->id])); $this->assertEquals(true, isset($result[$forumforce->id])); // Stop tracking so we can test again. forum_tp_stop_tracking($forumforce->id, $useron->id); forum_tp_stop_tracking($forumoptional->id, $useron->id); forum_tp_stop_tracking($forumforce->id, $useroff->id); forum_tp_stop_tracking($forumoptional->id, $useroff->id); // Allow force. $CFG->forum_allowforcedreadtracking = 1; // On user with force on. $result = forum_tp_get_untracked_forums($useron->id, $course->id); $this->assertEquals(2, count($result)); $this->assertEquals(true, isset($result[$forumoff->id])); $this->assertEquals(true, isset($result[$forumoptional->id])); // Off user with force on. $result = forum_tp_get_untracked_forums($useroff->id, $course->id); $this->assertEquals(2, count($result)); $this->assertEquals(true, isset($result[$forumoff->id])); $this->assertEquals(true, isset($result[$forumoptional->id])); // Don't allow force. $CFG->forum_allowforcedreadtracking = 0; // On user with force off. $result = forum_tp_get_untracked_forums($useron->id, $course->id); $this->assertEquals(3, count($result)); $this->assertEquals(true, isset($result[$forumoff->id])); $this->assertEquals(true, isset($result[$forumoptional->id])); $this->assertEquals(true, isset($result[$forumforce->id])); // Off user with force off. $result = forum_tp_get_untracked_forums($useroff->id, $course->id); $this->assertEquals(3, count($result)); $this->assertEquals(true, isset($result[$forumoff->id])); $this->assertEquals(true, isset($result[$forumoptional->id])); $this->assertEquals(true, isset($result[$forumforce->id])); } /** * Test subscription using automatic subscription on create. */ public function test_forum_auto_subscribe_on_create(): void { global $CFG; $this->resetAfterTest(); $usercount = 5; $course = $this->getDataGenerator()->create_course(); $users = array(); for ($i = 0; $i < $usercount; $i++) { $user = $this->getDataGenerator()->create_user(); $users[] = $user; $this->getDataGenerator()->enrol_user($user->id, $course->id); } $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE); // Automatic Subscription. $forum = $this->getDataGenerator()->create_module('forum', $options); $result = \mod_forum\subscriptions::fetch_subscribed_users($forum); $this->assertEquals($usercount, count($result)); foreach ($users as $user) { $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum)); } } /** * Test subscription using forced subscription on create. */ public function test_forum_forced_subscribe_on_create(): void { global $CFG; $this->resetAfterTest(); $usercount = 5; $course = $this->getDataGenerator()->create_course(); $users = array(); for ($i = 0; $i < $usercount; $i++) { $user = $this->getDataGenerator()->create_user(); $users[] = $user; $this->getDataGenerator()->enrol_user($user->id, $course->id); } $options = array('course' => $course->id, 'forcesubscribe' => FORUM_FORCESUBSCRIBE); // Forced subscription. $forum = $this->getDataGenerator()->create_module('forum', $options); $result = \mod_forum\subscriptions::fetch_subscribed_users($forum); $this->assertEquals($usercount, count($result)); foreach ($users as $user) { $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum)); } } /** * Test subscription using optional subscription on create. */ public function test_forum_optional_subscribe_on_create(): void { global $CFG; $this->resetAfterTest(); $usercount = 5; $course = $this->getDataGenerator()->create_course(); $users = array(); for ($i = 0; $i < $usercount; $i++) { $user = $this->getDataGenerator()->create_user(); $users[] = $user; $this->getDataGenerator()->enrol_user($user->id, $course->id); } $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); // Subscription optional. $forum = $this->getDataGenerator()->create_module('forum', $options); $result = \mod_forum\subscriptions::fetch_subscribed_users($forum); // No subscriptions by default. $this->assertEquals(0, count($result)); foreach ($users as $user) { $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum)); } } /** * Test subscription using disallow subscription on create. */ public function test_forum_disallow_subscribe_on_create(): void { global $CFG; $this->resetAfterTest(); $usercount = 5; $course = $this->getDataGenerator()->create_course(); $users = array(); for ($i = 0; $i < $usercount; $i++) { $user = $this->getDataGenerator()->create_user(); $users[] = $user; $this->getDataGenerator()->enrol_user($user->id, $course->id); } $options = array('course' => $course->id, 'forcesubscribe' => FORUM_DISALLOWSUBSCRIBE); // Subscription prevented. $forum = $this->getDataGenerator()->create_module('forum', $options); $result = \mod_forum\subscriptions::fetch_subscribed_users($forum); // No subscriptions by default. $this->assertEquals(0, count($result)); foreach ($users as $user) { $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum)); } } /** * Test that context fetching returns the appropriate context. */ public function test_forum_get_context(): void { global $DB, $PAGE; $this->resetAfterTest(); // Setup test data. $course = $this->getDataGenerator()->create_course(); $coursecontext = \context_course::instance($course->id); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); $forumcm = get_coursemodule_from_instance('forum', $forum->id); $forumcontext = \context_module::instance($forumcm->id); // First check that specifying the context results in the correct context being returned. // Do this before we set up the page object and we should return from the coursemodule record. // There should be no DB queries here because the context type was correct. $startcount = $DB->perf_get_reads(); $result = forum_get_context($forum->id, $forumcontext); $aftercount = $DB->perf_get_reads(); $this->assertEquals($forumcontext, $result); $this->assertEquals(0, $aftercount - $startcount); // And a context which is not the correct type. // This tests will result in a DB query to fetch the course_module. $startcount = $DB->perf_get_reads(); $result = forum_get_context($forum->id, $coursecontext); $aftercount = $DB->perf_get_reads(); $this->assertEquals($forumcontext, $result); $this->assertEquals(1, $aftercount - $startcount); // Now do not specify a context at all. // This tests will result in a DB query to fetch the course_module. $startcount = $DB->perf_get_reads(); $result = forum_get_context($forum->id); $aftercount = $DB->perf_get_reads(); $this->assertEquals($forumcontext, $result); $this->assertEquals(1, $aftercount - $startcount); // Set up the default page event to use the forum. $PAGE = new \moodle_page(); $PAGE->set_context($forumcontext); $PAGE->set_cm($forumcm, $course, $forum); // Now specify a context which is not a context_module. // There should be no DB queries here because we use the PAGE. $startcount = $DB->perf_get_reads(); $result = forum_get_context($forum->id, $coursecontext); $aftercount = $DB->perf_get_reads(); $this->assertEquals($forumcontext, $result); $this->assertEquals(0, $aftercount - $startcount); // Now do not specify a context at all. // There should be no DB queries here because we use the PAGE. $startcount = $DB->perf_get_reads(); $result = forum_get_context($forum->id); $aftercount = $DB->perf_get_reads(); $this->assertEquals($forumcontext, $result); $this->assertEquals(0, $aftercount - $startcount); // Now specify the page context of the course instead.. $PAGE = new \moodle_page(); $PAGE->set_context($coursecontext); // Now specify a context which is not a context_module. // This tests will result in a DB query to fetch the course_module. $startcount = $DB->perf_get_reads(); $result = forum_get_context($forum->id, $coursecontext); $aftercount = $DB->perf_get_reads(); $this->assertEquals($forumcontext, $result); $this->assertEquals(1, $aftercount - $startcount); // Now do not specify a context at all. // This tests will result in a DB query to fetch the course_module. $startcount = $DB->perf_get_reads(); $result = forum_get_context($forum->id); $aftercount = $DB->perf_get_reads(); $this->assertEquals($forumcontext, $result); $this->assertEquals(1, $aftercount - $startcount); } /** * Test getting the neighbour threads of a discussion. */ public function test_forum_get_neighbours(): void { global $CFG, $DB; $this->resetAfterTest(); $timenow = time(); $timenext = $timenow; // Setup test data. $forumgen = $this->getDataGenerator()->get_plugin_generator('mod_forum'); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); $record = new \stdClass(); $record->course = $course->id; $record->userid = $user->id; $record->forum = $forum->id; $record->timemodified = time(); $disc1 = $forumgen->create_discussion($record); $record->timemodified++; $disc2 = $forumgen->create_discussion($record); $record->timemodified++; $disc3 = $forumgen->create_discussion($record); $record->timemodified++; $disc4 = $forumgen->create_discussion($record); $record->timemodified++; $disc5 = $forumgen->create_discussion($record); // Getting the neighbours. $neighbours = forum_get_discussion_neighbours($cm, $disc1, $forum); $this->assertEmpty($neighbours['prev']); $this->assertEquals($disc2->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc2, $forum); $this->assertEquals($disc1->id, $neighbours['prev']->id); $this->assertEquals($disc3->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc3, $forum); $this->assertEquals($disc2->id, $neighbours['prev']->id); $this->assertEquals($disc4->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc4, $forum); $this->assertEquals($disc3->id, $neighbours['prev']->id); $this->assertEquals($disc5->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc5, $forum); $this->assertEquals($disc4->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); // Post in some discussions. We manually update the discussion record because // the data generator plays with timemodified in a way that would break this test. $record->timemodified++; $disc1->timemodified = $record->timemodified; $DB->update_record('forum_discussions', $disc1); $neighbours = forum_get_discussion_neighbours($cm, $disc5, $forum); $this->assertEquals($disc4->id, $neighbours['prev']->id); $this->assertEquals($disc1->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc2, $forum); $this->assertEmpty($neighbours['prev']); $this->assertEquals($disc3->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc1, $forum); $this->assertEquals($disc5->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); // After some discussions were created. $record->timemodified++; $disc6 = $forumgen->create_discussion($record); $neighbours = forum_get_discussion_neighbours($cm, $disc6, $forum); $this->assertEquals($disc1->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); $record->timemodified++; $disc7 = $forumgen->create_discussion($record); $neighbours = forum_get_discussion_neighbours($cm, $disc7, $forum); $this->assertEquals($disc6->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); // Adding timed discussions. $CFG->forum_enabletimedposts = true; $now = $record->timemodified; $past = $now - 600; $future = $now + 600; $record = new \stdClass(); $record->course = $course->id; $record->userid = $user->id; $record->forum = $forum->id; $record->timestart = $past; $record->timeend = $future; $record->timemodified = $now; $record->timemodified++; $disc8 = $forumgen->create_discussion($record); $record->timemodified++; $record->timestart = $future; $record->timeend = 0; $disc9 = $forumgen->create_discussion($record); $record->timemodified++; $record->timestart = 0; $record->timeend = 0; $disc10 = $forumgen->create_discussion($record); $record->timemodified++; $record->timestart = 0; $record->timeend = $past; $disc11 = $forumgen->create_discussion($record); $record->timemodified++; $record->timestart = $past; $record->timeend = $future; $disc12 = $forumgen->create_discussion($record); $record->timemodified++; $record->timestart = $future + 1; // Should be last post for those that can see it. $record->timeend = 0; $disc13 = $forumgen->create_discussion($record); // Admin user ignores the timed settings of discussions. // Post ordering taking into account timestart: // 8 = t // 10 = t+3 // 11 = t+4 // 12 = t+5 // 9 = t+60 // 13 = t+61. $this->setAdminUser(); $neighbours = forum_get_discussion_neighbours($cm, $disc8, $forum); $this->assertEquals($disc7->id, $neighbours['prev']->id); $this->assertEquals($disc10->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc9, $forum); $this->assertEquals($disc12->id, $neighbours['prev']->id); $this->assertEquals($disc13->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc10, $forum); $this->assertEquals($disc8->id, $neighbours['prev']->id); $this->assertEquals($disc11->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc11, $forum); $this->assertEquals($disc10->id, $neighbours['prev']->id); $this->assertEquals($disc12->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc12, $forum); $this->assertEquals($disc11->id, $neighbours['prev']->id); $this->assertEquals($disc9->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc13, $forum); $this->assertEquals($disc9->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); // Normal user can see their own timed discussions. $this->setUser($user); $neighbours = forum_get_discussion_neighbours($cm, $disc8, $forum); $this->assertEquals($disc7->id, $neighbours['prev']->id); $this->assertEquals($disc10->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc9, $forum); $this->assertEquals($disc12->id, $neighbours['prev']->id); $this->assertEquals($disc13->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc10, $forum); $this->assertEquals($disc8->id, $neighbours['prev']->id); $this->assertEquals($disc11->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc11, $forum); $this->assertEquals($disc10->id, $neighbours['prev']->id); $this->assertEquals($disc12->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc12, $forum); $this->assertEquals($disc11->id, $neighbours['prev']->id); $this->assertEquals($disc9->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc13, $forum); $this->assertEquals($disc9->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); // Normal user does not ignore timed settings. $this->setUser($user2); $neighbours = forum_get_discussion_neighbours($cm, $disc8, $forum); $this->assertEquals($disc7->id, $neighbours['prev']->id); $this->assertEquals($disc10->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc10, $forum); $this->assertEquals($disc8->id, $neighbours['prev']->id); $this->assertEquals($disc12->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc12, $forum); $this->assertEquals($disc10->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); // Reset to normal mode. $CFG->forum_enabletimedposts = false; $this->setAdminUser(); // Two discussions with identical timemodified will sort by id. $record->timemodified += 25; $DB->update_record('forum_discussions', (object) array('id' => $disc3->id, 'timemodified' => $record->timemodified)); $DB->update_record('forum_discussions', (object) array('id' => $disc2->id, 'timemodified' => $record->timemodified)); $DB->update_record('forum_discussions', (object) array('id' => $disc12->id, 'timemodified' => $record->timemodified - 5)); $disc2 = $DB->get_record('forum_discussions', array('id' => $disc2->id)); $disc3 = $DB->get_record('forum_discussions', array('id' => $disc3->id)); $neighbours = forum_get_discussion_neighbours($cm, $disc3, $forum); $this->assertEquals($disc2->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); $neighbours = forum_get_discussion_neighbours($cm, $disc2, $forum); $this->assertEquals($disc12->id, $neighbours['prev']->id); $this->assertEquals($disc3->id, $neighbours['next']->id); // Set timemodified to not be identical. $DB->update_record('forum_discussions', (object) array('id' => $disc2->id, 'timemodified' => $record->timemodified - 1)); // Test pinned posts behave correctly. $disc8->pinned = FORUM_DISCUSSION_PINNED; $DB->update_record('forum_discussions', (object) array('id' => $disc8->id, 'pinned' => $disc8->pinned)); $neighbours = forum_get_discussion_neighbours($cm, $disc8, $forum); $this->assertEquals($disc3->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); $neighbours = forum_get_discussion_neighbours($cm, $disc3, $forum); $this->assertEquals($disc2->id, $neighbours['prev']->id); $this->assertEquals($disc8->id, $neighbours['next']->id); // Test 3 pinned posts. $disc6->pinned = FORUM_DISCUSSION_PINNED; $DB->update_record('forum_discussions', (object) array('id' => $disc6->id, 'pinned' => $disc6->pinned)); $disc4->pinned = FORUM_DISCUSSION_PINNED; $DB->update_record('forum_discussions', (object) array('id' => $disc4->id, 'pinned' => $disc4->pinned)); $neighbours = forum_get_discussion_neighbours($cm, $disc6, $forum); $this->assertEquals($disc4->id, $neighbours['prev']->id); $this->assertEquals($disc8->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc4, $forum); $this->assertEquals($disc3->id, $neighbours['prev']->id); $this->assertEquals($disc6->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc8, $forum); $this->assertEquals($disc6->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); } /** * Test getting the neighbour threads of a blog-like forum. */ public function test_forum_get_neighbours_blog(): void { global $CFG, $DB; $this->resetAfterTest(); $timenow = time(); $timenext = $timenow; // Setup test data. $forumgen = $this->getDataGenerator()->get_plugin_generator('mod_forum'); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id, 'type' => 'blog')); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); $record = new \stdClass(); $record->course = $course->id; $record->userid = $user->id; $record->forum = $forum->id; $record->timemodified = time(); $disc1 = $forumgen->create_discussion($record); $record->timemodified++; $disc2 = $forumgen->create_discussion($record); $record->timemodified++; $disc3 = $forumgen->create_discussion($record); $record->timemodified++; $disc4 = $forumgen->create_discussion($record); $record->timemodified++; $disc5 = $forumgen->create_discussion($record); // Getting the neighbours. $neighbours = forum_get_discussion_neighbours($cm, $disc1, $forum); $this->assertEmpty($neighbours['prev']); $this->assertEquals($disc2->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc2, $forum); $this->assertEquals($disc1->id, $neighbours['prev']->id); $this->assertEquals($disc3->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc3, $forum); $this->assertEquals($disc2->id, $neighbours['prev']->id); $this->assertEquals($disc4->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc4, $forum); $this->assertEquals($disc3->id, $neighbours['prev']->id); $this->assertEquals($disc5->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc5, $forum); $this->assertEquals($disc4->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); // Make sure that the thread's timemodified does not affect the order. $record->timemodified++; $disc1->timemodified = $record->timemodified; $DB->update_record('forum_discussions', $disc1); $neighbours = forum_get_discussion_neighbours($cm, $disc1, $forum); $this->assertEmpty($neighbours['prev']); $this->assertEquals($disc2->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc2, $forum); $this->assertEquals($disc1->id, $neighbours['prev']->id); $this->assertEquals($disc3->id, $neighbours['next']->id); // Add another blog post. $record->timemodified++; $disc6 = $forumgen->create_discussion($record); $neighbours = forum_get_discussion_neighbours($cm, $disc6, $forum); $this->assertEquals($disc5->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); $record->timemodified++; $disc7 = $forumgen->create_discussion($record); $neighbours = forum_get_discussion_neighbours($cm, $disc7, $forum); $this->assertEquals($disc6->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); // Adding timed discussions. $CFG->forum_enabletimedposts = true; $now = $record->timemodified; $past = $now - 600; $future = $now + 600; $record = new \stdClass(); $record->course = $course->id; $record->userid = $user->id; $record->forum = $forum->id; $record->timestart = $past; $record->timeend = $future; $record->timemodified = $now; $record->timemodified++; $disc8 = $forumgen->create_discussion($record); $record->timemodified++; $record->timestart = $future; $record->timeend = 0; $disc9 = $forumgen->create_discussion($record); $record->timemodified++; $record->timestart = 0; $record->timeend = 0; $disc10 = $forumgen->create_discussion($record); $record->timemodified++; $record->timestart = 0; $record->timeend = $past; $disc11 = $forumgen->create_discussion($record); $record->timemodified++; $record->timestart = $past; $record->timeend = $future; $disc12 = $forumgen->create_discussion($record); // Admin user ignores the timed settings of discussions. $this->setAdminUser(); $neighbours = forum_get_discussion_neighbours($cm, $disc8, $forum); $this->assertEquals($disc7->id, $neighbours['prev']->id); $this->assertEquals($disc9->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc9, $forum); $this->assertEquals($disc8->id, $neighbours['prev']->id); $this->assertEquals($disc10->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc10, $forum); $this->assertEquals($disc9->id, $neighbours['prev']->id); $this->assertEquals($disc11->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc11, $forum); $this->assertEquals($disc10->id, $neighbours['prev']->id); $this->assertEquals($disc12->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc12, $forum); $this->assertEquals($disc11->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); // Normal user can see their own timed discussions. $this->setUser($user); $neighbours = forum_get_discussion_neighbours($cm, $disc8, $forum); $this->assertEquals($disc7->id, $neighbours['prev']->id); $this->assertEquals($disc9->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc9, $forum); $this->assertEquals($disc8->id, $neighbours['prev']->id); $this->assertEquals($disc10->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc10, $forum); $this->assertEquals($disc9->id, $neighbours['prev']->id); $this->assertEquals($disc11->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc11, $forum); $this->assertEquals($disc10->id, $neighbours['prev']->id); $this->assertEquals($disc12->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc12, $forum); $this->assertEquals($disc11->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); // Normal user does not ignore timed settings. $this->setUser($user2); $neighbours = forum_get_discussion_neighbours($cm, $disc8, $forum); $this->assertEquals($disc7->id, $neighbours['prev']->id); $this->assertEquals($disc10->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc10, $forum); $this->assertEquals($disc8->id, $neighbours['prev']->id); $this->assertEquals($disc12->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc12, $forum); $this->assertEquals($disc10->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); // Reset to normal mode. $CFG->forum_enabletimedposts = false; $this->setAdminUser(); $record->timemodified++; // Two blog posts with identical creation time will sort by id. $DB->update_record('forum_posts', (object) array('id' => $disc2->firstpost, 'created' => $record->timemodified)); $DB->update_record('forum_posts', (object) array('id' => $disc3->firstpost, 'created' => $record->timemodified)); $neighbours = forum_get_discussion_neighbours($cm, $disc2, $forum); $this->assertEquals($disc12->id, $neighbours['prev']->id); $this->assertEquals($disc3->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm, $disc3, $forum); $this->assertEquals($disc2->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); } /** * Test getting the neighbour threads of a discussion. */ public function test_forum_get_neighbours_with_groups(): void { $this->resetAfterTest(); $timenow = time(); $timenext = $timenow; // Setup test data. $forumgen = $this->getDataGenerator()->get_plugin_generator('mod_forum'); $course = $this->getDataGenerator()->create_course(); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course->id); $this->getDataGenerator()->enrol_user($user2->id, $course->id); $this->getDataGenerator()->create_group_member(array('userid' => $user1->id, 'groupid' => $group1->id)); $forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id, 'groupmode' => VISIBLEGROUPS)); $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id, 'groupmode' => SEPARATEGROUPS)); $cm1 = get_coursemodule_from_instance('forum', $forum1->id); $cm2 = get_coursemodule_from_instance('forum', $forum2->id); $context1 = \context_module::instance($cm1->id); $context2 = \context_module::instance($cm2->id); // Creating discussions in both forums. $record = new \stdClass(); $record->course = $course->id; $record->userid = $user1->id; $record->forum = $forum1->id; $record->groupid = $group1->id; $record->timemodified = time(); $disc11 = $forumgen->create_discussion($record); $record->forum = $forum2->id; $record->timemodified++; $disc21 = $forumgen->create_discussion($record); $record->timemodified++; $record->userid = $user2->id; $record->forum = $forum1->id; $record->groupid = $group2->id; $disc12 = $forumgen->create_discussion($record); $record->forum = $forum2->id; $disc22 = $forumgen->create_discussion($record); $record->timemodified++; $record->userid = $user1->id; $record->forum = $forum1->id; $record->groupid = null; $disc13 = $forumgen->create_discussion($record); $record->forum = $forum2->id; $disc23 = $forumgen->create_discussion($record); $record->timemodified++; $record->userid = $user2->id; $record->forum = $forum1->id; $record->groupid = $group2->id; $disc14 = $forumgen->create_discussion($record); $record->forum = $forum2->id; $disc24 = $forumgen->create_discussion($record); $record->timemodified++; $record->userid = $user1->id; $record->forum = $forum1->id; $record->groupid = $group1->id; $disc15 = $forumgen->create_discussion($record); $record->forum = $forum2->id; $disc25 = $forumgen->create_discussion($record); // Admin user can see all groups. $this->setAdminUser(); $neighbours = forum_get_discussion_neighbours($cm1, $disc11, $forum1); $this->assertEmpty($neighbours['prev']); $this->assertEquals($disc12->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm2, $disc21, $forum2); $this->assertEmpty($neighbours['prev']); $this->assertEquals($disc22->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm1, $disc12, $forum1); $this->assertEquals($disc11->id, $neighbours['prev']->id); $this->assertEquals($disc13->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm2, $disc22, $forum2); $this->assertEquals($disc21->id, $neighbours['prev']->id); $this->assertEquals($disc23->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm1, $disc13, $forum1); $this->assertEquals($disc12->id, $neighbours['prev']->id); $this->assertEquals($disc14->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm2, $disc23, $forum2); $this->assertEquals($disc22->id, $neighbours['prev']->id); $this->assertEquals($disc24->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm1, $disc14, $forum1); $this->assertEquals($disc13->id, $neighbours['prev']->id); $this->assertEquals($disc15->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm2, $disc24, $forum2); $this->assertEquals($disc23->id, $neighbours['prev']->id); $this->assertEquals($disc25->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm1, $disc15, $forum1); $this->assertEquals($disc14->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); $neighbours = forum_get_discussion_neighbours($cm2, $disc25, $forum2); $this->assertEquals($disc24->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); // Admin user is only viewing group 1. $_POST['group'] = $group1->id; $this->assertEquals($group1->id, groups_get_activity_group($cm1, true)); $this->assertEquals($group1->id, groups_get_activity_group($cm2, true)); $neighbours = forum_get_discussion_neighbours($cm1, $disc11, $forum1); $this->assertEmpty($neighbours['prev']); $this->assertEquals($disc13->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm2, $disc21, $forum2); $this->assertEmpty($neighbours['prev']); $this->assertEquals($disc23->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm1, $disc13, $forum1); $this->assertEquals($disc11->id, $neighbours['prev']->id); $this->assertEquals($disc15->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm2, $disc23, $forum2); $this->assertEquals($disc21->id, $neighbours['prev']->id); $this->assertEquals($disc25->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm1, $disc15, $forum1); $this->assertEquals($disc13->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); $neighbours = forum_get_discussion_neighbours($cm2, $disc25, $forum2); $this->assertEquals($disc23->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); // Normal user viewing non-grouped posts (this is only possible in visible groups). $this->setUser($user1); $_POST['group'] = 0; $this->assertEquals(0, groups_get_activity_group($cm1, true)); // They can see anything in visible groups. $neighbours = forum_get_discussion_neighbours($cm1, $disc12, $forum1); $this->assertEquals($disc11->id, $neighbours['prev']->id); $this->assertEquals($disc13->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm1, $disc13, $forum1); $this->assertEquals($disc12->id, $neighbours['prev']->id); $this->assertEquals($disc14->id, $neighbours['next']->id); // Normal user, orphan of groups, can only see non-grouped posts in separate groups. $this->setUser($user2); $_POST['group'] = 0; $this->assertEquals(0, groups_get_activity_group($cm2, true)); $neighbours = forum_get_discussion_neighbours($cm2, $disc23, $forum2); $this->assertEmpty($neighbours['prev']); $this->assertEmpty($neighbours['next']); $neighbours = forum_get_discussion_neighbours($cm2, $disc22, $forum2); $this->assertEmpty($neighbours['prev']); $this->assertEquals($disc23->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm2, $disc24, $forum2); $this->assertEquals($disc23->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); // Switching to viewing group 1. $this->setUser($user1); $_POST['group'] = $group1->id; $this->assertEquals($group1->id, groups_get_activity_group($cm1, true)); $this->assertEquals($group1->id, groups_get_activity_group($cm2, true)); // They can see non-grouped or same group. $neighbours = forum_get_discussion_neighbours($cm1, $disc11, $forum1); $this->assertEmpty($neighbours['prev']); $this->assertEquals($disc13->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm2, $disc21, $forum2); $this->assertEmpty($neighbours['prev']); $this->assertEquals($disc23->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm1, $disc13, $forum1); $this->assertEquals($disc11->id, $neighbours['prev']->id); $this->assertEquals($disc15->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm2, $disc23, $forum2); $this->assertEquals($disc21->id, $neighbours['prev']->id); $this->assertEquals($disc25->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm1, $disc15, $forum1); $this->assertEquals($disc13->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); $neighbours = forum_get_discussion_neighbours($cm2, $disc25, $forum2); $this->assertEquals($disc23->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); // Querying the neighbours of a discussion passing the wrong CM. $this->expectException('coding_exception'); forum_get_discussion_neighbours($cm2, $disc11, $forum2); } /** * Test getting the neighbour threads of a blog-like forum with groups involved. */ public function test_forum_get_neighbours_with_groups_blog(): void { $this->resetAfterTest(); $timenow = time(); $timenext = $timenow; // Setup test data. $forumgen = $this->getDataGenerator()->get_plugin_generator('mod_forum'); $course = $this->getDataGenerator()->create_course(); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course->id); $this->getDataGenerator()->enrol_user($user2->id, $course->id); $this->getDataGenerator()->create_group_member(array('userid' => $user1->id, 'groupid' => $group1->id)); $forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id, 'type' => 'blog', 'groupmode' => VISIBLEGROUPS)); $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id, 'type' => 'blog', 'groupmode' => SEPARATEGROUPS)); $cm1 = get_coursemodule_from_instance('forum', $forum1->id); $cm2 = get_coursemodule_from_instance('forum', $forum2->id); $context1 = \context_module::instance($cm1->id); $context2 = \context_module::instance($cm2->id); // Creating blog posts in both forums. $record = new \stdClass(); $record->course = $course->id; $record->userid = $user1->id; $record->forum = $forum1->id; $record->groupid = $group1->id; $record->timemodified = time(); $disc11 = $forumgen->create_discussion($record); $record->timenow = $timenext++; $record->forum = $forum2->id; $record->timemodified++; $disc21 = $forumgen->create_discussion($record); $record->timemodified++; $record->userid = $user2->id; $record->forum = $forum1->id; $record->groupid = $group2->id; $disc12 = $forumgen->create_discussion($record); $record->forum = $forum2->id; $disc22 = $forumgen->create_discussion($record); $record->timemodified++; $record->userid = $user1->id; $record->forum = $forum1->id; $record->groupid = null; $disc13 = $forumgen->create_discussion($record); $record->forum = $forum2->id; $disc23 = $forumgen->create_discussion($record); $record->timemodified++; $record->userid = $user2->id; $record->forum = $forum1->id; $record->groupid = $group2->id; $disc14 = $forumgen->create_discussion($record); $record->forum = $forum2->id; $disc24 = $forumgen->create_discussion($record); $record->timemodified++; $record->userid = $user1->id; $record->forum = $forum1->id; $record->groupid = $group1->id; $disc15 = $forumgen->create_discussion($record); $record->forum = $forum2->id; $disc25 = $forumgen->create_discussion($record); // Admin user can see all groups. $this->setAdminUser(); $neighbours = forum_get_discussion_neighbours($cm1, $disc11, $forum1); $this->assertEmpty($neighbours['prev']); $this->assertEquals($disc12->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm2, $disc21, $forum2); $this->assertEmpty($neighbours['prev']); $this->assertEquals($disc22->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm1, $disc12, $forum1); $this->assertEquals($disc11->id, $neighbours['prev']->id); $this->assertEquals($disc13->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm2, $disc22, $forum2); $this->assertEquals($disc21->id, $neighbours['prev']->id); $this->assertEquals($disc23->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm1, $disc13, $forum1); $this->assertEquals($disc12->id, $neighbours['prev']->id); $this->assertEquals($disc14->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm2, $disc23, $forum2); $this->assertEquals($disc22->id, $neighbours['prev']->id); $this->assertEquals($disc24->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm1, $disc14, $forum1); $this->assertEquals($disc13->id, $neighbours['prev']->id); $this->assertEquals($disc15->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm2, $disc24, $forum2); $this->assertEquals($disc23->id, $neighbours['prev']->id); $this->assertEquals($disc25->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm1, $disc15, $forum1); $this->assertEquals($disc14->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); $neighbours = forum_get_discussion_neighbours($cm2, $disc25, $forum2); $this->assertEquals($disc24->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); // Admin user is only viewing group 1. $_POST['group'] = $group1->id; $this->assertEquals($group1->id, groups_get_activity_group($cm1, true)); $this->assertEquals($group1->id, groups_get_activity_group($cm2, true)); $neighbours = forum_get_discussion_neighbours($cm1, $disc11, $forum1); $this->assertEmpty($neighbours['prev']); $this->assertEquals($disc13->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm2, $disc21, $forum2); $this->assertEmpty($neighbours['prev']); $this->assertEquals($disc23->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm1, $disc13, $forum1); $this->assertEquals($disc11->id, $neighbours['prev']->id); $this->assertEquals($disc15->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm2, $disc23, $forum2); $this->assertEquals($disc21->id, $neighbours['prev']->id); $this->assertEquals($disc25->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm1, $disc15, $forum1); $this->assertEquals($disc13->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); $neighbours = forum_get_discussion_neighbours($cm2, $disc25, $forum2); $this->assertEquals($disc23->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); // Normal user viewing non-grouped posts (this is only possible in visible groups). $this->setUser($user1); $_POST['group'] = 0; $this->assertEquals(0, groups_get_activity_group($cm1, true)); // They can see anything in visible groups. $neighbours = forum_get_discussion_neighbours($cm1, $disc12, $forum1); $this->assertEquals($disc11->id, $neighbours['prev']->id); $this->assertEquals($disc13->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm1, $disc13, $forum1); $this->assertEquals($disc12->id, $neighbours['prev']->id); $this->assertEquals($disc14->id, $neighbours['next']->id); // Normal user, orphan of groups, can only see non-grouped posts in separate groups. $this->setUser($user2); $_POST['group'] = 0; $this->assertEquals(0, groups_get_activity_group($cm2, true)); $neighbours = forum_get_discussion_neighbours($cm2, $disc23, $forum2); $this->assertEmpty($neighbours['prev']); $this->assertEmpty($neighbours['next']); $neighbours = forum_get_discussion_neighbours($cm2, $disc22, $forum2); $this->assertEmpty($neighbours['prev']); $this->assertEquals($disc23->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm2, $disc24, $forum2); $this->assertEquals($disc23->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); // Switching to viewing group 1. $this->setUser($user1); $_POST['group'] = $group1->id; $this->assertEquals($group1->id, groups_get_activity_group($cm1, true)); $this->assertEquals($group1->id, groups_get_activity_group($cm2, true)); // They can see non-grouped or same group. $neighbours = forum_get_discussion_neighbours($cm1, $disc11, $forum1); $this->assertEmpty($neighbours['prev']); $this->assertEquals($disc13->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm2, $disc21, $forum2); $this->assertEmpty($neighbours['prev']); $this->assertEquals($disc23->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm1, $disc13, $forum1); $this->assertEquals($disc11->id, $neighbours['prev']->id); $this->assertEquals($disc15->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm2, $disc23, $forum2); $this->assertEquals($disc21->id, $neighbours['prev']->id); $this->assertEquals($disc25->id, $neighbours['next']->id); $neighbours = forum_get_discussion_neighbours($cm1, $disc15, $forum1); $this->assertEquals($disc13->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); $neighbours = forum_get_discussion_neighbours($cm2, $disc25, $forum2); $this->assertEquals($disc23->id, $neighbours['prev']->id); $this->assertEmpty($neighbours['next']); // Querying the neighbours of a discussion passing the wrong CM. $this->expectException('coding_exception'); forum_get_discussion_neighbours($cm2, $disc11, $forum2); } public function test_count_discussion_replies_basic(): void { list($forum, $discussionids) = $this->create_multiple_discussions_with_replies(10, 5); // Count the discussion replies in the forum. $result = forum_count_discussion_replies($forum->id); $this->assertCount(10, $result); } public function test_count_discussion_replies_limited(): void { list($forum, $discussionids) = $this->create_multiple_discussions_with_replies(10, 5); // Adding limits shouldn't make a difference. $result = forum_count_discussion_replies($forum->id, "", 20); $this->assertCount(10, $result); } public function test_count_discussion_replies_paginated(): void { list($forum, $discussionids) = $this->create_multiple_discussions_with_replies(10, 5); // Adding paging shouldn't make any difference. $result = forum_count_discussion_replies($forum->id, "", -1, 0, 100); $this->assertCount(10, $result); } public function test_count_discussion_replies_paginated_sorted(): void { list($forum, $discussionids) = $this->create_multiple_discussions_with_replies(10, 5); // Specifying the forumsort should also give a good result. This follows a different path. $result = forum_count_discussion_replies($forum->id, "d.id asc", -1, 0, 100); $this->assertCount(10, $result); foreach ($result as $row) { // Grab the first discussionid. $discussionid = array_shift($discussionids); $this->assertEquals($discussionid, $row->discussion); } } public function test_count_discussion_replies_limited_sorted(): void { list($forum, $discussionids) = $this->create_multiple_discussions_with_replies(10, 5); // Adding limits, and a forumsort shouldn't make a difference. $result = forum_count_discussion_replies($forum->id, "d.id asc", 20); $this->assertCount(10, $result); foreach ($result as $row) { // Grab the first discussionid. $discussionid = array_shift($discussionids); $this->assertEquals($discussionid, $row->discussion); } } public function test_count_discussion_replies_paginated_sorted_small(): void { list($forum, $discussionids) = $this->create_multiple_discussions_with_replies(10, 5); // Grabbing a smaller subset and they should be ordered as expected. $result = forum_count_discussion_replies($forum->id, "d.id asc", -1, 0, 5); $this->assertCount(5, $result); foreach ($result as $row) { // Grab the first discussionid. $discussionid = array_shift($discussionids); $this->assertEquals($discussionid, $row->discussion); } } public function test_count_discussion_replies_paginated_sorted_small_reverse(): void { list($forum, $discussionids) = $this->create_multiple_discussions_with_replies(10, 5); // Grabbing a smaller subset and they should be ordered as expected. $result = forum_count_discussion_replies($forum->id, "d.id desc", -1, 0, 5); $this->assertCount(5, $result); foreach ($result as $row) { // Grab the last discussionid. $discussionid = array_pop($discussionids); $this->assertEquals($discussionid, $row->discussion); } } public function test_count_discussion_replies_limited_sorted_small_reverse(): void { list($forum, $discussionids) = $this->create_multiple_discussions_with_replies(10, 5); // Adding limits, and a forumsort shouldn't make a difference. $result = forum_count_discussion_replies($forum->id, "d.id desc", 5); $this->assertCount(5, $result); foreach ($result as $row) { // Grab the last discussionid. $discussionid = array_pop($discussionids); $this->assertEquals($discussionid, $row->discussion); } } /** * Test the reply count when used with private replies. */ public function test_forum_count_discussion_replies_private(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $context = \context_module::instance($forum->cmid); $cm = get_coursemodule_from_instance('forum', $forum->id); $student = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($student->id, $course->id); $teacher = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($teacher->id, $course->id); $privilegeduser = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($privilegeduser->id, $course->id, 'editingteacher'); $otheruser = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($otheruser->id, $course->id); $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum'); // Create a discussion with some replies. $record = new \stdClass(); $record->course = $forum->course; $record->forum = $forum->id; $record->userid = $student->id; $discussion = $generator->create_discussion($record); $replycount = 5; $replyto = $DB->get_record('forum_posts', array('discussion' => $discussion->id)); // Create a couple of standard replies. $post = new \stdClass(); $post->userid = $student->id; $post->discussion = $discussion->id; $post->parent = $replyto->id; for ($i = 0; $i < $replycount; $i++) { $post = $generator->create_post($post); } // Create a private reply post from the teacher back to the student. $reply = new \stdClass(); $reply->userid = $teacher->id; $reply->discussion = $discussion->id; $reply->parent = $replyto->id; $reply->privatereplyto = $replyto->userid; $generator->create_post($reply); // The user is the author of the private reply. $this->setUser($teacher->id); $counts = forum_count_discussion_replies($forum->id); $this->assertEquals($replycount + 1, $counts[$discussion->id]->replies); // The user is the intended recipient. $this->setUser($student->id); $counts = forum_count_discussion_replies($forum->id); $this->assertEquals($replycount + 1, $counts[$discussion->id]->replies); // The user is not the author or recipient, but does have the readprivatereplies capability. $this->setUser($privilegeduser->id); $counts = forum_count_discussion_replies($forum->id, "", -1, -1, 0, true); $this->assertEquals($replycount + 1, $counts[$discussion->id]->replies); // The user is not allowed to view this post. $this->setUser($otheruser->id); $counts = forum_count_discussion_replies($forum->id); $this->assertEquals($replycount, $counts[$discussion->id]->replies); } public function test_discussion_pinned_sort(): void { list($forum, $discussionids) = $this->create_multiple_discussions_with_replies(10, 5); $cm = get_coursemodule_from_instance('forum', $forum->id); $discussions = forum_get_discussions($cm); // First discussion should be pinned. $first = reset($discussions); $this->assertEquals(1, $first->pinned, "First discussion should be pinned discussion"); } public function test_forum_view(): void { global $CFG; $CFG->enablecompletion = 1; $this->resetAfterTest(); // Setup test data. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), array('completion' => 2, 'completionview' => 1)); $context = \context_module::instance($forum->cmid); $cm = get_coursemodule_from_instance('forum', $forum->id); // Trigger and capture the event. $sink = $this->redirectEvents(); $this->setAdminUser(); forum_view($forum, $course, $cm, $context); $events = $sink->get_events(); // 2 additional events thanks to completion. $this->assertCount(3, $events); $event = array_pop($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\course_module_viewed', $event); $this->assertEquals($context, $event->get_context()); $url = new \moodle_url('/mod/forum/view.php', array('f' => $forum->id)); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); // Check completion status. $completion = new \completion_info($course); $completiondata = $completion->get_data($cm); $this->assertEquals(1, $completiondata->completionstate); } /** * Test forum_discussion_view. */ public function test_forum_discussion_view(): void { global $CFG, $USER; $this->resetAfterTest(); // Setup test data. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $discussion = $this->create_single_discussion_with_replies($forum, $USER, 2); $context = \context_module::instance($forum->cmid); $cm = get_coursemodule_from_instance('forum', $forum->id); // Trigger and capture the event. $sink = $this->redirectEvents(); $this->setAdminUser(); forum_discussion_view($context, $forum, $discussion); $events = $sink->get_events(); $this->assertCount(1, $events); $event = array_pop($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\discussion_viewed', $event); $this->assertEquals($context, $event->get_context()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Create a new course, forum, and user with a number of discussions and replies. * * @param int $discussioncount The number of discussions to create * @param int $replycount The number of replies to create in each discussion * @return array Containing the created forum object, and the ids of the created discussions. */ protected function create_multiple_discussions_with_replies($discussioncount, $replycount) { $this->resetAfterTest(); // Setup the content. $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $record = new \stdClass(); $record->course = $course->id; $forum = $this->getDataGenerator()->create_module('forum', $record); // Create 10 discussions with replies. $discussionids = array(); for ($i = 0; $i < $discussioncount; $i++) { // Pin 3rd discussion. if ($i == 3) { $discussion = $this->create_single_discussion_pinned_with_replies($forum, $user, $replycount); } else { $discussion = $this->create_single_discussion_with_replies($forum, $user, $replycount); } $discussionids[] = $discussion->id; } return array($forum, $discussionids); } /** * Create a discussion with a number of replies. * * @param object $forum The forum which has been created * @param object $user The user making the discussion and replies * @param int $replycount The number of replies * @return object $discussion */ protected function create_single_discussion_with_replies($forum, $user, $replycount) { global $DB; $generator = self::getDataGenerator()->get_plugin_generator('mod_forum'); $record = new \stdClass(); $record->course = $forum->course; $record->forum = $forum->id; $record->userid = $user->id; $discussion = $generator->create_discussion($record); // Retrieve the first post. $replyto = $DB->get_record('forum_posts', array('discussion' => $discussion->id)); // Create the replies. $post = new \stdClass(); $post->userid = $user->id; $post->discussion = $discussion->id; $post->parent = $replyto->id; for ($i = 0; $i < $replycount; $i++) { $generator->create_post($post); } return $discussion; } /** * Create a discussion with a number of replies. * * @param object $forum The forum which has been created * @param object $user The user making the discussion and replies * @param int $replycount The number of replies * @return object $discussion */ protected function create_single_discussion_pinned_with_replies($forum, $user, $replycount) { global $DB; $generator = self::getDataGenerator()->get_plugin_generator('mod_forum'); $record = new \stdClass(); $record->course = $forum->course; $record->forum = $forum->id; $record->userid = $user->id; $record->pinned = FORUM_DISCUSSION_PINNED; $discussion = $generator->create_discussion($record); // Retrieve the first post. $replyto = $DB->get_record('forum_posts', array('discussion' => $discussion->id)); // Create the replies. $post = new \stdClass(); $post->userid = $user->id; $post->discussion = $discussion->id; $post->parent = $replyto->id; for ($i = 0; $i < $replycount; $i++) { $generator->create_post($post); } return $discussion; } /** * Tests for mod_forum_rating_can_see_item_ratings(). * * @throws coding_exception * @throws rating_exception */ public function test_mod_forum_rating_can_see_item_ratings(): void { global $DB; $this->resetAfterTest(); // Setup test data. $course = new \stdClass(); $course->groupmode = SEPARATEGROUPS; $course->groupmodeforce = true; $course = $this->getDataGenerator()->create_course($course); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $generator = self::getDataGenerator()->get_plugin_generator('mod_forum'); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); // Create users. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); $user4 = $this->getDataGenerator()->create_user(); // Groups and stuff. $role = $DB->get_record('role', array('shortname' => 'teacher'), '*', MUST_EXIST); $this->getDataGenerator()->enrol_user($user1->id, $course->id, $role->id); $this->getDataGenerator()->enrol_user($user2->id, $course->id, $role->id); $this->getDataGenerator()->enrol_user($user3->id, $course->id, $role->id); $this->getDataGenerator()->enrol_user($user4->id, $course->id, $role->id); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); groups_add_member($group1, $user1); groups_add_member($group1, $user2); groups_add_member($group2, $user3); groups_add_member($group2, $user4); $record = new \stdClass(); $record->course = $forum->course; $record->forum = $forum->id; $record->userid = $user1->id; $record->groupid = $group1->id; $discussion = $generator->create_discussion($record); // Retrieve the first post. $post = $DB->get_record('forum_posts', array('discussion' => $discussion->id)); $ratingoptions = new \stdClass; $ratingoptions->context = $context; $ratingoptions->ratingarea = 'post'; $ratingoptions->component = 'mod_forum'; $ratingoptions->itemid = $post->id; $ratingoptions->scaleid = 2; $ratingoptions->userid = $user2->id; $rating = new \rating($ratingoptions); $rating->update_rating(2); // Now try to access it as various users. unassign_capability('moodle/site:accessallgroups', $role->id); $params = array('contextid' => 2, 'component' => 'mod_forum', 'ratingarea' => 'post', 'itemid' => $post->id, 'scaleid' => 2); $this->setUser($user1); $this->assertTrue(mod_forum_rating_can_see_item_ratings($params)); $this->setUser($user2); $this->assertTrue(mod_forum_rating_can_see_item_ratings($params)); $this->setUser($user3); $this->assertFalse(mod_forum_rating_can_see_item_ratings($params)); $this->setUser($user4); $this->assertFalse(mod_forum_rating_can_see_item_ratings($params)); // Now try with accessallgroups cap and make sure everything is visible. assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $role->id, $context->id); $this->setUser($user1); $this->assertTrue(mod_forum_rating_can_see_item_ratings($params)); $this->setUser($user2); $this->assertTrue(mod_forum_rating_can_see_item_ratings($params)); $this->setUser($user3); $this->assertTrue(mod_forum_rating_can_see_item_ratings($params)); $this->setUser($user4); $this->assertTrue(mod_forum_rating_can_see_item_ratings($params)); // Change group mode and verify visibility. $course->groupmode = VISIBLEGROUPS; $DB->update_record('course', $course); unassign_capability('moodle/site:accessallgroups', $role->id); $this->setUser($user1); $this->assertTrue(mod_forum_rating_can_see_item_ratings($params)); $this->setUser($user2); $this->assertTrue(mod_forum_rating_can_see_item_ratings($params)); $this->setUser($user3); $this->assertTrue(mod_forum_rating_can_see_item_ratings($params)); $this->setUser($user4); $this->assertTrue(mod_forum_rating_can_see_item_ratings($params)); } /** * Test forum_get_discussions */ public function test_forum_get_discussions_with_groups(): void { global $DB; $this->resetAfterTest(true); // Create course to add the module. $course = self::getDataGenerator()->create_course(array('groupmode' => VISIBLEGROUPS, 'groupmodeforce' => 0)); $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); $user3 = self::getDataGenerator()->create_user(); $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST); self::getDataGenerator()->enrol_user($user1->id, $course->id, $role->id); self::getDataGenerator()->enrol_user($user2->id, $course->id, $role->id); self::getDataGenerator()->enrol_user($user3->id, $course->id, $role->id); // Forum forcing separate gropus. $record = new \stdClass(); $record->course = $course->id; $forum = self::getDataGenerator()->create_module('forum', $record, array('groupmode' => SEPARATEGROUPS)); $cm = get_coursemodule_from_instance('forum', $forum->id); // Create groups. $group1 = self::getDataGenerator()->create_group(array('courseid' => $course->id, 'name' => 'group1')); $group2 = self::getDataGenerator()->create_group(array('courseid' => $course->id, 'name' => 'group2')); $group3 = self::getDataGenerator()->create_group(array('courseid' => $course->id, 'name' => 'group3')); // Add the user1 to g1 and g2 groups. groups_add_member($group1->id, $user1->id); groups_add_member($group2->id, $user1->id); // Add the user 2 and 3 to only one group. groups_add_member($group1->id, $user2->id); groups_add_member($group3->id, $user3->id); // Add a few discussions. $record = array(); $record['course'] = $course->id; $record['forum'] = $forum->id; $record['userid'] = $user1->id; $record['groupid'] = $group1->id; $discussiong1u1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); $record['groupid'] = $group2->id; $discussiong2u1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); $record['userid'] = $user2->id; $record['groupid'] = $group1->id; $discussiong1u2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); $record['userid'] = $user3->id; $record['groupid'] = $group3->id; $discussiong3u3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); self::setUser($user1); // Test retrieve discussions not passing the groupid parameter. We will receive only first group discussions. $discussions = forum_get_discussions($cm); self::assertCount(2, $discussions); foreach ($discussions as $discussion) { self::assertEquals($group1->id, $discussion->groupid); } // Get all my discussions. $discussions = forum_get_discussions($cm, '', true, -1, -1, false, -1, 0, 0); self::assertCount(3, $discussions); // Get all my g1 discussions. $discussions = forum_get_discussions($cm, '', true, -1, -1, false, -1, 0, $group1->id); self::assertCount(2, $discussions); foreach ($discussions as $discussion) { self::assertEquals($group1->id, $discussion->groupid); } // Get all my g2 discussions. $discussions = forum_get_discussions($cm, '', true, -1, -1, false, -1, 0, $group2->id); self::assertCount(1, $discussions); $discussion = array_shift($discussions); self::assertEquals($group2->id, $discussion->groupid); self::assertEquals($user1->id, $discussion->userid); self::assertEquals($discussiong2u1->id, $discussion->discussion); // Get all my g3 discussions (I'm not enrolled in that group). $discussions = forum_get_discussions($cm, '', true, -1, -1, false, -1, 0, $group3->id); self::assertCount(0, $discussions); // This group does not exist. $discussions = forum_get_discussions($cm, '', true, -1, -1, false, -1, 0, $group3->id + 1000); self::assertCount(0, $discussions); self::setUser($user2); // Test retrieve discussions not passing the groupid parameter. We will receive only first group discussions. $discussions = forum_get_discussions($cm); self::assertCount(2, $discussions); foreach ($discussions as $discussion) { self::assertEquals($group1->id, $discussion->groupid); } // Get all my viewable discussions. $discussions = forum_get_discussions($cm, '', true, -1, -1, false, -1, 0, 0); self::assertCount(2, $discussions); foreach ($discussions as $discussion) { self::assertEquals($group1->id, $discussion->groupid); } // Get all my g2 discussions (I'm not enrolled in that group). $discussions = forum_get_discussions($cm, '', true, -1, -1, false, -1, 0, $group2->id); self::assertCount(0, $discussions); // Get all my g3 discussions (I'm not enrolled in that group). $discussions = forum_get_discussions($cm, '', true, -1, -1, false, -1, 0, $group3->id); self::assertCount(0, $discussions); } /** * Test forum_user_can_post_discussion */ public function test_forum_user_can_post_discussion(): void { global $DB; $this->resetAfterTest(true); // Create course to add the module. $course = self::getDataGenerator()->create_course(array('groupmode' => SEPARATEGROUPS, 'groupmodeforce' => 1)); $user = self::getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); // Forum forcing separate gropus. $record = new \stdClass(); $record->course = $course->id; $forum = self::getDataGenerator()->create_module('forum', $record, array('groupmode' => SEPARATEGROUPS)); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); self::setUser($user); // The user is not enroled in any group, try to post in a forum with separate groups. $can = forum_user_can_post_discussion($forum, null, -1, $cm, $context); $this->assertFalse($can); // Create a group. $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); // Try to post in a group the user is not enrolled. $can = forum_user_can_post_discussion($forum, $group->id, -1, $cm, $context); $this->assertFalse($can); // Add the user to a group. groups_add_member($group->id, $user->id); // Try to post in a group the user is not enrolled. $can = forum_user_can_post_discussion($forum, $group->id + 1, -1, $cm, $context); $this->assertFalse($can); // Now try to post in the user group. (null means it will guess the group). $can = forum_user_can_post_discussion($forum, null, -1, $cm, $context); $this->assertTrue($can); $can = forum_user_can_post_discussion($forum, $group->id, -1, $cm, $context); $this->assertTrue($can); // Test all groups. $can = forum_user_can_post_discussion($forum, -1, -1, $cm, $context); $this->assertFalse($can); $this->setAdminUser(); $can = forum_user_can_post_discussion($forum, -1, -1, $cm, $context); $this->assertTrue($can); // Change forum type. $forum->type = 'news'; $DB->update_record('forum', $forum); // Admin can post news. $can = forum_user_can_post_discussion($forum, null, -1, $cm, $context); $this->assertTrue($can); // Normal users don't. self::setUser($user); $can = forum_user_can_post_discussion($forum, null, -1, $cm, $context); $this->assertFalse($can); // Change forum type. $forum->type = 'eachuser'; $DB->update_record('forum', $forum); // I didn't post yet, so I should be able to post. $can = forum_user_can_post_discussion($forum, null, -1, $cm, $context); $this->assertTrue($can); // Post now. $record = new \stdClass(); $record->course = $course->id; $record->userid = $user->id; $record->forum = $forum->id; $record->groupid = $group->id; $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // I already posted, I shouldn't be able to post. $can = forum_user_can_post_discussion($forum, null, -1, $cm, $context); $this->assertFalse($can); // Last check with no groups, normal forum and course. $course->groupmode = NOGROUPS; $course->groupmodeforce = 0; $DB->update_record('course', $course); $forum->type = 'general'; $forum->groupmode = NOGROUPS; $DB->update_record('forum', $forum); $can = forum_user_can_post_discussion($forum, null, -1, $cm, $context); $this->assertTrue($can); } /** * Test forum_user_can_post_discussion_after_cutoff */ public function test_forum_user_can_post_discussion_after_cutoff(): void { $this->resetAfterTest(true); // Create course to add the module. $course = self::getDataGenerator()->create_course(array('groupmode' => SEPARATEGROUPS, 'groupmodeforce' => 1)); $student = self::getDataGenerator()->create_user(); $teacher = self::getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($student->id, $course->id); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher'); // Forum forcing separate gropus. $record = new \stdClass(); $record->course = $course->id; $record->cutoffdate = time() - 1; $forum = self::getDataGenerator()->create_module('forum', $record); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); self::setUser($student); // Students usually don't have the mod/forum:canoverridecutoff capability. $can = forum_user_can_post_discussion($forum, null, -1, $cm, $context); $this->assertFalse($can); self::setUser($teacher); // Teachers usually have the mod/forum:canoverridecutoff capability. $can = forum_user_can_post_discussion($forum, null, -1, $cm, $context); $this->assertTrue($can); } /** * Test forum_user_has_posted_discussion with no groups. */ public function test_forum_user_has_posted_discussion_no_groups(): void { global $CFG; $this->resetAfterTest(true); $course = self::getDataGenerator()->create_course(); $author = self::getDataGenerator()->create_user(); $other = self::getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($author->id, $course->id); $forum = self::getDataGenerator()->create_module('forum', (object) ['course' => $course->id ]); self::setUser($author); // Neither user has posted. $this->assertFalse(forum_user_has_posted_discussion($forum->id, $author->id)); $this->assertFalse(forum_user_has_posted_discussion($forum->id, $other->id)); // Post in the forum. $record = new \stdClass(); $record->course = $course->id; $record->userid = $author->id; $record->forum = $forum->id; $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // The author has now posted, but the other user has not. $this->assertTrue(forum_user_has_posted_discussion($forum->id, $author->id)); $this->assertFalse(forum_user_has_posted_discussion($forum->id, $other->id)); } /** * Test forum_user_has_posted_discussion with multiple forums */ public function test_forum_user_has_posted_discussion_multiple_forums(): void { global $CFG; $this->resetAfterTest(true); $course = self::getDataGenerator()->create_course(); $author = self::getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($author->id, $course->id); $forum1 = self::getDataGenerator()->create_module('forum', (object) ['course' => $course->id ]); $forum2 = self::getDataGenerator()->create_module('forum', (object) ['course' => $course->id ]); self::setUser($author); // No post in either forum. $this->assertFalse(forum_user_has_posted_discussion($forum1->id, $author->id)); $this->assertFalse(forum_user_has_posted_discussion($forum2->id, $author->id)); // Post in the forum. $record = new \stdClass(); $record->course = $course->id; $record->userid = $author->id; $record->forum = $forum1->id; $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // The author has now posted in forum1, but not forum2. $this->assertTrue(forum_user_has_posted_discussion($forum1->id, $author->id)); $this->assertFalse(forum_user_has_posted_discussion($forum2->id, $author->id)); } /** * Test forum_user_has_posted_discussion with multiple groups. */ public function test_forum_user_has_posted_discussion_multiple_groups(): void { global $CFG; $this->resetAfterTest(true); $course = self::getDataGenerator()->create_course(); $author = self::getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($author->id, $course->id); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); groups_add_member($group1->id, $author->id); groups_add_member($group2->id, $author->id); $forum = self::getDataGenerator()->create_module('forum', (object) ['course' => $course->id ], [ 'groupmode' => SEPARATEGROUPS, ]); self::setUser($author); // The user has not posted in either group. $this->assertFalse(forum_user_has_posted_discussion($forum->id, $author->id)); $this->assertFalse(forum_user_has_posted_discussion($forum->id, $author->id, $group1->id)); $this->assertFalse(forum_user_has_posted_discussion($forum->id, $author->id, $group2->id)); // Post in one group. $record = new \stdClass(); $record->course = $course->id; $record->userid = $author->id; $record->forum = $forum->id; $record->groupid = $group1->id; $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // The author has now posted in one group, but the other user has not. $this->assertTrue(forum_user_has_posted_discussion($forum->id, $author->id)); $this->assertTrue(forum_user_has_posted_discussion($forum->id, $author->id, $group1->id)); $this->assertFalse(forum_user_has_posted_discussion($forum->id, $author->id, $group2->id)); // Post in the other group. $record = new \stdClass(); $record->course = $course->id; $record->userid = $author->id; $record->forum = $forum->id; $record->groupid = $group2->id; $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // The author has now posted in one group, but the other user has not. $this->assertTrue(forum_user_has_posted_discussion($forum->id, $author->id)); $this->assertTrue(forum_user_has_posted_discussion($forum->id, $author->id, $group1->id)); $this->assertTrue(forum_user_has_posted_discussion($forum->id, $author->id, $group2->id)); } /** * Test the logic for forum_get_user_posted_mailnow where the user can select if qanda forum post should be sent without delay * * @covers ::forum_get_user_posted_mailnow */ public function test_forum_get_user_posted_mailnow(): void { $this->resetAfterTest(); // Create a forum. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $author = $this->getDataGenerator()->create_user(); $authorid = $author->id; $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum'); // Create a discussion. $record = new \stdClass(); $record->course = $forum->course; $record->forum = $forum->id; $record->userid = $authorid; $discussion = $generator->create_discussion($record); $did = $discussion->id; // Return False if no post exists with 'mailnow' selected. $generator->create_post(['userid' => $authorid, 'discussion' => $did, 'forum' => $forum->id, 'mailnow' => 0]); $result = forum_get_user_posted_mailnow($did, $authorid); $this->assertFalse($result); // Return True only if any post has 'mailnow' selected. $generator->create_post(['userid' => $authorid, 'discussion' => $did, 'forum' => $forum->id, 'mailnow' => 1]); $result = forum_get_user_posted_mailnow($did, $authorid); $this->assertTrue($result); } /** * Tests the mod_forum_myprofile_navigation() function. */ public function test_mod_forum_myprofile_navigation(): void { $this->resetAfterTest(true); // Set up the test. $tree = new \core_user\output\myprofile\tree(); $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $iscurrentuser = true; // Set as the current user. $this->setUser($user); // Check the node tree is correct. mod_forum_myprofile_navigation($tree, $user, $iscurrentuser, $course); $reflector = new \ReflectionObject($tree); $nodes = $reflector->getProperty('nodes'); $this->assertArrayHasKey('forumposts', $nodes->getValue($tree)); $this->assertArrayHasKey('forumdiscussions', $nodes->getValue($tree)); } /** * Tests the mod_forum_myprofile_navigation() function as a guest. */ public function test_mod_forum_myprofile_navigation_as_guest(): void { global $USER; $this->resetAfterTest(true); // Set up the test. $tree = new \core_user\output\myprofile\tree(); $course = $this->getDataGenerator()->create_course(); $iscurrentuser = true; // Set user as guest. $this->setGuestUser(); // Check the node tree is correct. mod_forum_myprofile_navigation($tree, $USER, $iscurrentuser, $course); $reflector = new \ReflectionObject($tree); $nodes = $reflector->getProperty('nodes'); $this->assertArrayNotHasKey('forumposts', $nodes->getValue($tree)); $this->assertArrayNotHasKey('forumdiscussions', $nodes->getValue($tree)); } /** * Tests the mod_forum_myprofile_navigation() function as a user viewing another user's profile. */ public function test_mod_forum_myprofile_navigation_different_user(): void { $this->resetAfterTest(true); // Set up the test. $tree = new \core_user\output\myprofile\tree(); $user = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $iscurrentuser = true; // Set to different user's profile. $this->setUser($user2); // Check the node tree is correct. mod_forum_myprofile_navigation($tree, $user, $iscurrentuser, $course); $reflector = new \ReflectionObject($tree); $nodes = $reflector->getProperty('nodes'); $this->assertArrayHasKey('forumposts', $nodes->getValue($tree)); $this->assertArrayHasKey('forumdiscussions', $nodes->getValue($tree)); } /** * Test test_pinned_discussion_with_group. */ public function test_pinned_discussion_with_group(): void { global $SESSION; $this->resetAfterTest(); $course1 = $this->getDataGenerator()->create_course(); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); // Create an author user. $author = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($author->id, $course1->id); // Create two viewer users - one in a group, one not. $viewer1 = $this->getDataGenerator()->create_user((object) array('trackforums' => 1)); $this->getDataGenerator()->enrol_user($viewer1->id, $course1->id); $viewer2 = $this->getDataGenerator()->create_user((object) array('trackforums' => 1)); $this->getDataGenerator()->enrol_user($viewer2->id, $course1->id); $this->getDataGenerator()->create_group_member(array('userid' => $viewer2->id, 'groupid' => $group1->id)); $forum1 = $this->getDataGenerator()->create_module('forum', (object) array( 'course' => $course1->id, 'groupmode' => SEPARATEGROUPS, )); $coursemodule = get_coursemodule_from_instance('forum', $forum1->id); $alldiscussions = array(); $group1discussions = array(); // Create 4 discussions in all participants group and group1, where the first // discussion is pinned in each group. $allrecord = new \stdClass(); $allrecord->course = $course1->id; $allrecord->userid = $author->id; $allrecord->forum = $forum1->id; $allrecord->pinned = FORUM_DISCUSSION_PINNED; $group1record = new \stdClass(); $group1record->course = $course1->id; $group1record->userid = $author->id; $group1record->forum = $forum1->id; $group1record->groupid = $group1->id; $group1record->pinned = FORUM_DISCUSSION_PINNED; $alldiscussions[] = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($allrecord); $group1discussions[] = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($group1record); // Create unpinned discussions. $allrecord->pinned = FORUM_DISCUSSION_UNPINNED; $group1record->pinned = FORUM_DISCUSSION_UNPINNED; for ($i = 0; $i < 3; $i++) { $alldiscussions[] = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($allrecord); $group1discussions[] = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($group1record); } // As viewer1 (no group). This user shouldn't see any of group1's discussions // so their expected discussion order is (where rightmost is highest priority): // Ad1, ad2, ad3, ad0. $this->setUser($viewer1->id); // CHECK 1. // Take the neighbours of ad3, which should be prev: ad2 and next: ad0. $neighbours = forum_get_discussion_neighbours($coursemodule, $alldiscussions[3], $forum1); // Ad2 check. $this->assertEquals($alldiscussions[2]->id, $neighbours['prev']->id); // Ad0 check. $this->assertEquals($alldiscussions[0]->id, $neighbours['next']->id); // CHECK 2. // Take the neighbours of ad0, which should be prev: ad3 and next: null. $neighbours = forum_get_discussion_neighbours($coursemodule, $alldiscussions[0], $forum1); // Ad3 check. $this->assertEquals($alldiscussions[3]->id, $neighbours['prev']->id); // Null check. $this->assertEmpty($neighbours['next']); // CHECK 3. // Take the neighbours of ad1, which should be prev: null and next: ad2. $neighbours = forum_get_discussion_neighbours($coursemodule, $alldiscussions[1], $forum1); // Null check. $this->assertEmpty($neighbours['prev']); // Ad2 check. $this->assertEquals($alldiscussions[2]->id, $neighbours['next']->id); // Temporary hack to workaround for MDL-52656. $SESSION->currentgroup = null; // As viewer2 (group1). This user should see all of group1's posts and the all participants group. // The expected discussion order is (rightmost is highest priority): // Ad1, gd1, ad2, gd2, ad3, gd3, ad0, gd0. $this->setUser($viewer2->id); // CHECK 1. // Take the neighbours of ad1, which should be prev: null and next: gd1. $neighbours = forum_get_discussion_neighbours($coursemodule, $alldiscussions[1], $forum1); // Null check. $this->assertEmpty($neighbours['prev']); // Gd1 check. $this->assertEquals($group1discussions[1]->id, $neighbours['next']->id); // CHECK 2. // Take the neighbours of ad3, which should be prev: gd2 and next: gd3. $neighbours = forum_get_discussion_neighbours($coursemodule, $alldiscussions[3], $forum1); // Gd2 check. $this->assertEquals($group1discussions[2]->id, $neighbours['prev']->id); // Gd3 check. $this->assertEquals($group1discussions[3]->id, $neighbours['next']->id); // CHECK 3. // Take the neighbours of gd3, which should be prev: ad3 and next: ad0. $neighbours = forum_get_discussion_neighbours($coursemodule, $group1discussions[3], $forum1); // Ad3 check. $this->assertEquals($alldiscussions[3]->id, $neighbours['prev']->id); // Ad0 check. $this->assertEquals($alldiscussions[0]->id, $neighbours['next']->id); // CHECK 4. // Take the neighbours of gd0, which should be prev: ad0 and next: null. $neighbours = forum_get_discussion_neighbours($coursemodule, $group1discussions[0], $forum1); // Ad0 check. $this->assertEquals($alldiscussions[0]->id, $neighbours['prev']->id); // Null check. $this->assertEmpty($neighbours['next']); } /** * Test test_pinned_with_timed_discussions. */ public function test_pinned_with_timed_discussions(): void { global $CFG; $CFG->forum_enabletimedposts = true; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); // Create an user. $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); // Create a forum. $record = new \stdClass(); $record->course = $course->id; $forum = $this->getDataGenerator()->create_module('forum', (object) array( 'course' => $course->id, 'groupmode' => SEPARATEGROUPS, )); $coursemodule = get_coursemodule_from_instance('forum', $forum->id); $now = time(); $discussions = array(); $discussiongenerator = $this->getDataGenerator()->get_plugin_generator('mod_forum'); $record = new \stdClass(); $record->course = $course->id; $record->userid = $user->id; $record->forum = $forum->id; $record->pinned = FORUM_DISCUSSION_PINNED; $record->timemodified = $now; $discussions[] = $discussiongenerator->create_discussion($record); $record->pinned = FORUM_DISCUSSION_UNPINNED; $record->timestart = $now + 10; $discussions[] = $discussiongenerator->create_discussion($record); $record->timestart = $now; $discussions[] = $discussiongenerator->create_discussion($record); // Expected order of discussions: // D2, d1, d0. $this->setUser($user->id); // CHECK 1. $neighbours = forum_get_discussion_neighbours($coursemodule, $discussions[2], $forum); // Null check. $this->assertEmpty($neighbours['prev']); // D1 check. $this->assertEquals($discussions[1]->id, $neighbours['next']->id); // CHECK 2. $neighbours = forum_get_discussion_neighbours($coursemodule, $discussions[1], $forum); // D2 check. $this->assertEquals($discussions[2]->id, $neighbours['prev']->id); // D0 check. $this->assertEquals($discussions[0]->id, $neighbours['next']->id); // CHECK 3. $neighbours = forum_get_discussion_neighbours($coursemodule, $discussions[0], $forum); // D2 check. $this->assertEquals($discussions[1]->id, $neighbours['prev']->id); // Null check. $this->assertEmpty($neighbours['next']); } /** * Test test_pinned_timed_discussions_with_timed_discussions. */ public function test_pinned_timed_discussions_with_timed_discussions(): void { global $CFG; $CFG->forum_enabletimedposts = true; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); // Create an user. $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); // Create a forum. $record = new \stdClass(); $record->course = $course->id; $forum = $this->getDataGenerator()->create_module('forum', (object) array( 'course' => $course->id, 'groupmode' => SEPARATEGROUPS, )); $coursemodule = get_coursemodule_from_instance('forum', $forum->id); $now = time(); $discussions = array(); $discussiongenerator = $this->getDataGenerator()->get_plugin_generator('mod_forum'); $record = new \stdClass(); $record->course = $course->id; $record->userid = $user->id; $record->forum = $forum->id; $record->pinned = FORUM_DISCUSSION_PINNED; $record->timemodified = $now; $record->timestart = $now + 10; $discussions[] = $discussiongenerator->create_discussion($record); $record->pinned = FORUM_DISCUSSION_UNPINNED; $discussions[] = $discussiongenerator->create_discussion($record); $record->timestart = $now; $discussions[] = $discussiongenerator->create_discussion($record); $record->pinned = FORUM_DISCUSSION_PINNED; $discussions[] = $discussiongenerator->create_discussion($record); // Expected order of discussions: // D2, d1, d3, d0. $this->setUser($user->id); // CHECK 1. $neighbours = forum_get_discussion_neighbours($coursemodule, $discussions[2], $forum); // Null check. $this->assertEmpty($neighbours['prev']); // D1 check. $this->assertEquals($discussions[1]->id, $neighbours['next']->id); // CHECK 2. $neighbours = forum_get_discussion_neighbours($coursemodule, $discussions[1], $forum); // D2 check. $this->assertEquals($discussions[2]->id, $neighbours['prev']->id); // D3 check. $this->assertEquals($discussions[3]->id, $neighbours['next']->id); // CHECK 3. $neighbours = forum_get_discussion_neighbours($coursemodule, $discussions[3], $forum); // D1 check. $this->assertEquals($discussions[1]->id, $neighbours['prev']->id); // D0 check. $this->assertEquals($discussions[0]->id, $neighbours['next']->id); // CHECK 4. $neighbours = forum_get_discussion_neighbours($coursemodule, $discussions[0], $forum); // D3 check. $this->assertEquals($discussions[3]->id, $neighbours['prev']->id); // Null check. $this->assertEmpty($neighbours['next']); } /** * Test for forum_is_author_hidden. */ public function test_forum_is_author_hidden(): void { // First post, different forum type. $post = (object) ['parent' => 0]; $forum = (object) ['type' => 'standard']; $this->assertFalse(forum_is_author_hidden($post, $forum)); // Child post, different forum type. $post->parent = 1; $this->assertFalse(forum_is_author_hidden($post, $forum)); // First post, single simple discussion forum type. $post->parent = 0; $forum->type = 'single'; $this->assertTrue(forum_is_author_hidden($post, $forum)); // Child post, single simple discussion forum type. $post->parent = 1; $this->assertFalse(forum_is_author_hidden($post, $forum)); // Incorrect parameters: $post. $this->expectException('coding_exception'); $this->expectExceptionMessage('$post->parent must be set.'); unset($post->parent); forum_is_author_hidden($post, $forum); // Incorrect parameters: $forum. $this->expectException('coding_exception'); $this->expectExceptionMessage('$forum->type must be set.'); unset($forum->type); forum_is_author_hidden($post, $forum); } /** * Test the forum_discussion_is_locked function. * * @dataProvider forum_discussion_is_locked_provider * @param \stdClass $forum * @param \stdClass $discussion * @param bool $expect */ public function test_forum_discussion_is_locked($forum, $discussion, $expect): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $plugingenerator = $datagenerator->get_plugin_generator('mod_forum'); $course = $datagenerator->create_course(); $user = $datagenerator->create_user(); $forum = $datagenerator->create_module('forum', (object) array_merge([ 'course' => $course->id ], $forum)); $discussion = $plugingenerator->create_discussion((object) array_merge([ 'course' => $course->id, 'userid' => $user->id, 'forum' => $forum->id, ], $discussion)); $this->assertEquals($expect, forum_discussion_is_locked($forum, $discussion)); } /** * Dataprovider for forum_discussion_is_locked tests. * * @return array */ public static function forum_discussion_is_locked_provider(): array { return [ 'Unlocked: lockdiscussionafter is false' => [ ['lockdiscussionafter' => false], [], false ], 'Unlocked: lockdiscussionafter is set; forum is of type single; post is recent' => [ ['lockdiscussionafter' => DAYSECS, 'type' => 'single'], ['timemodified' => time()], false ], 'Unlocked: lockdiscussionafter is set; forum is of type single; post is old' => [ ['lockdiscussionafter' => MINSECS, 'type' => 'single'], ['timemodified' => time() - DAYSECS], false ], 'Unlocked: lockdiscussionafter is set; forum is of type eachuser; post is recent' => [ ['lockdiscussionafter' => DAYSECS, 'type' => 'eachuser'], ['timemodified' => time()], false ], 'Locked: lockdiscussionafter is set; forum is of type eachuser; post is old' => [ ['lockdiscussionafter' => MINSECS, 'type' => 'eachuser'], ['timemodified' => time() - DAYSECS], true ], ]; } /** * Test the forum_is_cutoff_date_reached function. * * @dataProvider forum_is_cutoff_date_reached_provider * @param array $forum * @param bool $expect */ public function test_forum_is_cutoff_date_reached($forum, $expect): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', (object) array_merge([ 'course' => $course->id ], $forum)); $this->assertEquals($expect, forum_is_cutoff_date_reached($forum)); } /** * Dataprovider for forum_is_cutoff_date_reached tests. * * @return array */ public static function forum_is_cutoff_date_reached_provider(): array { $now = time(); return [ 'cutoffdate is unset' => [ [], false ], 'cutoffdate is 0' => [ ['cutoffdate' => 0], false ], 'cutoffdate is set and is in future' => [ ['cutoffdate' => $now + 86400], false ], 'cutoffdate is set and is in past' => [ ['cutoffdate' => $now - 86400], true ], ]; } /** * Test the forum_is_due_date_reached function. * * @dataProvider forum_is_due_date_reached_provider * @param \stdClass $forum * @param bool $expect */ public function test_forum_is_due_date_reached($forum, $expect): void { $this->resetAfterTest(); $this->setAdminUser(); $datagenerator = $this->getDataGenerator(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', (object) array_merge([ 'course' => $course->id ], $forum)); $this->assertEquals($expect, forum_is_due_date_reached($forum)); } /** * Dataprovider for forum_is_due_date_reached tests. * * @return array */ public static function forum_is_due_date_reached_provider(): array { $now = time(); return [ 'duedate is unset' => [ [], false ], 'duedate is 0' => [ ['duedate' => 0], false ], 'duedate is set and is in future' => [ ['duedate' => $now + 86400], false ], 'duedate is set and is in past' => [ ['duedate' => $now - 86400], true ], ]; } /** * Test that {@link forum_update_post()} keeps correct forum_discussions usermodified. */ public function test_forum_update_post_keeps_discussions_usermodified(): void { global $DB; $this->resetAfterTest(); // Let there be light. $teacher = self::getDataGenerator()->create_user(); $student = self::getDataGenerator()->create_user(); $course = self::getDataGenerator()->create_course(); $forum = self::getDataGenerator()->create_module('forum', (object)[ 'course' => $course->id, ]); $generator = self::getDataGenerator()->get_plugin_generator('mod_forum'); // Let the teacher start a discussion. $discussion = $generator->create_discussion((object)[ 'course' => $course->id, 'userid' => $teacher->id, 'forum' => $forum->id, ]); // On this freshly created discussion, the teacher is the author of the last post. $this->assertEquals($teacher->id, $DB->get_field('forum_discussions', 'usermodified', ['id' => $discussion->id])); // Fetch modified timestamp of the discussion. $discussionmodified = $DB->get_field('forum_discussions', 'timemodified', ['id' => $discussion->id]); $pasttime = $discussionmodified - 3600; // Adjust the discussion modified timestamp back an hour, so it's in the past. $adjustment = (object)[ 'id' => $discussion->id, 'timemodified' => $pasttime, ]; $DB->update_record('forum_discussions', $adjustment); // Let the student reply to the teacher's post. $reply = $generator->create_post((object)[ 'course' => $course->id, 'userid' => $student->id, 'forum' => $forum->id, 'discussion' => $discussion->id, 'parent' => $discussion->firstpost, ]); // The student should now be the last post's author. $this->assertEquals($student->id, $DB->get_field('forum_discussions', 'usermodified', ['id' => $discussion->id])); // Fetch modified timestamp of the discussion and student's post. $discussionmodified = $DB->get_field('forum_discussions', 'timemodified', ['id' => $discussion->id]); $postmodified = $DB->get_field('forum_posts', 'modified', ['id' => $reply->id]); // Discussion modified time should be updated to be equal to the newly created post's time. $this->assertEquals($discussionmodified, $postmodified); // Adjust the discussion and post timestamps, so they are in the past. $adjustment = (object)[ 'id' => $discussion->id, 'timemodified' => $pasttime, ]; $DB->update_record('forum_discussions', $adjustment); $adjustment = (object)[ 'id' => $reply->id, 'modified' => $pasttime, ]; $DB->update_record('forum_posts', $adjustment); // The discussion and student's post time should now be an hour in the past. $this->assertEquals($pasttime, $DB->get_field('forum_discussions', 'timemodified', ['id' => $discussion->id])); $this->assertEquals($pasttime, $DB->get_field('forum_posts', 'modified', ['id' => $reply->id])); // Let the teacher edit the student's reply. $this->setUser($teacher->id); $newpost = (object)[ 'id' => $reply->id, 'itemid' => 0, 'subject' => 'Amended subject', ]; forum_update_post($newpost, null); // The student should still be the last post's author. $this->assertEquals($student->id, $DB->get_field('forum_discussions', 'usermodified', ['id' => $discussion->id])); // The discussion modified time should not have changed. $this->assertEquals($pasttime, $DB->get_field('forum_discussions', 'timemodified', ['id' => $discussion->id])); // The post time should be updated. $this->assertGreaterThan($pasttime, $DB->get_field('forum_posts', 'modified', ['id' => $reply->id])); } public function test_forum_core_calendar_provide_event_action(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create the activity. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id, 'completionreplies' => 5, 'completiondiscussions' => 2)); // Create a calendar event. $event = $this->create_action_event($course->id, $forum->id, \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event. $actionevent = mod_forum_core_calendar_provide_event_action($event, $factory); // Confirm the event was decorated. $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('view'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(7, $actionevent->get_item_count()); $this->assertTrue($actionevent->is_actionable()); } public function test_forum_core_calendar_provide_event_action_in_hidden_section(): void { global $CFG; $this->resetAfterTest(); $this->setAdminUser(); // Create a course. $course = $this->getDataGenerator()->create_course(); // Create a student. $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); // Create the activity. $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id, 'completionreplies' => 5, 'completiondiscussions' => 2)); // Create a calendar event. $event = $this->create_action_event($course->id, $forum->id, \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED); // Set sections 0 as hidden. set_section_visible($course->id, 0, 0); // Now, log out. $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities. $this->setUser(); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event for the student. $actionevent = mod_forum_core_calendar_provide_event_action($event, $factory, $student->id); // Confirm the event is not shown at all. $this->assertNull($actionevent); } public function test_forum_core_calendar_provide_event_action_for_user(): void { global $CFG; $this->resetAfterTest(); $this->setAdminUser(); // Create a course. $course = $this->getDataGenerator()->create_course(); // Create a student. $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); // Create the activity. $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id, 'completionreplies' => 5, 'completiondiscussions' => 2)); // Create a calendar event. $event = $this->create_action_event($course->id, $forum->id, \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED); // Now log out. $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities. $this->setUser(); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event for the student. $actionevent = mod_forum_core_calendar_provide_event_action($event, $factory, $student->id); // Confirm the event was decorated. $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('view'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(7, $actionevent->get_item_count()); $this->assertTrue($actionevent->is_actionable()); } public function test_forum_core_calendar_provide_event_action_as_non_user(): void { global $CFG; $this->resetAfterTest(); $this->setAdminUser(); // Create the activity. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); // Create a calendar event. $event = $this->create_action_event($course->id, $forum->id, \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED); // Log out the user and set force login to true. \core\session\manager::init_empty_session(); $CFG->forcelogin = true; // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event. $actionevent = mod_forum_core_calendar_provide_event_action($event, $factory); // Ensure result was null. $this->assertNull($actionevent); } public function test_forum_core_calendar_provide_event_action_already_completed(): void { global $CFG; $this->resetAfterTest(); $this->setAdminUser(); $CFG->enablecompletion = 1; // Create the activity. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), array('completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS)); // Get some additional data. $cm = get_coursemodule_from_instance('forum', $forum->id); // Create a calendar event. $event = $this->create_action_event($course->id, $forum->id, \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED); // Mark the activity as completed. $completion = new \completion_info($course); $completion->set_module_viewed($cm); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event. $actionevent = mod_forum_core_calendar_provide_event_action($event, $factory); // Ensure result was null. $this->assertNull($actionevent); } public function test_forum_core_calendar_provide_event_action_already_completed_for_user(): void { global $CFG; $this->resetAfterTest(); $this->setAdminUser(); $CFG->enablecompletion = 1; // Create a course. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); // Create a student. $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); // Create the activity. $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), array('completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS)); // Get some additional data. $cm = get_coursemodule_from_instance('forum', $forum->id); // Create a calendar event. $event = $this->create_action_event($course->id, $forum->id, \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED); // Mark the activity as completed for the student. $completion = new \completion_info($course); $completion->set_module_viewed($cm, $student->id); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event. $actionevent = mod_forum_core_calendar_provide_event_action($event, $factory, $student->id); // Ensure result was null. $this->assertNull($actionevent); } public function test_mod_forum_get_tagged_posts(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Setup test data. $forumgenerator = $this->getDataGenerator()->get_plugin_generator('mod_forum'); $course3 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $course1 = $this->getDataGenerator()->create_course(); $forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id)); $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id)); $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $course3->id)); $post11 = $forumgenerator->create_content($forum1, array('tags' => array('Cats', 'Dogs'))); $post12 = $forumgenerator->create_content($forum1, array('tags' => array('Cats', 'mice'))); $post13 = $forumgenerator->create_content($forum1, array('tags' => array('Cats'))); $post14 = $forumgenerator->create_content($forum1); $post15 = $forumgenerator->create_content($forum1, array('tags' => array('Cats'))); $post16 = $forumgenerator->create_content($forum1, array('tags' => array('Cats'), 'hidden' => true)); $post21 = $forumgenerator->create_content($forum2, array('tags' => array('Cats'))); $post22 = $forumgenerator->create_content($forum2, array('tags' => array('Cats', 'Dogs'))); $post23 = $forumgenerator->create_content($forum2, array('tags' => array('mice', 'Cats'))); $post31 = $forumgenerator->create_content($forum3, array('tags' => array('mice', 'Cats'))); $tag = \core_tag_tag::get_by_name(0, 'Cats'); // Admin can see everything. $res = mod_forum_get_tagged_posts($tag, /*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */0, /*$rec = */1, /*$post = */0); $this->assertMatchesRegularExpression('/'.$post11->subject.'content); $this->assertMatchesRegularExpression('/'.$post12->subject.'content); $this->assertMatchesRegularExpression('/'.$post13->subject.'content); $this->assertDoesNotMatchRegularExpression('/'.$post14->subject.'content); $this->assertMatchesRegularExpression('/'.$post15->subject.'content); $this->assertMatchesRegularExpression('/'.$post16->subject.'content); $this->assertDoesNotMatchRegularExpression('/'.$post21->subject.'content); $this->assertDoesNotMatchRegularExpression('/'.$post22->subject.'content); $this->assertDoesNotMatchRegularExpression('/'.$post23->subject.'content); $this->assertDoesNotMatchRegularExpression('/'.$post31->subject.'content); $this->assertEmpty($res->prevpageurl); $this->assertNotEmpty($res->nextpageurl); $res = mod_forum_get_tagged_posts($tag, /*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */0, /*$rec = */1, /*$post = */1); $this->assertDoesNotMatchRegularExpression('/'.$post11->subject.'content); $this->assertDoesNotMatchRegularExpression('/'.$post12->subject.'content); $this->assertDoesNotMatchRegularExpression('/'.$post13->subject.'content); $this->assertDoesNotMatchRegularExpression('/'.$post14->subject.'content); $this->assertDoesNotMatchRegularExpression('/'.$post15->subject.'content); $this->assertDoesNotMatchRegularExpression('/'.$post16->subject.'content); $this->assertMatchesRegularExpression('/'.$post21->subject.'content); $this->assertMatchesRegularExpression('/'.$post22->subject.'content); $this->assertMatchesRegularExpression('/'.$post23->subject.'content); $this->assertMatchesRegularExpression('/'.$post31->subject.'content); $this->assertNotEmpty($res->prevpageurl); $this->assertEmpty($res->nextpageurl); // Create and enrol a user. $student = self::getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($student->id, $course1->id, $studentrole->id, 'manual'); $this->getDataGenerator()->enrol_user($student->id, $course2->id, $studentrole->id, 'manual'); $this->setUser($student); \core_tag_index_builder::reset_caches(); // User can not see posts in course 3 because he is not enrolled. $res = mod_forum_get_tagged_posts($tag, /*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */0, /*$rec = */1, /*$post = */1); $this->assertMatchesRegularExpression('/'.$post22->subject.'/', $res->content); $this->assertMatchesRegularExpression('/'.$post23->subject.'/', $res->content); $this->assertDoesNotMatchRegularExpression('/'.$post31->subject.'/', $res->content); // User can search forum posts inside a course. $coursecontext = \context_course::instance($course1->id); $res = mod_forum_get_tagged_posts($tag, /*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */$coursecontext->id, /*$rec = */1, /*$post = */0); $this->assertMatchesRegularExpression('/'.$post11->subject.'/', $res->content); $this->assertMatchesRegularExpression('/'.$post12->subject.'/', $res->content); $this->assertMatchesRegularExpression('/'.$post13->subject.'/', $res->content); $this->assertDoesNotMatchRegularExpression('/'.$post14->subject.'/', $res->content); $this->assertMatchesRegularExpression('/'.$post15->subject.'/', $res->content); $this->assertMatchesRegularExpression('/'.$post16->subject.'/', $res->content); $this->assertDoesNotMatchRegularExpression('/'.$post21->subject.'/', $res->content); $this->assertDoesNotMatchRegularExpression('/'.$post22->subject.'/', $res->content); $this->assertDoesNotMatchRegularExpression('/'.$post23->subject.'/', $res->content); $this->assertEmpty($res->nextpageurl); } /** * Creates an action event. * * @param int $courseid The course id. * @param int $instanceid The instance id. * @param string $eventtype The event type. * @return bool|calendar_event */ private function create_action_event($courseid, $instanceid, $eventtype) { $event = new \stdClass(); $event->name = 'Calendar event'; $event->modulename = 'forum'; $event->courseid = $courseid; $event->instance = $instanceid; $event->type = CALENDAR_EVENT_TYPE_ACTION; $event->eventtype = $eventtype; $event->timestart = time(); return \calendar_event::create($event); } /** * Test the callback responsible for returning the completion rule descriptions. * This function should work given either an instance of the module (cm_info), such as when checking the active rules, * or if passed a stdClass of similar structure, such as when checking the the default completion settings for a mod type. */ public function test_mod_forum_completion_get_active_rule_descriptions(): void { $this->resetAfterTest(); $this->setAdminUser(); // Two activities, both with automatic completion. One has the 'completionsubmit' rule, one doesn't. $course = $this->getDataGenerator()->create_course(['enablecompletion' => 2]); $forum1 = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, 'completion' => 2, 'completiondiscussions' => 3, 'completionreplies' => 3, 'completionposts' => 3 ]); $forum2 = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, 'completion' => 2, 'completiondiscussions' => 0, 'completionreplies' => 0, 'completionposts' => 0 ]); $cm1 = \cm_info::create(get_coursemodule_from_instance('forum', $forum1->id)); $cm2 = \cm_info::create(get_coursemodule_from_instance('forum', $forum2->id)); // Data for the stdClass input type. // This type of input would occur when checking the default completion rules for an activity type, where we don't have // any access to cm_info, rather the input is a stdClass containing completion and customdata attributes, just like cm_info. $moddefaults = new \stdClass(); $moddefaults->customdata = ['customcompletionrules' => [ 'completiondiscussions' => 3, 'completionreplies' => 3, 'completionposts' => 3 ]]; $moddefaults->completion = 2; $activeruledescriptions = [ get_string('completiondiscussionsdesc', 'forum', 3), get_string('completionrepliesdesc', 'forum', 3), get_string('completionpostsdesc', 'forum', 3) ]; $this->assertEquals(mod_forum_get_completion_active_rule_descriptions($cm1), $activeruledescriptions); $this->assertEquals(mod_forum_get_completion_active_rule_descriptions($cm2), []); $this->assertEquals(mod_forum_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions); $this->assertEquals(mod_forum_get_completion_active_rule_descriptions(new \stdClass()), []); } /** * Test the forum_post_is_visible_privately function used in private replies. */ public function test_forum_post_is_visible_privately(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $context = \context_module::instance($forum->cmid); $cm = get_coursemodule_from_instance('forum', $forum->id); $author = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($author->id, $course->id); $recipient = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($recipient->id, $course->id); $privilegeduser = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($privilegeduser->id, $course->id, 'editingteacher'); $otheruser = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($otheruser->id, $course->id); // Fake a post - this does not need to be persisted to the DB. $post = new \stdClass(); $post->userid = $author->id; $post->privatereplyto = $recipient->id; // The user is the author. $this->setUser($author->id); $this->assertTrue(forum_post_is_visible_privately($post, $cm)); // The user is the intended recipient. $this->setUser($recipient->id); $this->assertTrue(forum_post_is_visible_privately($post, $cm)); // The user is not the author or recipient, but does have the readprivatereplies capability. $this->setUser($privilegeduser->id); $this->assertTrue(forum_post_is_visible_privately($post, $cm)); // The user is not allowed to view this post. $this->setUser($otheruser->id); $this->assertFalse(forum_post_is_visible_privately($post, $cm)); } /** * An unkown event type should not have any limits */ public function test_mod_forum_core_calendar_get_valid_event_timestart_range_unknown_event(): void { global $CFG; require_once($CFG->dirroot . "/calendar/lib.php"); $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $duedate = time() + DAYSECS; $forum = new \stdClass(); $forum->duedate = $duedate; // Create a valid event. $event = new \calendar_event([ 'name' => 'Test event', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'forum', 'instance' => 1, 'eventtype' => FORUM_EVENT_TYPE_DUE . "SOMETHING ELSE", 'timestart' => 1, 'timeduration' => 86400, 'visible' => 1 ]); list ($min, $max) = mod_forum_core_calendar_get_valid_event_timestart_range($event, $forum); $this->assertNull($min); $this->assertNull($max); } /** * Forums configured without a cutoff date should not have any limits applied. */ public function test_mod_forum_core_calendar_get_valid_event_timestart_range_due_no_limit(): void { global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $duedate = time() + DAYSECS; $forum = new \stdClass(); $forum->duedate = $duedate; // Create a valid event. $event = new \calendar_event([ 'name' => 'Test event', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'forum', 'instance' => 1, 'eventtype' => FORUM_EVENT_TYPE_DUE, 'timestart' => 1, 'timeduration' => 86400, 'visible' => 1 ]); list($min, $max) = mod_forum_core_calendar_get_valid_event_timestart_range($event, $forum); $this->assertNull($min); $this->assertNull($max); } /** * Forums should be top bound by the cutoff date. */ public function test_mod_forum_core_calendar_get_valid_event_timestart_range_due_with_limits(): void { global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $duedate = time() + DAYSECS; $cutoffdate = $duedate + DAYSECS; $forum = new \stdClass(); $forum->duedate = $duedate; $forum->cutoffdate = $cutoffdate; // Create a valid event. $event = new \calendar_event([ 'name' => 'Test event', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'forum', 'instance' => 1, 'eventtype' => FORUM_EVENT_TYPE_DUE, 'timestart' => 1, 'timeduration' => 86400, 'visible' => 1 ]); list($min, $max) = mod_forum_core_calendar_get_valid_event_timestart_range($event, $forum); $this->assertNull($min); $this->assertEquals($cutoffdate, $max[0]); $this->assertNotEmpty($max[1]); } /** * An unknown event type should not change the forum instance. */ public function test_mod_forum_core_calendar_event_timestart_updated_unknown_event(): void { global $CFG, $DB; require_once($CFG->dirroot . "/calendar/lib.php"); $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $forumgenerator = $generator->get_plugin_generator('mod_forum'); $duedate = time() + DAYSECS; $cutoffdate = $duedate + DAYSECS; $forum = $forumgenerator->create_instance(['course' => $course->id]); $forum->duedate = $duedate; $forum->cutoffdate = $cutoffdate; $DB->update_record('forum', $forum); // Create a valid event. $event = new \calendar_event([ 'name' => 'Test event', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'forum', 'instance' => $forum->id, 'eventtype' => FORUM_EVENT_TYPE_DUE . "SOMETHING ELSE", 'timestart' => 1, 'timeduration' => 86400, 'visible' => 1 ]); mod_forum_core_calendar_event_timestart_updated($event, $forum); $forum = $DB->get_record('forum', ['id' => $forum->id]); $this->assertEquals($duedate, $forum->duedate); $this->assertEquals($cutoffdate, $forum->cutoffdate); } /** * Due date events should update the forum due date. */ public function test_mod_forum_core_calendar_event_timestart_updated_due_event(): void { global $CFG, $DB; require_once($CFG->dirroot . "/calendar/lib.php"); $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $forumgenerator = $generator->get_plugin_generator('mod_forum'); $duedate = time() + DAYSECS; $cutoffdate = $duedate + DAYSECS; $newduedate = $duedate + 1; $forum = $forumgenerator->create_instance(['course' => $course->id]); $forum->duedate = $duedate; $forum->cutoffdate = $cutoffdate; $DB->update_record('forum', $forum); // Create a valid event. $event = new \calendar_event([ 'name' => 'Test event', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'forum', 'instance' => $forum->id, 'eventtype' => FORUM_EVENT_TYPE_DUE, 'timestart' => $newduedate, 'timeduration' => 86400, 'visible' => 1 ]); mod_forum_core_calendar_event_timestart_updated($event, $forum); $forum = $DB->get_record('forum', ['id' => $forum->id]); $this->assertEquals($newduedate, $forum->duedate); $this->assertEquals($cutoffdate, $forum->cutoffdate); } /** * Test forum_get_layout_modes function. */ public function test_forum_get_layout_modes(): void { $expectednormal = [ FORUM_MODE_FLATOLDEST => get_string('modeflatoldestfirst', 'forum'), FORUM_MODE_FLATNEWEST => get_string('modeflatnewestfirst', 'forum'), FORUM_MODE_THREADED => get_string('modethreaded', 'forum'), FORUM_MODE_NESTED => get_string('modenested', 'forum') ]; $expectedexperimental = [ FORUM_MODE_FLATOLDEST => get_string('modeflatoldestfirst', 'forum'), FORUM_MODE_FLATNEWEST => get_string('modeflatnewestfirst', 'forum'), FORUM_MODE_THREADED => get_string('modethreaded', 'forum'), FORUM_MODE_NESTED_V2 => get_string('modenestedv2', 'forum') ]; $this->assertEquals($expectednormal, forum_get_layout_modes()); $this->assertEquals($expectednormal, forum_get_layout_modes(false)); $this->assertEquals($expectedexperimental, forum_get_layout_modes(true)); } /** * Provides data for tests that cause forum_check_throttling to return early. * * @return array */ public static function forum_check_throttling_early_returns_provider(): array { return [ 'Empty blockafter' => [(object)['id' => 1, 'course' => SITEID, 'blockafter' => 0]], 'Empty blockperiod' => [(object)['id' => 1, 'course' => SITEID, 'blockafter' => DAYSECS, 'blockperiod' => 0]], ]; } /** * Tests the early return scenarios of forum_check_throttling. * * @dataProvider forum_check_throttling_early_returns_provider * @covers ::forum_check_throttling * @param \stdClass $forum The forum data. */ public function test_forum_check_throttling_early_returns(\stdClass $forum): void { $this->assertFalse(forum_check_throttling($forum)); } /** * Provides data for tests that cause forum_check_throttling to throw exceptions early. * * @return array */ public static function forum_check_throttling_early_exceptions_provider(): array { return [ 'Non-object forum' => ['a'], 'Forum ID not set' => [(object)['id' => false]], 'Course ID not set' => [(object)['id' => 1]], ]; } /** * Tests the early exception scenarios of forum_check_throttling. * * @dataProvider forum_check_throttling_early_exceptions_provider * @covers ::forum_check_throttling * @param mixed $forum The forum data. */ public function test_forum_check_throttling_early_exceptions($forum): void { $this->expectException(\coding_exception::class); $this->assertFalse(forum_check_throttling($forum)); } /** * Tests forum_check_throttling when a non-existent numeric ID is passed for its forum parameter. * * @covers ::forum_check_throttling */ public function test_forum_check_throttling_nonexistent_numeric_id(): void { $this->resetAfterTest(); $this->expectException(\moodle_exception::class); forum_check_throttling(1); } /** * Tests forum_check_throttling when a non-existent forum record is passed for its forum parameter. * * @covers ::forum_check_throttling */ public function test_forum_check_throttling_nonexistent_forum_cm(): void { $this->resetAfterTest(); $dummyforum = (object)[ 'id' => 1, 'course' => SITEID, 'blockafter' => 2, 'blockperiod' => DAYSECS, ]; $this->expectException(\moodle_exception::class); forum_check_throttling($dummyforum); } /** * Tests forum_check_throttling when a user with the 'mod/forum:postwithoutthrottling' capability. * * @covers ::forum_check_throttling */ public function test_forum_check_throttling_teacher(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $teacher = $generator->create_and_enrol($course, 'teacher'); /** @var mod_forum_generator $forumgenerator */ $forumgenerator = $generator->get_plugin_generator('mod_forum'); // Forum that limits students from creating more than two posts per day. $forum = $forumgenerator->create_instance( [ 'course' => $course->id, 'blockafter' => 2, 'blockperiod' => DAYSECS, ] ); $this->setUser($teacher); $discussionrecord = [ 'course' => $course->id, 'forum' => $forum->id, 'userid' => $teacher->id, ]; $discussion = $forumgenerator->create_discussion($discussionrecord); // Create a forum post as the teacher. $postrecord = [ 'userid' => $teacher->id, 'discussion' => $discussion->id, ]; $forumgenerator->create_post($postrecord); // Create another forum post. $forumgenerator->create_post($postrecord); $this->assertFalse(forum_check_throttling($forum)); } /** * Tests forum_check_throttling for students. * * @covers ::forum_check_throttling */ public function test_forum_check_throttling_student(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $student = $generator->create_and_enrol($course, 'student'); /** @var mod_forum_generator $forumgenerator */ $forumgenerator = $generator->get_plugin_generator('mod_forum'); // Forum that limits students from creating more than two posts per day. $forum = $forumgenerator->create_instance( [ 'course' => $course->id, 'blockafter' => 2, 'blockperiod' => DAYSECS, 'warnafter' => 1, ] ); $this->setUser($student); // Student hasn't posted yet so no warning will be shown. $throttling = forum_check_throttling($forum); $this->assertFalse($throttling); // Create a discussion. $discussionrecord = [ 'course' => $course->id, 'forum' => $forum->id, 'userid' => $student->id, ]; $discussion = $forumgenerator->create_discussion($discussionrecord); // A warning will be shown to the student, but they should still be able to post. $throttling = forum_check_throttling($forum); $this->assertIsObject($throttling); $this->assertTrue($throttling->canpost); // Create another forum post as the student. $postrecord = [ 'userid' => $student->id, 'discussion' => $discussion->id, ]; $forumgenerator->create_post($postrecord); // Student should now be unable to post after their second post. $throttling = forum_check_throttling($forum); $this->assertIsObject($throttling); $this->assertFalse($throttling->canpost); } /** * Tests forum_count_discussions. * * @covers ::forum_count_discussions */ public function test_forum_count_discussions(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $forumgenerator = $generator->get_plugin_generator('mod_forum'); $course1 = $generator->create_course(); $course2 = $generator->create_course(); $student = $generator->create_user(['trackforums' => 1]); // First forum. $forumobj1 = new \stdClass(); $forumobj1->introformat = FORMAT_HTML; $forumobj1->course = $course1->id; $forumobj1->trackingtype = FORUM_TRACKING_FORCED; $forum1 = $generator->create_module('forum', $forumobj1); $forum1cm = get_coursemodule_from_id('forum', $forum1->cmid, 0, false, MUST_EXIST); // Second forum. $forumobj2 = new \stdClass(); $forumobj2->introformat = FORMAT_HTML; $forumobj2->course = $course2->id; $forumobj2->trackingtype = FORUM_TRACKING_OFF; $forum2 = $generator->create_module('forum', $forumobj2); $forum2cm = get_coursemodule_from_id('forum', $forum2->cmid, 0, false, MUST_EXIST); // Third forum. $forumobj3 = new \stdClass(); $forumobj3->introformat = FORMAT_HTML; $forumobj3->course = $course2->id; $forumobj3->trackingtype = FORUM_TRACKING_OFF; $forum3 = $generator->create_module('forum', $forumobj3); $forum3cm = get_coursemodule_from_id('forum', $forum3->cmid, 0, false, MUST_EXIST); // First make sure there are no discussions for any of the forums. $f1discussionscount = forum_count_discussions($forum1, $forum1cm, $course1); $this->assertEquals(0, $f1discussionscount); $f2discussionscount = forum_count_discussions($forum2, $forum2cm, $course2); $this->assertEquals(0, $f2discussionscount); $f3discussionscount = forum_count_discussions($forum3, $forum3cm, $course2); $this->assertEquals(0, $f3discussionscount); // Add 3 discussions to forum 1. $discussionobj1 = new \stdClass(); $discussionobj1->course = $course1->id; $discussionobj1->userid = $student->id; $discussionobj1->forum = $forum1->id; $forumgenerator->create_discussion($discussionobj1); $forumgenerator->create_discussion($discussionobj1); $forumgenerator->create_discussion($discussionobj1); // Make sure there are 3 discussions. $f1discussionscount = forum_count_discussions($forum1, $forum1cm, $course1); $this->assertEquals(3, $f1discussionscount); // Add 4 discussions to forum 2. $discussionobj2 = new \stdClass(); $discussionobj2->course = $course2->id; $discussionobj2->userid = $student->id; $discussionobj2->forum = $forum2->id; $forumgenerator->create_discussion($discussionobj2); $forumgenerator->create_discussion($discussionobj2); $forumgenerator->create_discussion($discussionobj2); $discussion24 = $forumgenerator->create_discussion($discussionobj2); // Make sure there are 4 discussions. $f2discussionscount = forum_count_discussions($forum2, $forum2cm, $course2); $this->assertEquals(4, $f2discussionscount); // Delete one discussion from forum 2. forum_delete_discussion($discussion24, true, $course2, $forum2cm, $forum2); $f2discussionscount = forum_count_discussions($forum2, $forum2cm, $course2); $this->assertEquals(3, $f2discussionscount); // Make sure there are no discussions. $f3discussionscount = forum_count_discussions($forum3, $forum3cm, $course2); $this->assertEquals(0, $f3discussionscount); } /** * @covers ::forum_reset_userdata */ public function test_forum_reset_userdata(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $now = time(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, 'duedate' => $now + HOURSECS, 'cutoffdate' => $now + DAYSECS, ]); forum_reset_userdata((object) [ 'courseid' => $course->id, 'timeshift' => DAYSECS * 2, ]); // Reload the instance data. $instance = $DB->get_record('forum', ['id' => $forum->id]); $this->assertEquals($forum->duedate + (DAYSECS * 2), $instance->duedate); $this->assertEquals($forum->cutoffdate + (DAYSECS * 2), $instance->cutoffdate); } } PK-B\|g g *tests/entities_discussion_summary_test.phpnu[. namespace mod_forum; use mod_forum\local\entities\author as author_entity; use mod_forum\local\entities\discussion as discussion_entity; use mod_forum\local\entities\discussion_summary as discussion_summary_entity; use mod_forum\local\entities\post as post_entity; /** * The discussion_summary entity tests. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class entities_discussion_summary_test extends \advanced_testcase { /** * Test the entity returns expected values. */ public function test_entity(): void { $this->resetAfterTest(); $firstauthor = new author_entity( 1, 2, 'test', 'person', 'test person', 'test@example.com', false ); $lastauthor = new author_entity( 2, 3, 'test 2', 'person 2', 'test 2 person 2', 'test2@example.com', false ); $discussion = new discussion_entity( 1, 1, 1, 'test discussion', 1, 1, 0, false, time(), time(), 0, 0, false, 0 ); $firstpost = new post_entity( 1, 1, 0, 1, time(), time(), true, 'post subject', 'post message', 1, true, false, 0, false, false, false, null, null ); $discussionsummary = new discussion_summary_entity($discussion, $firstpost, $firstauthor, $lastauthor); $this->assertEquals($discussion, $discussionsummary->get_discussion()); $this->assertEquals($firstauthor, $discussionsummary->get_first_post_author()); $this->assertEquals($lastauthor, $discussionsummary->get_latest_post_author()); $this->assertEquals($firstpost, $discussionsummary->get_first_post()); } } PK-B\&8Y 66%tests/vaults_post_attachment_test.phpnu[. namespace mod_forum; use mod_forum_tests_generator_trait; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/generator_trait.php'); /** * The post_attachment vault tests. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class vaults_post_attachment_test extends \advanced_testcase { // Make use of the test generator trait. use mod_forum_tests_generator_trait; /** * Test get_attachments_for_posts. */ public function test_get_attachments_for_posts(): void { $this->resetAfterTest(); $filestorage = get_file_storage(); $entityfactory = \mod_forum\local\container::get_entity_factory(); $vaultfactory = \mod_forum\local\container::get_vault_factory(); $vault = $vaultfactory->get_post_attachment_vault(); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); $coursemodule = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($coursemodule->id); [$discussion, $post1] = $this->helper_post_to_forum($forum, $user); $post2 = $this->helper_reply_to_post($post1, $user); $post3 = $this->helper_reply_to_post($post1, $user); $attachment1 = $filestorage->create_file_from_string( [ 'contextid' => $context->id, 'component' => 'mod_forum', 'filearea' => 'attachment', 'itemid' => $post1->id, 'filepath' => '/', 'filename' => 'example1.jpg', ], 'image contents' ); $attachment2 = $filestorage->create_file_from_string( [ 'contextid' => $context->id, 'component' => 'mod_forum', 'filearea' => 'attachment', 'itemid' => $post2->id, 'filepath' => '/', 'filename' => 'example2.jpg', ], 'image contents' ); $post1 = $entityfactory->get_post_from_stdClass($post1); $post2 = $entityfactory->get_post_from_stdClass($post2); $post3 = $entityfactory->get_post_from_stdClass($post3); $results = $vault->get_attachments_for_posts(\context_system::instance(), [$post1, $post2, $post3]); $this->assertCount(3, $results); $this->assertEquals([], $results[$post1->get_id()]); $this->assertEquals([], $results[$post2->get_id()]); $this->assertEquals([], $results[$post3->get_id()]); $results = $vault->get_attachments_for_posts($context, [$post1]); $this->assertCount(1, $results); $this->assertEquals($attachment1->get_filename(), $results[$post1->get_id()][0]->get_filename()); $results = $vault->get_attachments_for_posts($context, [$post1, $post2]); $this->assertCount(2, $results); $this->assertEquals($attachment1->get_filename(), $results[$post1->get_id()][0]->get_filename()); $this->assertEquals($attachment2->get_filename(), $results[$post2->get_id()][0]->get_filename()); $results = $vault->get_attachments_for_posts($context, [$post1, $post2, $post3]); $this->assertCount(3, $results); $this->assertEquals($attachment1->get_filename(), $results[$post1->get_id()][0]->get_filename()); $this->assertEquals($attachment2->get_filename(), $results[$post2->get_id()][0]->get_filename()); $this->assertEquals([], $results[$post3->get_id()]); } /** * Test get_inline_attachments_for_posts. */ public function test_get_inline_attachments_for_posts(): void { $this->resetAfterTest(); $filestorage = get_file_storage(); $entityfactory = \mod_forum\local\container::get_entity_factory(); $vaultfactory = \mod_forum\local\container::get_vault_factory(); $vault = $vaultfactory->get_post_attachment_vault(); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); $coursemodule = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($coursemodule->id); [$discussion, $post1] = $this->helper_post_to_forum($forum, $user); $post2 = $this->helper_reply_to_post($post1, $user); $post3 = $this->helper_reply_to_post($post1, $user); $attachment1 = $filestorage->create_file_from_string( [ 'contextid' => $context->id, 'component' => 'mod_forum', 'filearea' => 'post', 'itemid' => $post1->id, 'filepath' => '/', 'filename' => 'example1.jpg', ], 'image contents' ); $attachment2 = $filestorage->create_file_from_string( [ 'contextid' => $context->id, 'component' => 'mod_forum', 'filearea' => 'post', 'itemid' => $post2->id, 'filepath' => '/', 'filename' => 'example2.jpg', ], 'image contents' ); $post1 = $entityfactory->get_post_from_stdClass($post1); $post2 = $entityfactory->get_post_from_stdClass($post2); $post3 = $entityfactory->get_post_from_stdClass($post3); $results = $vault->get_inline_attachments_for_posts(\context_system::instance(), [$post1, $post2, $post3]); $this->assertCount(3, $results); $this->assertEquals([], $results[$post1->get_id()]); $this->assertEquals([], $results[$post2->get_id()]); $this->assertEquals([], $results[$post3->get_id()]); $results = $vault->get_inline_attachments_for_posts($context, [$post1]); $this->assertCount(1, $results); $this->assertEquals($attachment1->get_filename(), $results[$post1->get_id()][0]->get_filename()); $results = $vault->get_inline_attachments_for_posts($context, [$post1, $post2]); $this->assertCount(2, $results); $this->assertEquals($attachment1->get_filename(), $results[$post1->get_id()][0]->get_filename()); $this->assertEquals($attachment2->get_filename(), $results[$post2->get_id()][0]->get_filename()); $results = $vault->get_inline_attachments_for_posts($context, [$post1, $post2, $post3]); $this->assertCount(3, $results); $this->assertEquals($attachment1->get_filename(), $results[$post1->get_id()][0]->get_filename()); $this->assertEquals($attachment2->get_filename(), $results[$post2->get_id()][0]->get_filename()); $this->assertEquals([], $results[$post3->get_id()]); } } PK-B\.͇^{{tests/maildigest_test.phpnu[. namespace mod_forum; use mod_forum_tests_cron_trait; use mod_forum_tests_generator_trait; defined('MOODLE_INTERNAL') || die; require_once(__DIR__ . '/cron_trait.php'); require_once(__DIR__ . '/generator_trait.php'); /** * The module forums external functions unit tests * * @package mod_forum * @category test * @copyright 2013 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class maildigest_test extends \advanced_testcase { // Make use of the cron tester trait. use mod_forum_tests_cron_trait; // Make use of the test generator trait. use mod_forum_tests_generator_trait; /** @var \phpunit_message_sink */ protected $messagesink; /** @var \phpunit_message_sink */ protected $mailsink; /** * Set up message and mail sinks, and set up other requirements for the * cron to be tested here. */ public function setUp(): void { global $CFG; parent::setUp(); // Messaging is not compatible with transactions... $this->preventResetByRollback(); // Catch all messages $this->messagesink = $this->redirectMessages(); $this->mailsink = $this->redirectEmails(); // Confirm that we have an empty message sink so far. $messages = $this->messagesink->get_messages_by_component('mod_forum'); $this->assertEquals(0, count($messages)); $messages = $this->mailsink->get_messages(); $this->assertEquals(0, count($messages)); // Tell Moodle that we've not sent any digest messages out recently. $CFG->digestmailtimelast = 0; // And set the digest sending time to a negative number - this has // the effect of making it 11pm the previous day. $CFG->digestmailtime = -1; // Forcibly reduce the maxeditingtime to a one second to ensure that // messages are sent out. $CFG->maxeditingtime = 1; // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); \mod_forum\subscriptions::reset_discussion_cache(); } /** * Clear the message sinks set up in this test. */ public function tearDown(): void { $this->messagesink->clear(); $this->messagesink->close(); $this->mailsink->clear(); $this->mailsink->close(); parent::tearDown(); } /** * Setup a user, course, and forums. * * @return stdClass containing the list of forums, courses, forumids, * and the user enrolled in them. */ protected function helper_setup_user_in_course() { global $DB; $return = new \stdClass(); $return->courses = new \stdClass(); $return->forums = new \stdClass(); $return->forumids = array(); // Create a user. $user = $this->getDataGenerator()->create_user(); $return->user = $user; // Create courses to add the modules. $return->courses->course1 = $this->getDataGenerator()->create_course(); // Create forums. $record = new \stdClass(); $record->course = $return->courses->course1->id; $record->forcesubscribe = 1; $return->forums->forum1 = $this->getDataGenerator()->create_module('forum', $record); $return->forumsids[] = $return->forums->forum1->id; $return->forums->forum2 = $this->getDataGenerator()->create_module('forum', $record); $return->forumsids[] = $return->forums->forum2->id; // Check the forum was correctly created. list ($test, $params) = $DB->get_in_or_equal($return->forumsids); // Enrol the user in the courses. // DataGenerator->enrol_user automatically sets a role for the user $this->getDataGenerator()->enrol_user($return->user->id, $return->courses->course1->id); return $return; } public function test_set_maildigest(): void { global $DB; $this->resetAfterTest(true); $helper = $this->helper_setup_user_in_course(); $user = $helper->user; $course1 = $helper->courses->course1; $forum1 = $helper->forums->forum1; // Set to the user. self::setUser($helper->user); // Confirm that there is no current value. $currentsetting = $DB->get_record('forum_digests', array( 'forum' => $forum1->id, 'userid' => $user->id, )); $this->assertFalse($currentsetting); // Test with each of the valid values: // 0, 1, and 2 are valid values. forum_set_user_maildigest($forum1, 0, $user); $currentsetting = $DB->get_record('forum_digests', array( 'forum' => $forum1->id, 'userid' => $user->id, )); $this->assertEquals($currentsetting->maildigest, 0); forum_set_user_maildigest($forum1, 1, $user); $currentsetting = $DB->get_record('forum_digests', array( 'forum' => $forum1->id, 'userid' => $user->id, )); $this->assertEquals($currentsetting->maildigest, 1); forum_set_user_maildigest($forum1, 2, $user); $currentsetting = $DB->get_record('forum_digests', array( 'forum' => $forum1->id, 'userid' => $user->id, )); $this->assertEquals($currentsetting->maildigest, 2); // And the default value - this should delete the record again forum_set_user_maildigest($forum1, -1, $user); $currentsetting = $DB->get_record('forum_digests', array( 'forum' => $forum1->id, 'userid' => $user->id, )); $this->assertFalse($currentsetting); // Try with an invalid value. $this->expectException('moodle_exception'); forum_set_user_maildigest($forum1, 42, $user); } public function test_get_user_digest_options_default(): void { global $USER, $DB; $this->resetAfterTest(true); // Set up a basic user enrolled in a course. $helper = $this->helper_setup_user_in_course(); $user = $helper->user; $course1 = $helper->courses->course1; $forum1 = $helper->forums->forum1; // Set to the user. self::setUser($helper->user); // We test against these options. $digestoptions = array( '0' => get_string('emaildigestoffshort', 'mod_forum'), '1' => get_string('emaildigestcompleteshort', 'mod_forum'), '2' => get_string('emaildigestsubjectsshort', 'mod_forum'), ); // The default settings is 0. $this->assertEquals(0, $user->maildigest); $options = forum_get_user_digest_options(); $this->assertEquals($options[-1], get_string('emaildigestdefault', 'mod_forum', $digestoptions[0])); // Update the setting to 1. $USER->maildigest = 1; $this->assertEquals(1, $USER->maildigest); $options = forum_get_user_digest_options(); $this->assertEquals($options[-1], get_string('emaildigestdefault', 'mod_forum', $digestoptions[1])); // Update the setting to 2. $USER->maildigest = 2; $this->assertEquals(2, $USER->maildigest); $options = forum_get_user_digest_options(); $this->assertEquals($options[-1], get_string('emaildigestdefault', 'mod_forum', $digestoptions[2])); } public function test_get_user_digest_options_sorting(): void { global $USER, $DB; $this->resetAfterTest(true); // Set up a basic user enrolled in a course. $helper = $this->helper_setup_user_in_course(); $user = $helper->user; $course1 = $helper->courses->course1; $forum1 = $helper->forums->forum1; // Set to the user. self::setUser($helper->user); // Retrieve the list of applicable options. $options = forum_get_user_digest_options(); // The default option must always be at the top of the list. $lastoption = -2; foreach ($options as $value => $description) { $this->assertGreaterThan($lastoption, $value); $lastoption = $value; } } public function test_cron_no_posts(): void { global $DB; $this->resetAfterTest(true); // Initially the forum cron should generate no messages as we've made no posts. $expect = []; $this->queue_tasks_and_assert($expect); } /** * Sends several notifications to one user as: * * single messages based on a user profile setting. */ public function test_cron_profile_single_mails(): void { global $DB; $this->resetAfterTest(true); // Set up a basic user enrolled in a course. $userhelper = $this->helper_setup_user_in_course(); $user = $userhelper->user; $course1 = $userhelper->courses->course1; $forum1 = $userhelper->forums->forum1; $forum2 = $userhelper->forums->forum2; // Add 5 discussions to forum 1. $posts = []; for ($i = 0; $i < 5; $i++) { list($discussion, $post) = $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]); $posts[] = $post; } // Add 5 discussions to forum 2. for ($i = 0; $i < 5; $i++) { list($discussion, $post) = $this->helper_post_to_forum($forum2, $user, ['mailnow' => 1]); $posts[] = $post; } // Set the tested user's default maildigest setting. $DB->set_field('user', 'maildigest', 0, array('id' => $user->id)); // Set the maildigest preference for forum1 to default. forum_set_user_maildigest($forum1, -1, $user); // Set the maildigest preference for forum2 to default. forum_set_user_maildigest($forum2, -1, $user); // No digests mails should be sent, but 10 forum mails will be sent. $expect = [ (object) [ 'userid' => $user->id, 'messages' => 10, 'digests' => 0, ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($user, $posts); } /** * Sends several notifications to one user as: * * daily digests coming from the user profile setting. */ public function test_cron_profile_digest_email(): void { global $DB, $CFG; $this->resetAfterTest(true); // Set up a basic user enrolled in a course. $userhelper = $this->helper_setup_user_in_course(); $user = $userhelper->user; $course1 = $userhelper->courses->course1; $forum1 = $userhelper->forums->forum1; $forum2 = $userhelper->forums->forum2; $posts = []; // Add 5 discussions to forum 1. for ($i = 0; $i < 5; $i++) { list($discussion, $post) = $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]); $posts[] = $post; } // Add 5 discussions to forum 2. for ($i = 0; $i < 5; $i++) { list($discussion, $post) = $this->helper_post_to_forum($forum2, $user, ['mailnow' => 1]); $posts[] = $post; } // Set the tested user's default maildigest setting. $DB->set_field('user', 'maildigest', 1, array('id' => $user->id)); // Set the maildigest preference for forum1 to default. forum_set_user_maildigest($forum1, -1, $user); // Set the maildigest preference for forum2 to default. forum_set_user_maildigest($forum2, -1, $user); // No digests mails should be sent, but 10 forum mails will be sent. $expect = [ (object) [ 'userid' => $user->id, 'messages' => 0, 'digests' => 1, ], ]; $this->queue_tasks_and_assert($expect); $this->send_digests_and_assert($user, $posts); } /** * Send digests to a user who cannot view fullnames */ public function test_cron_digest_view_fullnames_off(): void { global $DB, $CFG; $CFG->fullnamedisplay = 'lastname'; $this->resetAfterTest(true); // Set up a basic user enrolled in a course. $userhelper = $this->helper_setup_user_in_course(); $user = $userhelper->user; $course1 = $userhelper->courses->course1; $forum1 = $userhelper->forums->forum1; $posts = []; // Add 1 discussions to forum 1. list($discussion, $post) = $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]); $posts[] = $post; // Set the tested user's default maildigest setting. $DB->set_field('user', 'maildigest', 1, array('id' => $user->id)); // No digests mails should be sent, but 1 forum mails will be sent. $expect = [ (object) [ 'userid' => $user->id, 'messages' => 0, 'digests' => 1, ], ]; $this->queue_tasks_and_assert($expect); $this->send_digests_and_assert($user, $posts); // The user does not, by default, have permission to view the fullname. $messages = $this->messagesink->get_messages_by_component('mod_forum'); $messages = reset($messages); $messagecontent = $messages->fullmessage; // Assert that the expected name is present (lastname only). $this->assertStringContainsString(fullname($user, false), $messagecontent); // Assert that the full name is not present (firstname lastname only). $this->assertStringNotContainsString(fullname($user, true), $messagecontent); } /** * Send digests to a user who can view fullnames. */ public function test_cron_digest_view_fullnames_on(): void { global $DB, $CFG; $CFG->fullnamedisplay = 'lastname'; $this->resetAfterTest(true); // Set up a basic user enrolled in a course. $userhelper = $this->helper_setup_user_in_course(); $user = $userhelper->user; $course1 = $userhelper->courses->course1; $forum1 = $userhelper->forums->forum1; $posts = []; assign_capability( 'moodle/site:viewfullnames', CAP_ALLOW, $DB->get_field('role', 'id', ['shortname' => 'student']), \context_course::instance($course1->id) ); // Add 1 discussions to forum 1. list($discussion, $post) = $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]); $posts[] = $post; // Set the tested user's default maildigest setting. $DB->set_field('user', 'maildigest', 1, array('id' => $user->id)); // No digests mails should be sent, but 1 forum mails will be sent. $expect = [ (object) [ 'userid' => $user->id, 'messages' => 0, 'digests' => 1, ], ]; $this->queue_tasks_and_assert($expect); $this->send_digests_and_assert($user, $posts); // The user does not, by default, have permission to view the fullname. // However we have given the user that capability so we expect to see both firstname and lastname. $messages = $this->messagesink->get_messages_by_component('mod_forum'); $messages = reset($messages); $messagecontent = $messages->fullmessage; // Assert that the expected name is present (lastname only). $this->assertStringContainsString(fullname($user, false), $messagecontent); // Assert that the full name is also present (firstname lastname only). $this->assertStringContainsString(fullname($user, true), $messagecontent); } /** * Sends several notifications to one user as: * * daily digests coming from the per-forum setting; and * * single e-mails from the profile setting. */ public function test_cron_mixed_email_1(): void { global $DB, $CFG; $this->resetAfterTest(true); // Set up a basic user enrolled in a course. $userhelper = $this->helper_setup_user_in_course(); $user = $userhelper->user; $course1 = $userhelper->courses->course1; $forum1 = $userhelper->forums->forum1; $forum2 = $userhelper->forums->forum2; $posts = []; $digests = []; // Add 5 discussions to forum 1. for ($i = 0; $i < 5; $i++) { list($discussion, $post) = $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]); $digests[] = $post; } // Add 5 discussions to forum 2. for ($i = 0; $i < 5; $i++) { list($discussion, $post) = $this->helper_post_to_forum($forum2, $user, ['mailnow' => 1]); $posts[] = $post; } // Set the tested user's default maildigest setting. $DB->set_field('user', 'maildigest', 0, array('id' => $user->id)); // Set the maildigest preference for forum1 to digest. forum_set_user_maildigest($forum1, 1, $user); // Set the maildigest preference for forum2 to default (single). forum_set_user_maildigest($forum2, -1, $user); // One digest e-mail should be sent, and five individual notifications. $expect = [ (object) [ 'userid' => $user->id, 'messages' => 5, 'digests' => 1, ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($user, $posts); $this->send_digests_and_assert($user, $digests); } /** * Sends several notifications to one user as: * * single e-mails from the per-forum setting; and * * daily digests coming from the per-user setting. */ public function test_cron_mixed_email_2(): void { global $DB, $CFG; $this->resetAfterTest(true); // Set up a basic user enrolled in a course. $userhelper = $this->helper_setup_user_in_course(); $user = $userhelper->user; $course1 = $userhelper->courses->course1; $forum1 = $userhelper->forums->forum1; $forum2 = $userhelper->forums->forum2; $posts = []; $digests = []; // Add 5 discussions to forum 1. for ($i = 0; $i < 5; $i++) { list($discussion, $post) = $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]); $digests[] = $post; } // Add 5 discussions to forum 2. for ($i = 0; $i < 5; $i++) { list($discussion, $post) = $this->helper_post_to_forum($forum2, $user, ['mailnow' => 1]); $posts[] = $post; } // Set the tested user's default maildigest setting. $DB->set_field('user', 'maildigest', 1, array('id' => $user->id)); // Set the maildigest preference for forum1 to digest. forum_set_user_maildigest($forum1, -1, $user); // Set the maildigest preference for forum2 to single. forum_set_user_maildigest($forum2, 0, $user); // One digest e-mail should be sent, and five individual notifications. $expect = [ (object) [ 'userid' => $user->id, 'messages' => 5, 'digests' => 1, ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($user, $posts); $this->send_digests_and_assert($user, $digests); } /** * Sends several notifications to one user as: * * daily digests coming from the per-forum setting. */ public function test_cron_forum_digest_email(): void { global $DB, $CFG; $this->resetAfterTest(true); // Set up a basic user enrolled in a course. $userhelper = $this->helper_setup_user_in_course(); $user = $userhelper->user; $course1 = $userhelper->courses->course1; $forum1 = $userhelper->forums->forum1; $forum2 = $userhelper->forums->forum2; $fulldigests = []; $shortdigests = []; // Add 5 discussions to forum 1. for ($i = 0; $i < 5; $i++) { list($discussion, $post) = $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]); $fulldigests[] = $post; } // Add 5 discussions to forum 2. for ($i = 0; $i < 5; $i++) { list($discussion, $post) = $this->helper_post_to_forum($forum2, $user, ['mailnow' => 1]); $shortdigests[] = $post; } // Set the tested user's default maildigest setting. $DB->set_field('user', 'maildigest', 0, array('id' => $user->id)); // Set the maildigest preference for forum1 to digest (complete). forum_set_user_maildigest($forum1, 1, $user); // Set the maildigest preference for forum2 to digest (short). forum_set_user_maildigest($forum2, 2, $user); // One digest e-mail should be sent, and no individual notifications. $expect = [ (object) [ 'userid' => $user->id, 'digests' => 1, ], ]; $this->queue_tasks_and_assert($expect); $this->send_digests_and_assert($user, $fulldigests, $shortdigests); } /** * The digest being in the past is queued til the next day. */ public function test_cron_digest_previous_day(): void { global $DB, $CFG; $this->resetAfterTest(true); // Set up a basic user enrolled in a course. $userhelper = $this->helper_setup_user_in_course(); $user = $userhelper->user; $course1 = $userhelper->courses->course1; $forum1 = $userhelper->forums->forum1; $forum2 = $userhelper->forums->forum2; $fulldigests = []; $shortdigests = []; // Add 1 discussions to forum 1. list($discussion, $post) = $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]); $fulldigests[] = $post; // Set the tested user's default maildigest setting. $DB->set_field('user', 'maildigest', 1, array('id' => $user->id)); // Set the digest time to midnight. $CFG->digestmailtime = 0; // One digest e-mail should be sent, and no individual notifications. $expect = [ (object) [ 'userid' => $user->id, 'digests' => 1, ], ]; $this->queue_tasks_and_assert($expect); $tasks = $DB->get_records('task_adhoc', ['component' => 'mod_forum']); $task = reset($tasks); $this->assertGreaterThanOrEqual(time(), $task->nextruntime); } /** * The digest being in the future is queued for today. */ public function test_cron_digest_same_day(): void { global $DB, $CFG; $this->resetAfterTest(true); // Set up a basic user enrolled in a course. $userhelper = $this->helper_setup_user_in_course(); $user = $userhelper->user; $course1 = $userhelper->courses->course1; $forum1 = $userhelper->forums->forum1; $forum2 = $userhelper->forums->forum2; $fulldigests = []; $shortdigests = []; // Add 1 discussions to forum 1. list($discussion, $post) = $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]); $fulldigests[] = $post; // Set the tested user's default maildigest setting. $DB->set_field('user', 'maildigest', 1, array('id' => $user->id)); // Set the digest time to the future (magic, shouldn't work). $CFG->digestmailtime = 25; // One digest e-mail should be sent, and no individual notifications. $expect = [ (object) [ 'userid' => $user->id, 'digests' => 1, ], ]; $this->queue_tasks_and_assert($expect); $tasks = $DB->get_records('task_adhoc', ['component' => 'mod_forum']); $task = reset($tasks); $digesttime = usergetmidnight(time(), \core_date::get_server_timezone()) + ($CFG->digestmailtime * 3600); $this->assertLessThanOrEqual($digesttime, $task->nextruntime); } /** * Tests that if a new message is posted after the days digest time, * but before that days digests are sent a new task is created. */ public function test_cron_digest_queue_next_before_current_processed(): void { global $DB, $CFG; $this->resetAfterTest(true); // Set up a basic user enrolled in a course. $userhelper = $this->helper_setup_user_in_course(); $user = $userhelper->user; $forum1 = $userhelper->forums->forum1; // Add 1 discussions to forum 1. $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]); // Set the tested user's default maildigest setting. $DB->set_field('user', 'maildigest', 1, ['id' => $user->id]); // Set the digest time to the future (magic, shouldn't work). $CFG->digestmailtime = 25; // One digest e-mail should be sent, and no individual notifications. $expect = [ (object) [ 'userid' => $user->id, 'digests' => 1, ], ]; $this->queue_tasks_and_assert($expect); // Set the digest time to midnight. $CFG->digestmailtime = 0; // Add another discussions to forum 1. $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]); // One digest e-mail should be sent, and no individual notifications. $expect = [ (object) [ 'userid' => $user->id, 'digests' => 1, ], ]; $this->queue_tasks_and_assert($expect); // There should now be two tasks queued. $tasks = $DB->get_records('task_adhoc', ['component' => 'mod_forum']); $this->assertCount(2, $tasks); // Add yet another another discussions to forum 1. $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]); // One digest e-mail should be sent, and no individual notifications. $expect = [ (object) [ 'userid' => $user->id, 'digests' => 1, ], ]; $this->queue_tasks_and_assert($expect); // There should still be two tasks queued. $tasks = $DB->get_records('task_adhoc', ['component' => 'mod_forum']); $this->assertCount(2, $tasks); } /** * The sending of a digest marks posts as read if automatic message read marking is set. */ public function test_cron_digest_marks_posts_read(): void { global $DB, $CFG; $this->resetAfterTest(true); // Disable the 'Manual message read marking' option. $CFG->forum_usermarksread = false; // Set up a basic user enrolled in a course. $userhelper = $this->helper_setup_user_in_course(); $user = $userhelper->user; $course1 = $userhelper->courses->course1; $forum1 = $userhelper->forums->forum1; $posts = []; // Set the tested user's default maildigest, trackforums, read tracking settings. $DB->set_field('user', 'maildigest', 1, ['id' => $user->id]); $DB->set_field('user', 'trackforums', 1, ['id' => $user->id]); set_user_preference('forum_markasreadonnotification', 1, $user->id); // Set the maildigest preference for forum1 to default. forum_set_user_maildigest($forum1, -1, $user); // Add 5 discussions to forum 1. for ($i = 0; $i < 5; $i++) { list($discussion, $post) = $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]); $posts[] = $post; } // There should be unread posts for the forum. $expectedposts = [ $forum1->id => (object) [ 'id' => $forum1->id, 'unread' => count($posts), ], ]; $this->assertEquals($expectedposts, forum_tp_get_course_unread_posts($user->id, $course1->id)); // One digest mail should be sent and no other messages. $expect = [ (object) [ 'userid' => $user->id, 'messages' => 0, 'digests' => 1, ], ]; $this->queue_tasks_and_assert($expect); $this->send_digests_and_assert($user, $posts); // Verify that there are no unread posts for any forums. $this->assertEmpty(forum_tp_get_course_unread_posts($user->id, $course1->id)); } /** * The sending of a digest does not mark posts as read when manual message read marking is set. */ public function test_cron_digest_leaves_posts_unread(): void { global $DB, $CFG; $this->resetAfterTest(true); // Enable the 'Manual message read marking' option. $CFG->forum_usermarksread = true; // Set up a basic user enrolled in a course. $userhelper = $this->helper_setup_user_in_course(); $user = $userhelper->user; $course1 = $userhelper->courses->course1; $forum1 = $userhelper->forums->forum1; $posts = []; // Set the tested user's default maildigest, trackforums, read tracking settings. $DB->set_field('user', 'maildigest', 1, ['id' => $user->id]); $DB->set_field('user', 'trackforums', 1, ['id' => $user->id]); set_user_preference('forum_markasreadonnotification', 1, $user->id); // Set the maildigest preference for forum1 to default. forum_set_user_maildigest($forum1, -1, $user); // Add 5 discussions to forum 1. for ($i = 0; $i < 5; $i++) { list($discussion, $post) = $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]); $posts[] = $post; } // There should be unread posts for the forum. $expectedposts = [ $forum1->id => (object) [ 'id' => $forum1->id, 'unread' => count($posts), ], ]; $this->assertEquals($expectedposts, forum_tp_get_course_unread_posts($user->id, $course1->id)); // One digest mail should be sent and no other messages. $expect = [ (object) [ 'userid' => $user->id, 'messages' => 0, 'digests' => 1, ], ]; $this->queue_tasks_and_assert($expect); $this->send_digests_and_assert($user, $posts); // Verify that there are still the same unread posts for the forum. $this->assertEquals($expectedposts, forum_tp_get_course_unread_posts($user->id, $course1->id)); } } PK-B\G-/ / tests/entities_sorter_test.phpnu[. namespace mod_forum; use mod_forum\local\entities\sorter as sorter_entity; /** * The discussion entity tests. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class entities_sorter_test extends \advanced_testcase { /** * Test the entity returns expected values. */ public function test_entity_sort_into_children(): void { $this->resetAfterTest(); $sorter = new sorter_entity( function($entity) { return $entity['id']; }, function($entity) { return $entity['parent']; } ); $a = ['id' => 1, 'parent' => 0]; $b = ['id' => 2, 'parent' => 1]; $c = ['id' => 3, 'parent' => 1]; $d = ['id' => 4, 'parent' => 2]; $e = ['id' => 5, 'parent' => 0]; $expected = [ [$e, []], [$a, [[$b, [[$d, []]]], [$c, []]]], ]; $actual = $sorter->sort_into_children([$d, $b, $e, $a, $c]); $this->assertEquals($expected, $actual); } /** * Test the entity returns expected values. */ public function test_entity_flatten_children(): void { $this->resetAfterTest(); $sorter = new sorter_entity( function($entity) { return $entity['id']; }, function($entity) { return $entity['parent']; } ); $a = ['id' => 1, 'parent' => 0]; $b = ['id' => 2, 'parent' => 1]; $c = ['id' => 3, 'parent' => 1]; $d = ['id' => 4, 'parent' => 3]; $sorted = [ [$a, [[$b, [[$d, []]]], [$c, []]]] ]; $expected = [$a, $b, $d, $c]; $actual = $sorter->flatten_children($sorted); $this->assertEquals($expected, $actual); } } PK-B\ֈtests/entities_author_test.phpnu[. namespace mod_forum; use mod_forum\local\entities\author as author_entity; /** * The author entity tests. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class entities_author_test extends \advanced_testcase { /** * Test the entity returns expected values. */ public function test_entity(): void { $this->resetAfterTest(); $author = new author_entity( 1, 2, 'test', 'person', 'test person', 'test@example.com', false, 'middle', 'tteeeeest', 'ppppeeerssson', 'maverick', 'image alt' ); $this->assertEquals(1, $author->get_id()); $this->assertEquals(2, $author->get_picture_item_id()); $this->assertEquals('test', $author->get_first_name()); $this->assertEquals('person', $author->get_last_name()); $this->assertEquals('test person', $author->get_full_name()); $this->assertEquals('test@example.com', $author->get_email()); $this->assertEquals(false, $author->is_deleted()); $this->assertEquals('middle', $author->get_middle_name()); $this->assertEquals('tteeeeest', $author->get_first_name_phonetic()); $this->assertEquals('ppppeeerssson', $author->get_last_name_phonetic()); $this->assertEquals('maverick', $author->get_alternate_name()); $this->assertEquals('image alt', $author->get_image_alt()); } } PK-B\tz""tests/mail_group_test.phpnu[. namespace mod_forum; use mod_forum_tests_cron_trait; use mod_forum_tests_generator_trait; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/forum/lib.php'); require_once(__DIR__ . '/cron_trait.php'); require_once(__DIR__ . '/generator_trait.php'); /** * The forum module mail generation tests for groups. * * @copyright 2013 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class mail_group_test extends \advanced_testcase { // Make use of the cron tester trait. use mod_forum_tests_cron_trait; // Make use of the test generator trait. use mod_forum_tests_generator_trait; /** * @var \phpunit_message_sink */ protected $messagesink; /** * @var \phpunit_mailer_sink */ protected $mailsink; public function setUp(): void { global $CFG; parent::setUp(); // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); \mod_forum\subscriptions::reset_discussion_cache(); // Messaging is not compatible with transactions... $this->preventResetByRollback(); // Catch all messages. $this->messagesink = $this->redirectMessages(); $this->mailsink = $this->redirectEmails(); // Forcibly reduce the maxeditingtime to a second in the past to // ensure that messages are sent out. $CFG->maxeditingtime = -1; } public function tearDown(): void { // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); $this->messagesink->clear(); $this->messagesink->close(); unset($this->messagesink); $this->mailsink->clear(); $this->mailsink->close(); unset($this->mailsink); parent::tearDown(); } /** * Ensure that posts written in a forum marked for separate groups includes notifications for the members of that * group, and any user with accessallgroups. */ public function test_separate_group(): void { global $CFG, $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE, 'groupmode' => SEPARATEGROUPS, ]); // Create three students: // - author, enrolled in group A; and // - recipient, enrolled in group B; and // - other, enrolled in the course, but no groups. list($author, $recipient, $otheruser) = $this->helper_create_users($course, 3); // Create one teacher, not in any group and no accessallgroups capability. list($teacher) = $this->helper_create_users($course, 1, 'teacher'); // Create one editing teacher, not in any group but with accessallgroups capability. list($editingteacher) = $this->helper_create_users($course, 1, 'editingteacher'); $groupa = $this->getDataGenerator()->create_group(['courseid' => $course->id]); $groupb = $this->getDataGenerator()->create_group(['courseid' => $course->id]); $this->getDataGenerator()->create_group_member([ 'groupid' => $groupa->id, 'userid' => $author->id, ]); $this->getDataGenerator()->create_group_member([ 'groupid' => $groupb->id, 'userid' => $recipient->id, ]); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author, [ 'groupid' => $groupa->id, ]); // Only the author should receive. $expect = [ 'author' => (object) [ 'userid' => $author->id, 'messages' => 1, ], 'recipient' => (object) [ 'userid' => $recipient->id, 'messages' => 0, ], 'otheruser' => (object) [ 'userid' => $otheruser->id, 'messages' => 0, ], 'teacher' => (object) [ 'userid' => $teacher->id, 'messages' => 0, ], 'editingteacher' => (object) [ 'userid' => $editingteacher->id, 'messages' => 1, ], ]; $this->queue_tasks_and_assert($expect); // No notifications should be queued. $this->send_notifications_and_assert($author, [$post]); $this->send_notifications_and_assert($recipient, []); $this->send_notifications_and_assert($otheruser, []); $this->send_notifications_and_assert($teacher, []); $this->send_notifications_and_assert($editingteacher, [$post]); } /** * Ensure that posts written in a forum marked for visible groups includes notifications for the members of that * group, and any user with accessallgroups. */ public function test_visible_group(): void { global $CFG, $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE, 'groupmode' => VISIBLEGROUPS, ]); // Create three students: // - author, enrolled in group A; and // - recipient, enrolled in group B; and // - other, enrolled in the course, but no groups. list($author, $recipient, $otheruser) = $this->helper_create_users($course, 3); // Create one teacher, not in any group and no accessallgroups capability. list($teacher) = $this->helper_create_users($course, 1, 'teacher'); // Create one editing teacher, not in any group but with accessallgroups capability. list($editingteacher) = $this->helper_create_users($course, 1, 'editingteacher'); $groupa = $this->getDataGenerator()->create_group(['courseid' => $course->id]); $groupb = $this->getDataGenerator()->create_group(['courseid' => $course->id]); $this->getDataGenerator()->create_group_member([ 'groupid' => $groupa->id, 'userid' => $author->id, ]); $this->getDataGenerator()->create_group_member([ 'groupid' => $groupb->id, 'userid' => $recipient->id, ]); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author, [ 'groupid' => $groupa->id, ]); // Only the author should receive. $expect = [ 'author' => (object) [ 'userid' => $author->id, 'messages' => 1, ], 'recipient' => (object) [ 'userid' => $recipient->id, 'messages' => 0, ], 'otheruser' => (object) [ 'userid' => $otheruser->id, 'messages' => 0, ], 'teacher' => (object) [ 'userid' => $teacher->id, 'messages' => 0, ], 'editingteacher' => (object) [ 'userid' => $editingteacher->id, 'messages' => 1, ], ]; $this->queue_tasks_and_assert($expect); // No notifications should be queued. $this->send_notifications_and_assert($author, [$post]); $this->send_notifications_and_assert($recipient, []); $this->send_notifications_and_assert($otheruser, []); $this->send_notifications_and_assert($teacher, []); $this->send_notifications_and_assert($editingteacher, [$post]); } } PK-B\-,tests/message/inbound/reply_handler_test.phpnu[. namespace mod_forum\message\inbound; use advanced_testcase; use core\context\module; use core\message\inbound\{manager, processing_failed_exception}; use mod_forum_generator; use stdClass; use Throwable; /** * Unit tests for the reply handler * * @package mod_forum * @covers \mod_forum\message\inbound\reply_handler * @copyright 2024 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class reply_handler_test extends advanced_testcase { /** * Test attachment processing */ public function test_process_message_attachments(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(['maxbytes' => 1024]); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id, 'maxbytes' => 0]); $user = $this->getDataGenerator()->create_and_enrol($course); $this->setUser($user); [$discussion, $post] = $this->create_discussion_with_post($forum, $user); $output = $this->reply_handler_process_message($post, 'My reply', 'Hello', (object) [ 'filename' => 'Foo.txt', 'content' => 'Foo', 'filesize' => 3, ]); $this->assertMatchesRegularExpression('/Processing Foo.txt as an attachment./', $output); $this->assertMatchesRegularExpression('/Attaching Foo.txt to/', $output); $this->assertMatchesRegularExpression('/Created a post \d+ in \d+./', $output); // Assert our reply was added with attachment. $newpost = $DB->get_record('forum_posts', [ 'discussion' => $discussion->id, 'subject' => 'My reply', 'attachment' => 1, ], '*', MUST_EXIST); $attachments = get_file_storage()->get_area_files( module::instance($forum->cmid)->id, 'mod_forum', 'attachment', $newpost->id, 'id', false, ); $this->assertCount(1, $attachments); $this->assertEquals('Foo.txt', reset($attachments)->get_filename()); } /** * Test attachment processing where maxbytes is exceeded */ public function test_process_message_attachments_over_maxbytes(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id, 'maxbytes' => 2]); $user = $this->getDataGenerator()->create_and_enrol($course); $this->setUser($user); [$discussion, $post] = $this->create_discussion_with_post($forum, $user); $this->expectException(processing_failed_exception::class); $this->expectExceptionMessage('Unable to post your reply, since the total attachment size (3 bytes) is greater than ' . 'the maximum size allowed for the forum (2 bytes)'); $this->reply_handler_process_message($post, 'My reply', 'Hello', (object) [ 'filename' => 'Foo.txt', 'content' => 'Foo', 'filesize' => 3, ]); } /** * Helper to generate discussion/post data * * @param stdClass $forum * @param stdClass $user * @return stdClass[] */ private function create_discussion_with_post(stdClass $forum, stdClass $user): array { /** @var mod_forum_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum'); $discussion = $generator->create_discussion(['course' => $forum->course, 'forum' => $forum->id, 'userid' => $user->id]); $post = $generator->create_post(['discussion' => $discussion->id, 'userid' => $user->id]); return [$discussion, $post]; } /** * Helper to process message details using the reply handler * * @param stdClass $post * @param string $subject * @param string $message * @param stdClass $attachment * @return string Output buffer from the handler */ private function reply_handler_process_message( stdClass $post, string $subject, string $message, stdClass $attachment, ): string { // Start capturing output. ob_start(); /** @var reply_handler $handler */ $handler = manager::get_handler(reply_handler::class); try { $handler->process_message( (object) [ 'datavalue' => $post->id, ], (object) [ 'envelope' => (object) [ 'subject' => $subject, ], 'plain' => $message, 'attachments' => [ 'attachment' => [ $attachment, ], ], 'timestamp' => time(), ], ); } catch (Throwable $ex) { ob_end_clean(); throw $ex; } // Return captured output buffer. return (string) ob_get_clean(); } } PK-B\$GGtests/h5p/canedit_test.phpnu[. declare(strict_types = 1); namespace mod_forum\h5p; use stdClass; /** * Test class covering the H5P canedit class. * * @package mod_forum * @copyright 2021 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \mod_forum\h5p\canedit */ final class canedit_test extends \advanced_testcase { /** * Test the behaviour of can_edit_content(). * * @covers ::can_edit_content * @dataProvider can_edit_content_provider * * @param string $currentuser User who will call the method. * @param string $fileauthor Author of the file to check. * @param string $filecomponent Component of the file to check. * @param bool $expected Expected result after calling the can_edit_content method. * @param string $filearea Area of the file to check. * * @return void */ public function test_can_edit_content(string $currentuser, string $fileauthor, string $filecomponent, bool $expected, $filearea = 'unittest'): void { global $USER, $DB; $this->setRunTestInSeparateProcess(true); $this->resetAfterTest(); // Create course. $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); // Create some users. $this->setAdminUser(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $users = [ 'admin' => $USER, 'teacher' => $teacher, 'student' => $student, ]; // Set current user. if ($currentuser !== 'admin') { $this->setUser($users[$currentuser]); } $itemid = rand(); if ($filearea === 'post') { // Create a forum and add a discussion. $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $record = new stdClass(); $record->course = $course->id; $record->userid = $users[$fileauthor]->id; $record->forum = $forum->id; $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); $post = $DB->get_record('forum_posts', ['discussion' => $discussion->id]); $itemid = $post->id; } // Create the file. $filename = 'greeting-card.h5p'; $path = __DIR__ . '/../../../../h5p/tests/fixtures/' . $filename; if ($filecomponent === 'contentbank') { $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); $contents = $generator->generate_contentbank_data( 'contenttype_h5p', 1, (int)$users[$fileauthor]->id, $context, true, $path ); $content = array_shift($contents); $file = $content->get_file(); } else { $filerecord = [ 'contextid' => $context->id, 'component' => $filecomponent, 'filearea' => $filearea, 'itemid' => $itemid, 'filepath' => '/', 'filename' => basename($path), 'userid' => $users[$fileauthor]->id, ]; $fs = get_file_storage(); $file = $fs->create_file_from_pathname($filerecord, $path); } // Check if the currentuser can edit the file. $result = canedit::can_edit_content($file); $this->assertEquals($expected, $result); } /** * Data provider for test_can_edit_content(). * * @return array */ public static function can_edit_content_provider(): array { return [ // Component = mod_forum. 'mod_forum: Admin user is author' => [ 'currentuser' => 'admin', 'fileauthor' => 'admin', 'filecomponent' => 'mod_forum', 'expected' => true, ], 'mod_forum: Admin user, teacher is author' => [ 'currentuser' => 'admin', 'fileauthor' => 'teacher', 'filecomponent' => 'mod_forum', 'expected' => true, ], 'mod_forum: Teacher user, admin is author' => [ 'currentuser' => 'teacher', 'fileauthor' => 'admin', 'filecomponent' => 'mod_forum', 'expected' => true, ], 'mod_forum: Student user, teacher is author' => [ 'currentuser' => 'student', 'fileauthor' => 'teacher', 'filecomponent' => 'mod_forum', 'expected' => false, ], 'mod_forum/post: Admin user is author' => [ 'currentuser' => 'admin', 'fileauthor' => 'admin', 'filecomponent' => 'mod_forum', 'expected' => true, 'filearea' => 'post', ], 'mod_forum/post: Teacher user, admin is author' => [ 'currentuser' => 'teacher', 'fileauthor' => 'admin', 'filecomponent' => 'mod_forum', 'expected' => true, 'filearea' => 'post', ], 'mod_forum/post: Student user, teacher is author' => [ 'currentuser' => 'student', 'fileauthor' => 'teacher', 'filecomponent' => 'mod_forum', 'expected' => false, 'filearea' => 'post', ], // Component <> mod_forum. 'mod_page: Admin user is author' => [ 'currentuser' => 'admin', 'fileauthor' => 'admin', 'filecomponent' => 'mod_page', 'expected' => false, ], // Unexisting components. 'Unexisting component' => [ 'currentuser' => 'admin', 'fileauthor' => 'admin', 'filecomponent' => 'unexisting_component', 'expected' => false, ], 'Unexisting module activity' => [ 'currentuser' => 'admin', 'fileauthor' => 'admin', 'filecomponent' => 'mod_unexisting', 'expected' => false, ], 'Unexisting block' => [ 'currentuser' => 'admin', 'fileauthor' => 'admin', 'filecomponent' => 'block_unexisting', 'expected' => false, ], ]; } } PK-B\Atq q tests/locallib_test.phpnu[. /** * File containing the forum module local library function tests. * * @package mod_forum * @category test * @copyright 2018 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/forum/lib.php'); /** * Class mod_forum_locallib_testcase. * * @copyright 2018 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class locallib_test extends \advanced_testcase { public function test_forum_update_calendar(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Create a course. $course = $this->getDataGenerator()->create_course(); // Create a forum activity. $time = time(); $forum = $this->getDataGenerator()->create_module('forum', array( 'course' => $course->id, 'duedate' => $time ) ); // Check that there is now an event in the database. $events = $DB->get_records('event'); $this->assertCount(1, $events); // Get the event. $event = reset($events); // Confirm the event is correct. $this->assertEquals('forum', $event->modulename); $this->assertEquals($forum->id, $event->instance); $this->assertEquals(CALENDAR_EVENT_TYPE_ACTION, $event->type); $this->assertEquals(FORUM_EVENT_TYPE_DUE, $event->eventtype); $this->assertEquals($time, $event->timestart); $this->assertEquals($time, $event->timesort); } } PK-B\U,(,( tests/custom_completion_test.phpnu[. /** * Contains unit tests for core_completion/activity_custom_completion. * * @package mod_forum * @copyright Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types=1); namespace mod_forum; use advanced_testcase; use cm_info; use coding_exception; use mod_forum\completion\custom_completion; use moodle_exception; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->libdir . '/completionlib.php'); require_once($CFG->dirroot . '/mod/forum/tests/generator/lib.php'); require_once($CFG->dirroot . '/mod/forum/tests/generator_trait.php'); /** * Class for unit testing mod_forum/activity_custom_completion. * * @package mod_forum * @copyright Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class custom_completion_test extends advanced_testcase { use \mod_forum_tests_generator_trait; /** * Data provider for get_state(). * * @return array[] */ public static function get_state_provider(): array { return [ 'Undefined rule' => [ 'somenonexistentrule', 0, COMPLETION_TRACKING_NONE, 0, 0, 0, null, coding_exception::class ], 'Completion discussions rule not available' => [ 'completiondiscussions', 0, COMPLETION_TRACKING_NONE, 0, 0, 0, null, moodle_exception::class ], 'Completion discussions rule available, user has not created discussion' => [ 'completiondiscussions', 0, COMPLETION_TRACKING_AUTOMATIC, 5, 0, 0, COMPLETION_INCOMPLETE, null ], 'Rule available, user has created discussions' => [ 'completiondiscussions', 5, COMPLETION_TRACKING_AUTOMATIC, 5, 0, 0, COMPLETION_COMPLETE, null ], 'Completion replies rule not available' => [ 'completionreplies', 0, COMPLETION_TRACKING_NONE, 0, 0, 0, null, moodle_exception::class ], 'Rule available, user has not replied' => [ 'completionreplies', 0, COMPLETION_TRACKING_AUTOMATIC, 0, 5, 0, COMPLETION_INCOMPLETE, null ], 'Rule available, user has created replied' => [ 'completionreplies', 5, COMPLETION_TRACKING_AUTOMATIC, 0, 5, 0, COMPLETION_COMPLETE, null ], 'Completion posts rule not available' => [ 'completionposts', 0, COMPLETION_TRACKING_NONE, 0, 0, 0, null, moodle_exception::class ], 'Rule available, user has not posted' => [ 'completionposts', 0, COMPLETION_TRACKING_AUTOMATIC, 0, 0, 5, COMPLETION_INCOMPLETE, null ], 'Rule available, user has posted' => [ 'completionposts', 5, COMPLETION_TRACKING_AUTOMATIC, 0, 0, 5, COMPLETION_COMPLETE, null ], ]; } /** * Test for get_state(). * * @dataProvider get_state_provider * @param string $rule The custom completion rule. * @param int $rulecount Quantity of discussions, replies or posts to be created. * @param int $available Whether this rule is available. * @param int|null $discussions The number of discussions. * @param int|null $replies The number of replies. * @param int|null $posts The number of posts. * @param int|null $status Expected status. * @param string|null $exception Expected exception. */ public function test_get_state(string $rule, int $rulecount, int $available, ?int $discussions, ?int $replies, ?int $posts, ?int $status, ?string $exception): void { if (!is_null($exception)) { $this->expectException($exception); } $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(['enablecompletion' => COMPLETION_ENABLED]); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $forumgenerator = $this->getDataGenerator()->get_plugin_generator('mod_forum'); $params = [ 'course' => $course->id, 'completion' => $available, 'completiondiscussions' => $discussions, 'completionreplies' => $replies, 'completionposts' => $posts ]; $forum = $this->getDataGenerator()->create_module('forum', $params); $cm = get_coursemodule_from_instance('forum', $forum->id); if ($rulecount > 0) { if ($rule == 'completiondiscussions') { // Create x number of discussions. for ($i = 0; $i < $rulecount; $i++) { $forumgenerator->create_discussion((object) [ 'course' => $forum->course, 'userid' => $student->id, 'forum' => $forum->id, ]); } } else if ($rule == 'completionreplies') { [$discussion1, $post1] = $this->helper_post_to_forum($forum, $student); for ($i = 0; $i < $rulecount; $i++) { $this->helper_reply_to_post($post1, $student); } } else if ($rule == 'completionposts') { for ($i = 0; $i < $rulecount; $i++) { $this->helper_post_to_forum($forum, $student); } } } // Make sure we're using a cm_info object. $cm = cm_info::create($cm); $customcompletion = new custom_completion($cm, (int)$student->id); $this->assertEquals($status, $customcompletion->get_state($rule)); } /** * Test for get_defined_custom_rules(). */ public function test_get_defined_custom_rules(): void { $rules = custom_completion::get_defined_custom_rules(); $this->assertCount(3, $rules); $this->assertEquals('completiondiscussions', reset($rules)); } /** * Test for get_defined_custom_rule_descriptions(). */ public function test_get_custom_rule_descriptions(): void { // Get defined custom rules. $rules = custom_completion::get_defined_custom_rules(); // Build a mock cm_info instance. $mockcminfo = $this->getMockBuilder(cm_info::class) ->disableOriginalConstructor() ->onlyMethods(['__get']) ->getMock(); // Instantiate a custom_completion object using the mocked cm_info. $customcompletion = new custom_completion($mockcminfo, 1); // Get custom rule descriptions. $ruledescriptions = $customcompletion->get_custom_rule_descriptions(); // Confirm that defined rules and rule descriptions are consistent with each other. $this->assertEquals(count($rules), count($ruledescriptions)); foreach ($rules as $rule) { $this->assertArrayHasKey($rule, $ruledescriptions); } } /** * Test for is_defined(). */ public function test_is_defined(): void { // Build a mock cm_info instance. $mockcminfo = $this->getMockBuilder(cm_info::class) ->disableOriginalConstructor() ->getMock(); $customcompletion = new custom_completion($mockcminfo, 1); // Rule is defined. $this->assertTrue($customcompletion->is_defined('completiondiscussions')); // Undefined rule. $this->assertFalse($customcompletion->is_defined('somerandomrule')); } /** * Data provider for test_get_available_custom_rules(). * * @return array[] */ public static function get_available_custom_rules_provider(): array { return [ 'Completion discussions available' => [ COMPLETION_ENABLED, ['completiondiscussions'] ], 'Completion discussions not available' => [ COMPLETION_DISABLED, [] ], 'Completion replies available' => [ COMPLETION_ENABLED, ['completionreplies'] ], 'Completion replies not available' => [ COMPLETION_DISABLED, [] ], 'Completion posts available' => [ COMPLETION_ENABLED, ['completionposts'] ], 'Completion posts not available' => [ COMPLETION_DISABLED, [] ], ]; } /** * Test for get_available_custom_rules(). * * @dataProvider get_available_custom_rules_provider * @param int $status * @param array $expected */ public function test_get_available_custom_rules(int $status, array $expected): void { $customdataval = [ 'customcompletionrules' => [] ]; if ($status == COMPLETION_ENABLED) { $rule = $expected[0]; $customdataval = [ 'customcompletionrules' => [$rule => $status] ]; } // Build a mock cm_info instance. $mockcminfo = $this->getMockBuilder(cm_info::class) ->disableOriginalConstructor() ->onlyMethods(['__get']) ->getMock(); // Mock the return of magic getter for the customdata attribute. $mockcminfo->expects($this->any()) ->method('__get') ->with('customdata') ->willReturn($customdataval); $customcompletion = new custom_completion($mockcminfo, 1); $this->assertEquals($expected, $customcompletion->get_available_custom_rules()); } } PK-B\Iww)tests/backup_forum_activity_task_test.phpnu[. namespace mod_forum; use backup_forum_activity_task; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_stepslib.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_activity_task.class.php'); require_once($CFG->dirroot . '/mod/forum/backup/moodle2/backup_forum_activity_task.class.php'); /** * Tests for mod_forum_backup_forum_activity_task. * * @package mod_forum * @category test * @copyright 2016 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class backup_forum_activity_task_test extends \advanced_testcase { /** * Test the encoding of forum content links. * * @param string $content The incoming content * @param string $expectation The expected result * * @dataProvider encode_content_links_provider */ public function test_encode_content_links($content, $expectation): void { $this->assertEquals($expectation, backup_forum_activity_task::encode_content_links($content)); } public static function encode_content_links_provider(): array { global $CFG; $altwwwroot = 'http://invalid.example.com/'; return [ 'Link to the list of forums for current wwwroot' => [ sprintf('%s/mod/forum/index.php?id=42', $CFG->wwwroot), '$@FORUMINDEX*42@$', ], 'Link to forum view by moduleid for current wwwroot' => [ sprintf('%s/mod/forum/view.php?id=29', $CFG->wwwroot), '$@FORUMVIEWBYID*29@$', ], 'Link to forum view by forumid for current wwwroot' => [ sprintf('%s/mod/forum/view.php?f=31', $CFG->wwwroot), '$@FORUMVIEWBYF*31@$', ], 'Link to forum discussion with parent syntax for current wwwroot' => [ sprintf('%s/mod/forum/discuss.php?d=26&parent=99', $CFG->wwwroot), '$@FORUMDISCUSSIONVIEWPARENT*26*99@$', ], 'Link to forum discussion with parent syntax for current wwwroot encoded' => [ sprintf('%s/mod/forum/discuss.php?d=26&parent=99', $CFG->wwwroot), '$@FORUMDISCUSSIONVIEWPARENT*26*99@$', ], 'Link to forum discussion with relative syntax for current wwwroot' => [ sprintf('%s/mod/forum/discuss.php?d=1040#9930', $CFG->wwwroot), '$@FORUMDISCUSSIONVIEWINSIDE*1040*9930@$', ], 'Link to forum discussion by discussionid for current wwwroot' => [ sprintf('%s/mod/forum/discuss.php?d=9304', $CFG->wwwroot), '$@FORUMDISCUSSIONVIEW*9304@$', ], 'Link to the list of forums for other wwwroot' => [ sprintf('%s/mod/forum/index.php?id=42', $altwwwroot), sprintf('%s/mod/forum/index.php?id=42', $altwwwroot), ], 'Link to forum view by moduleid for other wwwroot' => [ sprintf('%s/mod/forum/view.php?id=29', $altwwwroot), sprintf('%s/mod/forum/view.php?id=29', $altwwwroot), ], 'Link to forum view by forumid for other wwwroot' => [ sprintf('%s/mod/forum/view.php?f=31', $altwwwroot), sprintf('%s/mod/forum/view.php?f=31', $altwwwroot), ], 'Link to forum discussion with parent syntax for other wwwroot' => [ sprintf('%s/mod/forum/discuss.php?d=26&parent=99', $altwwwroot), sprintf('%s/mod/forum/discuss.php?d=26&parent=99', $altwwwroot), ], 'Link to forum discussion with parent syntax for other wwwroot encoded' => [ sprintf('%s/mod/forum/discuss.php?d=26&parent=99', $altwwwroot), sprintf('%s/mod/forum/discuss.php?d=26&parent=99', $altwwwroot), ], 'Link to forum discussion with relative syntax for other wwwroot' => [ sprintf('%s/mod/forum/discuss.php?d=1040#9930', $altwwwroot), sprintf('%s/mod/forum/discuss.php?d=1040#9930', $altwwwroot), ], 'Link to forum discussion by discussionid for other wwwroot' => [ sprintf('%s/mod/forum/discuss.php?d=9304', $altwwwroot), sprintf('%s/mod/forum/discuss.php?d=9304', $altwwwroot), ], ]; } } PK-B\QWWtests/search/search_test.phpnu[. /** * Forum search unit tests. * * @package mod_forum * @category test * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\search; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php'); require_once($CFG->dirroot . '/mod/forum/tests/generator/lib.php'); require_once($CFG->dirroot . '/mod/forum/lib.php'); /** * Provides the unit tests for forum search. * * @package mod_forum * @category test * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class search_test extends \advanced_testcase { /** * @var string Area id */ protected $forumpostareaid = null; public function setUp(): void { parent::setUp(); $this->resetAfterTest(true); set_config('enableglobalsearch', true); $this->forumpostareaid = \core_search\manager::generate_areaid('mod_forum', 'post'); // Set \core_search::instance to the mock_search_engine as we don't require the search engine to be working to test this. $search = \testable_core_search::instance(); } /** * Availability. * * @return void */ public function test_search_enabled(): void { $searcharea = \core_search\manager::get_search_area($this->forumpostareaid); list($componentname, $varname) = $searcharea->get_config_var_name(); // Enabled by default once global search is enabled. $this->assertTrue($searcharea->is_enabled()); set_config($varname . '_enabled', 0, $componentname); $this->assertFalse($searcharea->is_enabled()); set_config($varname . '_enabled', 1, $componentname); $this->assertTrue($searcharea->is_enabled()); } /** * Indexing mod forum contents. * * @return void */ public function test_posts_indexing(): void { global $DB; // Returns the instance as long as the area is supported. $searcharea = \core_search\manager::get_search_area($this->forumpostareaid); $this->assertInstanceOf('\mod_forum\search\post', $searcharea); $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); $course1 = self::getDataGenerator()->create_course(); $course2 = self::getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'student'); $this->getDataGenerator()->enrol_user($user2->id, $course1->id, 'student'); $record = new \stdClass(); $record->course = $course1->id; // Available for both student and teacher. $forum1 = self::getDataGenerator()->create_module('forum', $record); // Create discussion1. $record = new \stdClass(); $record->course = $course1->id; $record->userid = $user1->id; $record->forum = $forum1->id; $record->message = 'discussion'; $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // Create post1 in discussion1. $record = new \stdClass(); $record->discussion = $discussion1->id; $record->parent = $discussion1->firstpost; $record->userid = $user2->id; $record->message = 'post2'; $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); // All records. $recordset = $searcharea->get_recordset_by_timestamp(0); $this->assertTrue($recordset->valid()); $nrecords = 0; foreach ($recordset as $record) { $this->assertInstanceOf('stdClass', $record); $doc = $searcharea->get_document($record); $this->assertInstanceOf('\core_search\document', $doc); // Static caches are working. $dbreads = $DB->perf_get_reads(); $doc = $searcharea->get_document($record); $this->assertEquals($dbreads, $DB->perf_get_reads()); $this->assertInstanceOf('\core_search\document', $doc); $nrecords++; } // If there would be an error/failure in the foreach above the recordset would be closed on shutdown. $recordset->close(); $this->assertEquals(2, $nrecords); // The +2 is to prevent race conditions. $recordset = $searcharea->get_recordset_by_timestamp(time() + 2); // No new records. $this->assertFalse($recordset->valid()); $recordset->close(); // Context test: create another forum with 1 post. $forum2 = self::getDataGenerator()->create_module('forum', ['course' => $course1->id]); $record = new \stdClass(); $record->course = $course1->id; $record->userid = $user1->id; $record->forum = $forum2->id; $record->message = 'discussion'; self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // Test indexing with each forum then combined course context. $rs = $searcharea->get_document_recordset(0, \context_module::instance($forum1->cmid)); $this->assertEquals(2, iterator_count($rs)); $rs->close(); $rs = $searcharea->get_document_recordset(0, \context_module::instance($forum2->cmid)); $this->assertEquals(1, iterator_count($rs)); $rs->close(); $rs = $searcharea->get_document_recordset(0, \context_course::instance($course1->id)); $this->assertEquals(3, iterator_count($rs)); $rs->close(); } /** * Document contents. * * @return void */ public function test_posts_document(): void { global $DB; // Returns the instance as long as the area is supported. $searcharea = \core_search\manager::get_search_area($this->forumpostareaid); $this->assertInstanceOf('\mod_forum\search\post', $searcharea); $user = self::getDataGenerator()->create_user(); $course1 = self::getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($user->id, $course1->id, 'teacher'); $record = new \stdClass(); $record->course = $course1->id; $forum1 = self::getDataGenerator()->create_module('forum', $record); // Teacher only. $forum2 = self::getDataGenerator()->create_module('forum', $record); set_coursemodule_visible($forum2->cmid, 0); // Create discussion1. $record = new \stdClass(); $record->course = $course1->id; $record->userid = $user->id; $record->forum = $forum1->id; $record->message = 'discussion'; $record->groupid = 0; $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // Create post1 in discussion1. $record = new \stdClass(); $record->discussion = $discussion1->id; $record->parent = $discussion1->firstpost; $record->userid = $user->id; $record->subject = 'subject1'; $record->message = 'post1'; $record->groupid = -1; $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $post1 = $DB->get_record('forum_posts', array('id' => $discussion1reply1->id)); $post1->forumid = $forum1->id; $post1->courseid = $forum1->course; $post1->groupid = -1; $doc = $searcharea->get_document($post1); $this->assertInstanceOf('\core_search\document', $doc); $this->assertEquals($discussion1reply1->id, $doc->get('itemid')); $this->assertEquals($this->forumpostareaid . '-' . $discussion1reply1->id, $doc->get('id')); $this->assertEquals($course1->id, $doc->get('courseid')); $this->assertEquals($user->id, $doc->get('userid')); $this->assertEquals($discussion1reply1->subject, $doc->get('title')); $this->assertEquals($discussion1reply1->message, $doc->get('content')); } /** * Group support for forum posts. */ public function test_posts_group_support(): void { // Get the search area and test generators. $searcharea = \core_search\manager::get_search_area($this->forumpostareaid); $generator = $this->getDataGenerator(); $forumgenerator = $generator->get_plugin_generator('mod_forum'); // Create a course, a user, and two groups. $course = $generator->create_course(); $user = $generator->create_user(); $generator->enrol_user($user->id, $course->id, 'teacher'); $group1 = $generator->create_group(['courseid' => $course->id]); $group2 = $generator->create_group(['courseid' => $course->id]); // Separate groups forum. $forum = self::getDataGenerator()->create_module('forum', ['course' => $course->id, 'groupmode' => SEPARATEGROUPS]); // Create discussion with each group and one for all groups. One has a post in. $discussion1 = $forumgenerator->create_discussion(['course' => $course->id, 'userid' => $user->id, 'forum' => $forum->id, 'message' => 'd1', 'groupid' => $group1->id]); $forumgenerator->create_discussion(['course' => $course->id, 'userid' => $user->id, 'forum' => $forum->id, 'message' => 'd2', 'groupid' => $group2->id]); $forumgenerator->create_discussion(['course' => $course->id, 'userid' => $user->id, 'forum' => $forum->id, 'message' => 'd3']); // Create a reply in discussion1. $forumgenerator->create_post(['discussion' => $discussion1->id, 'parent' => $discussion1->firstpost, 'userid' => $user->id, 'message' => 'p1']); // Do the indexing of all 4 posts. $rs = $searcharea->get_recordset_by_timestamp(0); $results = []; foreach ($rs as $rec) { $results[$rec->message] = $rec; } $rs->close(); $this->assertCount(4, $results); // Check each document has the correct groupid. $doc = $searcharea->get_document($results['d1']); $this->assertTrue($doc->is_set('groupid')); $this->assertEquals($group1->id, $doc->get('groupid')); $doc = $searcharea->get_document($results['d2']); $this->assertTrue($doc->is_set('groupid')); $this->assertEquals($group2->id, $doc->get('groupid')); $doc = $searcharea->get_document($results['d3']); $this->assertFalse($doc->is_set('groupid')); $doc = $searcharea->get_document($results['p1']); $this->assertTrue($doc->is_set('groupid')); $this->assertEquals($group1->id, $doc->get('groupid')); // While we're here, also test that the search area requests restriction by group. $modinfo = get_fast_modinfo($course); $this->assertTrue($searcharea->restrict_cm_access_by_group($modinfo->get_cm($forum->cmid))); // In visible groups mode, it won't request restriction by group. set_coursemodule_groupmode($forum->cmid, VISIBLEGROUPS); $modinfo = get_fast_modinfo($course); $this->assertFalse($searcharea->restrict_cm_access_by_group($modinfo->get_cm($forum->cmid))); } /** * Document accesses. * * @return void */ public function test_posts_access(): void { global $DB; // Returns the instance as long as the area is supported. $searcharea = \core_search\manager::get_search_area($this->forumpostareaid); $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); $course1 = self::getDataGenerator()->create_course(); $course2 = self::getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'teacher'); $this->getDataGenerator()->enrol_user($user2->id, $course1->id, 'student'); $record = new \stdClass(); $record->course = $course1->id; // Available for both student and teacher. $forum1 = self::getDataGenerator()->create_module('forum', $record); // Teacher only. $forum2 = self::getDataGenerator()->create_module('forum', $record); set_coursemodule_visible($forum2->cmid, 0); // Create discussion1. $record = new \stdClass(); $record->course = $course1->id; $record->userid = $user1->id; $record->forum = $forum1->id; $record->message = 'discussion'; $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // Create post1 in discussion1. $record = new \stdClass(); $record->discussion = $discussion1->id; $record->parent = $discussion1->firstpost; $record->userid = $user2->id; $record->message = 'post1'; $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); // Create discussion2 only visible to teacher. $record = new \stdClass(); $record->course = $course1->id; $record->userid = $user1->id; $record->forum = $forum2->id; $record->message = 'discussion'; $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // Create post2 in discussion2. $record = new \stdClass(); $record->discussion = $discussion2->id; $record->parent = $discussion2->firstpost; $record->userid = $user1->id; $record->message = 'post2'; $discussion2reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $this->setUser($user2); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($discussion1reply1->id)); $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($discussion2reply1->id)); } /** * Test for post attachments. * * @return void */ public function test_attach_files(): void { global $DB; $fs = get_file_storage(); // Returns the instance as long as the area is supported. $searcharea = \core_search\manager::get_search_area($this->forumpostareaid); $this->assertInstanceOf('\mod_forum\search\post', $searcharea); $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); $course1 = self::getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'student'); $this->getDataGenerator()->enrol_user($user2->id, $course1->id, 'student'); $record = new \stdClass(); $record->course = $course1->id; $forum1 = self::getDataGenerator()->create_module('forum', $record); // Create discussion1. $record = new \stdClass(); $record->course = $course1->id; $record->userid = $user1->id; $record->forum = $forum1->id; $record->message = 'discussion'; $record->attachemt = 1; $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // Attach 2 file to the discussion post. $post = $DB->get_record('forum_posts', array('discussion' => $discussion1->id)); $filerecord = array( 'contextid' => \context_module::instance($forum1->cmid)->id, 'component' => 'mod_forum', 'filearea' => 'attachment', 'itemid' => $post->id, 'filepath' => '/', 'filename' => 'myfile1' ); $file1 = $fs->create_file_from_string($filerecord, 'Some contents 1'); $filerecord['filename'] = 'myfile2'; $file2 = $fs->create_file_from_string($filerecord, 'Some contents 2'); // Create post1 in discussion1. $record = new \stdClass(); $record->discussion = $discussion1->id; $record->parent = $discussion1->firstpost; $record->userid = $user2->id; $record->message = 'post2'; $record->attachemt = 1; $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $filerecord['itemid'] = $discussion1reply1->id; $filerecord['filename'] = 'myfile3'; $file3 = $fs->create_file_from_string($filerecord, 'Some contents 3'); // Create post2 in discussion1. $record = new \stdClass(); $record->discussion = $discussion1->id; $record->parent = $discussion1->firstpost; $record->userid = $user2->id; $record->message = 'post3'; $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); // Now get all the posts and see if they have the right files attached. $searcharea = \core_search\manager::get_search_area($this->forumpostareaid); $recordset = $searcharea->get_recordset_by_timestamp(0); $nrecords = 0; foreach ($recordset as $record) { $doc = $searcharea->get_document($record); $searcharea->attach_files($doc); $files = $doc->get_files(); // Now check that each doc has the right files on it. switch ($doc->get('itemid')) { case ($post->id): $this->assertCount(2, $files); $this->assertEquals($file1->get_id(), $files[$file1->get_id()]->get_id()); $this->assertEquals($file2->get_id(), $files[$file2->get_id()]->get_id()); break; case ($discussion1reply1->id): $this->assertCount(1, $files); $this->assertEquals($file3->get_id(), $files[$file3->get_id()]->get_id()); break; case ($discussion1reply2->id): $this->assertCount(0, $files); break; default: $this->fail('Unexpected post returned'); break; } $nrecords++; } $recordset->close(); $this->assertEquals(3, $nrecords); } /** * Tests that reindexing works in order starting from the forum with most recent discussion. */ public function test_posts_get_contexts_to_reindex(): void { global $DB; $generator = $this->getDataGenerator(); $adminuser = get_admin(); $course1 = $generator->create_course(); $course2 = $generator->create_course(); $time = time() - 1000; // Create 3 forums (two in course 1, one in course 2 - doesn't make a difference). $forum1 = $generator->create_module('forum', ['course' => $course1->id]); $forum2 = $generator->create_module('forum', ['course' => $course1->id]); $forum3 = $generator->create_module('forum', ['course' => $course2->id]); $forum4 = $generator->create_module('forum', ['course' => $course2->id]); // Hack added time for the course_modules entries. These should not be used (they would // be used by the base class implementation). We are setting this so that the order would // be 4, 3, 2, 1 if this ordering were used (newest first). $DB->set_field('course_modules', 'added', $time + 100, ['id' => $forum1->cmid]); $DB->set_field('course_modules', 'added', $time + 110, ['id' => $forum2->cmid]); $DB->set_field('course_modules', 'added', $time + 120, ['id' => $forum3->cmid]); $DB->set_field('course_modules', 'added', $time + 130, ['id' => $forum4->cmid]); $forumgenerator = $generator->get_plugin_generator('mod_forum'); // Create one discussion in forums 1 and 3, three in forum 2, and none in forum 4. $forumgenerator->create_discussion(['course' => $course1->id, 'forum' => $forum1->id, 'userid' => $adminuser->id, 'timemodified' => $time + 20]); $forumgenerator->create_discussion(['course' => $course1->id, 'forum' => $forum2->id, 'userid' => $adminuser->id, 'timemodified' => $time + 10]); $forumgenerator->create_discussion(['course' => $course1->id, 'forum' => $forum2->id, 'userid' => $adminuser->id, 'timemodified' => $time + 30]); $forumgenerator->create_discussion(['course' => $course1->id, 'forum' => $forum2->id, 'userid' => $adminuser->id, 'timemodified' => $time + 11]); $forumgenerator->create_discussion(['course' => $course2->id, 'forum' => $forum3->id, 'userid' => $adminuser->id, 'timemodified' => $time + 25]); // Get the contexts in reindex order. $area = \core_search\manager::get_search_area($this->forumpostareaid); $contexts = iterator_to_array($area->get_contexts_to_reindex(), false); // We expect them in order of newest discussion. Forum 4 is not included at all (which is // correct because it has no content). $expected = [ \context_module::instance($forum2->cmid), \context_module::instance($forum3->cmid), \context_module::instance($forum1->cmid) ]; $this->assertEquals($expected, $contexts); } } PK-B\v&tests/portfolio_caller_test.phpnu[. namespace mod_forum; /** * Class mod_forum_portfolio_caller_testcase * * Tests behaviour of the forum_portfolio_caller class. * * @package mod_forum * @copyright 2018 onwards Totara Learning Solutions LTD {@link http://www.totaralms.com/} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Brendan Cox */ final class portfolio_caller_test extends \advanced_testcase { /** * Ensure that a file will be loaded in an instance of the caller when supplied valid and * accessible post and attachment file ids. */ public function test_file_in_user_post_is_loaded(): void { global $CFG; require_once($CFG->dirroot . '/mod/forum/locallib.php'); $this->resetAfterTest(true); $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $context = \context_module::instance($forum->cmid); /* @var mod_forum_generator $forumgenerator */ $forumgenerator = $this->getDataGenerator()->get_plugin_generator('mod_forum'); $discussion = $forumgenerator->create_discussion( array( 'course' => $course->id, 'forum' => $forum->id, 'userid' => $user->id, 'attachment' => 1 ) ); $fs = get_file_storage(); $dummy = (object) array( 'contextid' => $context->id, 'component' => 'mod_forum', 'filearea' => 'attachment', 'itemid' => $discussion->firstpost, 'filepath' => '/', 'filename' => 'myassignmnent.pdf' ); $firstpostfile = $fs->create_file_from_string($dummy, 'Content of ' . $dummy->filename); $caller = new \forum_portfolio_caller(array( 'postid' => $discussion->firstpost, 'attachment' => $firstpostfile->get_id() )); $caller->load_data(); $this->assertEquals($caller->get_sha1_file(), $firstpostfile->get_contenthash()); } /** * Ensure that files will not be loaded if the supplied attachment id is for a file that is not attached to * the supplied post id. */ public function test_file_not_in_user_post_not_loaded(): void { global $CFG; require_once($CFG->dirroot . '/mod/forum/locallib.php'); $this->resetAfterTest(true); $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $context = \context_module::instance($forum->cmid); /* @var mod_forum_generator $forumgenerator */ $forumgenerator = $this->getDataGenerator()->get_plugin_generator('mod_forum'); $discussion = $forumgenerator->create_discussion( array( 'course' => $course->id, 'forum' => $forum->id, 'userid' => $user->id, 'attachment' => 1 ) ); $fs = get_file_storage(); $dummyone = (object) array( 'contextid' => $context->id, 'component' => 'mod_forum', 'filearea' => 'attachment', 'itemid' => $discussion->firstpost, 'filepath' => '/', 'filename' => 'myassignmnent.pdf' ); $firstpostfile = $fs->create_file_from_string($dummyone, 'Content of ' . $dummyone->filename); // Create a second post and add a file there. $secondpost = $forumgenerator->create_post( array( 'discussion' => $discussion->id, 'userid' => $user->id, 'attachment' => 1 ) ); $dummytwo = (object) array( 'contextid' => $context->id, 'component' => 'mod_forum', 'filearea' => 'attachment', 'itemid' => $secondpost->id, 'filepath' => '/', 'filename' => 'myotherthing.pdf' ); $secondpostfile = $fs->create_file_from_string($dummytwo, 'Content of ' . $dummytwo->filename); $caller = new \forum_portfolio_caller(array( 'postid' => $discussion->firstpost, 'attachment' => $secondpostfile->get_id() )); $this->expectExceptionMessage('Sorry, the requested file could not be found'); $caller->load_data(); } } PK-B\ tests/local_container_test.phpnu[. namespace mod_forum; /** * Tests for the local\container class. * * @package mod_forum * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \mod_forum\local\container */ final class local_container_test extends \advanced_testcase { /** * Ensure that a renderer factory is returned. * * @covers ::get_renderer_factory */ public function test_get_renderer_factory(): void { $this->assertInstanceOf(\mod_forum\local\factories\renderer::class, \mod_forum\local\container::get_renderer_factory()); } /** * Ensure that a legacy_data_mapper_factory factory is returned. * * @covers ::get_legacy_data_mapper_factory */ public function test_get_legacy_data_mapper_factory(): void { $this->assertInstanceOf( \mod_forum\local\factories\legacy_data_mapper::class, \mod_forum\local\container::get_legacy_data_mapper_factory() ); } /** * Ensure that a exporter factory is returned. * * @covers ::get_exporter_factory */ public function test_get_exporter_factory(): void { $this->assertInstanceOf(\mod_forum\local\factories\exporter::class, \mod_forum\local\container::get_exporter_factory()); } /** * Ensure that a vault factory is returned. * * @covers ::get_vault_factory */ public function test_get_vault_factory(): void { $this->assertInstanceOf(\mod_forum\local\factories\vault::class, \mod_forum\local\container::get_vault_factory()); } /** * Ensure that a manager factory is returned. * * @covers ::get_manager_factory */ public function test_get_manager_factory(): void { $this->assertInstanceOf(\mod_forum\local\factories\manager::class, \mod_forum\local\container::get_manager_factory()); } /** * Ensure that a entity factory is returned. * * @covers ::get_entity_factory */ public function test_get_entity_factory(): void { $this->assertInstanceOf(\mod_forum\local\factories\entity::class, \mod_forum\local\container::get_entity_factory()); } /** * Ensure that a builder factory is returned. * * @covers ::get_builder_factory */ public function test_get_builder_factory(): void { $this->assertInstanceOf(\mod_forum\local\factories\builder::class, \mod_forum\local\container::get_builder_factory()); } /** * Ensure that a url factory is returned. * * @covers ::get_url_factory */ public function test_get_url_factory(): void { $this->assertInstanceOf(\mod_forum\local\factories\url::class, \mod_forum\local\container::get_url_factory()); } } PK-B\} tests/dates_test.phpnu[. /** * Contains unit tests for mod_forum\dates. * * @package mod_forum * @category test * @copyright 2021 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types=1); namespace mod_forum; use advanced_testcase; use cm_info; use core\activity_dates; /** * Class for unit testing mod_forum\dates. * * @copyright 2021 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class dates_test extends advanced_testcase { /** * Data provider for get_dates_for_module(). * @return array[] */ public static function get_dates_for_module_provider(): array { $now = time(); $before = $now - DAYSECS; $after = $now + DAYSECS; return [ 'without any dates' => [ null, [] ], 'future due date' => [ $after, [ ['label' => 'Due:', 'timestamp' => $after, 'dataid' => 'duedate'], ] ], 'due date is past' => [ $before, [ ['label' => 'Due:', 'timestamp' => $before, 'dataid' => 'duedate'], ] ], ]; } /** * Test for get_dates_for_module(). * * @dataProvider get_dates_for_module_provider * @param int|null $duedate Forum's due date. * @param array $expected The expected value of calling get_dates_for_module() */ public function test_get_dates_for_module(?int $duedate, array $expected): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $data = ['course' => $course->id]; if ($duedate) { $data['duedate'] = $duedate; } $this->setAdminUser(); $forum = $this->getDataGenerator()->create_module('forum', $data); $this->setUser($user); $cm = get_coursemodule_from_instance('forum', $forum->id); // Make sure we're using a cm_info object. $cm = cm_info::create($cm); $dates = activity_dates::get_dates_for_module($cm, (int) $user->id); $this->assertEquals($expected, $dates); } } PK-B\r!e!etests/externallib_test.phpnu[. namespace mod_forum; use core_external\external_api; use externallib_advanced_testcase; use mod_forum_external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); require_once($CFG->dirroot . '/mod/forum/lib.php'); /** * The module forums external functions unit tests * * @package mod_forum * @category external * @copyright 2012 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class externallib_test extends externallib_advanced_testcase { /** * Tests set up */ protected function setUp(): void { global $CFG; parent::setUp(); // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); require_once($CFG->dirroot . '/mod/forum/externallib.php'); } public function tearDown(): void { // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); parent::tearDown(); } /** * Get the expected attachment. * * @param \stored_file $file * @param array $values * @param \moodle_url|null $url * @return array */ protected function get_expected_attachment(\stored_file $file, array $values = [], ?\moodle_url $url = null): array { if (!$url) { $url = \moodle_url::make_pluginfile_url( $file->get_contextid(), $file->get_component(), $file->get_filearea(), $file->get_itemid(), $file->get_filepath(), $file->get_filename() ); $url->param('forcedownload', 1); } return array_merge( [ 'contextid' => $file->get_contextid(), 'component' => $file->get_component(), 'filearea' => $file->get_filearea(), 'itemid' => $file->get_itemid(), 'filepath' => $file->get_filepath(), 'filename' => $file->get_filename(), 'isdir' => $file->is_directory(), 'isimage' => $file->is_valid_image(), 'timemodified' => $file->get_timemodified(), 'timecreated' => $file->get_timecreated(), 'filesize' => $file->get_filesize(), 'author' => $file->get_author(), 'license' => $file->get_license(), 'filenameshort' => $file->get_filename(), 'filesizeformatted' => display_size((int) $file->get_filesize()), 'icon' => $file->is_directory() ? file_folder_icon() : file_file_icon($file), 'timecreatedformatted' => userdate($file->get_timecreated()), 'timemodifiedformatted' => userdate($file->get_timemodified()), 'url' => $url->out(), ], $values); } /** * Test get forums */ public function test_mod_forum_get_forums_by_courses(): void { global $USER, $CFG, $DB; $this->resetAfterTest(true); // Create a user. $user = self::getDataGenerator()->create_user(array('trackforums' => 1)); // Set to the user. self::setUser($user); // Create courses to add the modules. $course1 = self::getDataGenerator()->create_course(); $course2 = self::getDataGenerator()->create_course(); // First forum. $record = new \stdClass(); $record->introformat = FORMAT_HTML; $record->course = $course1->id; $record->trackingtype = FORUM_TRACKING_FORCED; $forum1 = self::getDataGenerator()->create_module('forum', $record); // Second forum. $record = new \stdClass(); $record->introformat = FORMAT_HTML; $record->course = $course2->id; $record->trackingtype = FORUM_TRACKING_OFF; $forum2 = self::getDataGenerator()->create_module('forum', $record); $forum2->introfiles = []; // Add discussions to the forums. $record = new \stdClass(); $record->course = $course1->id; $record->userid = $user->id; $record->forum = $forum1->id; $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // Expect one discussion. $forum1->numdiscussions = 1; $forum1->cancreatediscussions = true; $forum1->istracked = true; $forum1->unreadpostscount = 0; $forum1->introfiles = []; $forum1->lang = ''; $record = new \stdClass(); $record->course = $course2->id; $record->userid = $user->id; $record->forum = $forum2->id; $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); $discussion3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // Expect two discussions. $forum2->numdiscussions = 2; // Default limited role, no create discussion capability enabled. $forum2->cancreatediscussions = false; $forum2->istracked = false; $forum2->lang = ''; // Check the forum was correctly created. $this->assertEquals(2, $DB->count_records_select('forum', 'id = :forum1 OR id = :forum2', array('forum1' => $forum1->id, 'forum2' => $forum2->id))); // Enrol the user in two courses. // DataGenerator->enrol_user automatically sets a role for the user with the permission mod/form:viewdiscussion. $this->getDataGenerator()->enrol_user($user->id, $course1->id, null, 'manual'); // Execute real Moodle enrolment as we'll call unenrol() method on the instance later. $enrol = enrol_get_plugin('manual'); $enrolinstances = enrol_get_instances($course2->id, true); foreach ($enrolinstances as $courseenrolinstance) { if ($courseenrolinstance->enrol == "manual") { $instance2 = $courseenrolinstance; break; } } $enrol->enrol_user($instance2, $user->id); // Assign capabilities to view forums for forum 2. $cm2 = get_coursemodule_from_id('forum', $forum2->cmid, 0, false, MUST_EXIST); $context2 = \context_module::instance($cm2->id); $newrole = create_role('Role 2', 'role2', 'Role 2 description'); $roleid2 = $this->assignUserCapability('mod/forum:viewdiscussion', $context2->id, $newrole); // Create what we expect to be returned when querying the two courses. unset($forum1->displaywordcount); unset($forum2->displaywordcount); $expectedforums = array(); $expectedforums[$forum1->id] = (array) $forum1; $expectedforums[$forum2->id] = (array) $forum2; // Call the external function passing course ids. $forums = mod_forum_external::get_forums_by_courses(array($course1->id, $course2->id)); $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums); $this->assertCount(2, $forums); foreach ($forums as $forum) { $this->assertEquals($expectedforums[$forum['id']], $forum); } // Call the external function without passing course id. $forums = mod_forum_external::get_forums_by_courses(); $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums); $this->assertCount(2, $forums); foreach ($forums as $forum) { $this->assertEquals($expectedforums[$forum['id']], $forum); } // Unenrol user from second course and alter expected forums. $enrol->unenrol_user($instance2, $user->id); unset($expectedforums[$forum2->id]); // Call the external function without passing course id. $forums = mod_forum_external::get_forums_by_courses(); $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums); $this->assertCount(1, $forums); $this->assertEquals($expectedforums[$forum1->id], $forums[0]); $this->assertTrue($forums[0]['cancreatediscussions']); // Change the type of the forum, the user shouldn't be able to add discussions. $DB->set_field('forum', 'type', 'news', array('id' => $forum1->id)); $forums = mod_forum_external::get_forums_by_courses(); $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums); $this->assertFalse($forums[0]['cancreatediscussions']); // Call for the second course we unenrolled the user from. $forums = mod_forum_external::get_forums_by_courses(array($course2->id)); $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums); $this->assertCount(0, $forums); } /** * Test the toggle favourite state */ public function test_mod_forum_toggle_favourite_state(): void { global $USER, $CFG, $DB; $this->resetAfterTest(true); // Create a user. $user = self::getDataGenerator()->create_user(array('trackforums' => 1)); // Set to the user. self::setUser($user); // Create courses to add the modules. $course1 = self::getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($user->id, $course1->id); $record = new \stdClass(); $record->introformat = FORMAT_HTML; $record->course = $course1->id; $record->trackingtype = FORUM_TRACKING_OFF; $forum1 = self::getDataGenerator()->create_module('forum', $record); $forum1->introfiles = []; // Add discussions to the forums. $record = new \stdClass(); $record->course = $course1->id; $record->userid = $user->id; $record->forum = $forum1->id; $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); $response = mod_forum_external::toggle_favourite_state($discussion1->id, 1); $response = external_api::clean_returnvalue(mod_forum_external::toggle_favourite_state_returns(), $response); $this->assertTrue($response['userstate']['favourited']); $response = mod_forum_external::toggle_favourite_state($discussion1->id, 0); $response = external_api::clean_returnvalue(mod_forum_external::toggle_favourite_state_returns(), $response); $this->assertFalse($response['userstate']['favourited']); $this->setUser(0); try { $response = mod_forum_external::toggle_favourite_state($discussion1->id, 0); } catch (\moodle_exception $e) { $this->assertEquals('requireloginerror', $e->errorcode); } } /** * Test the toggle pin state */ public function test_mod_forum_set_pin_state(): void { $this->resetAfterTest(true); // Create a user. $user = self::getDataGenerator()->create_user(array('trackforums' => 1)); // Set to the user. self::setUser($user); // Create courses to add the modules. $course1 = self::getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($user->id, $course1->id); $record = new \stdClass(); $record->introformat = FORMAT_HTML; $record->course = $course1->id; $record->trackingtype = FORUM_TRACKING_OFF; $forum1 = self::getDataGenerator()->create_module('forum', $record); $forum1->introfiles = []; // Add discussions to the forums. $record = new \stdClass(); $record->course = $course1->id; $record->userid = $user->id; $record->forum = $forum1->id; $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); try { $response = mod_forum_external::set_pin_state($discussion1->id, 1); } catch (\Exception $e) { $this->assertEquals('cannotpindiscussions', $e->errorcode); } self::setAdminUser(); $response = mod_forum_external::set_pin_state($discussion1->id, 1); $response = external_api::clean_returnvalue(mod_forum_external::set_pin_state_returns(), $response); $this->assertTrue($response['pinned']); $response = mod_forum_external::set_pin_state($discussion1->id, 0); $response = external_api::clean_returnvalue(mod_forum_external::set_pin_state_returns(), $response); $this->assertFalse($response['pinned']); } /** * Test get forum posts * * Tests is similar to the get_forum_discussion_posts only utilizing the new return structure and entities */ public function test_mod_forum_get_discussion_posts(): void { global $CFG; $this->resetAfterTest(true); // Set the CFG variable to allow track forums. $CFG->forum_trackreadposts = true; $urlfactory = \mod_forum\local\container::get_url_factory(); $legacyfactory = \mod_forum\local\container::get_legacy_data_mapper_factory(); $entityfactory = \mod_forum\local\container::get_entity_factory(); // Create course to add the module. $course1 = self::getDataGenerator()->create_course(); // Create a user who can track forums. $record = new \stdClass(); $record->trackforums = true; $user1 = self::getDataGenerator()->create_user($record); // 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), 'isdeleted' => false, 'groups' => [], 'urls' => [ 'profile' => $urlfactory->get_author_profile_url($user2entity, $course1->id)->out(false), 'profileimage' => $urlfactory->get_author_profile_image_url($user2entity), ] ]; $user2->fullname = $exporteduser2['fullname']; $user3 = self::getDataGenerator()->create_user(['fullname' => "Mr Pants 1"]); $user3entity = $entityfactory->get_author_from_stdClass($user3); $exporteduser3 = [ 'id' => (int) $user3->id, 'fullname' => fullname($user3), 'groups' => [], 'isdeleted' => false, 'urls' => [ 'profile' => $urlfactory->get_author_profile_url($user3entity, $course1->id)->out(false), 'profileimage' => $urlfactory->get_author_profile_image_url($user3entity), ] ]; $user3->fullname = $exporteduser3['fullname']; $forumgenerator = self::getDataGenerator()->get_plugin_generator('mod_forum'); // Set the first created user to the test user. self::setUser($user1); // Forum with tracking off. $record = new \stdClass(); $record->course = $course1->id; $record->trackingtype = FORUM_TRACKING_OFF; // Display word count. Otherwise, word and char counts will be set to null by the forum post exporter. $record->displaywordcount = true; $forum1 = self::getDataGenerator()->create_module('forum', $record); $forum1context = \context_module::instance($forum1->cmid); // Forum with tracking enabled. $record = new \stdClass(); $record->course = $course1->id; $forum2 = self::getDataGenerator()->create_module('forum', $record); $forum2cm = get_coursemodule_from_id('forum', $forum2->cmid); $forum2context = \context_module::instance($forum2->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); $record = new \stdClass(); $record->course = $course1->id; $record->userid = $user2->id; $record->forum = $forum1->id; $discussion2 = $forumgenerator->create_discussion($record); $record = new \stdClass(); $record->course = $course1->id; $record->userid = $user2->id; $record->forum = $forum2->id; $discussion3 = $forumgenerator->create_discussion($record); // Add 2 replies to the discussion 1 from different users. $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(); $timepost = time(); $file = $fs->create_file_from_string($filerecordinline, 'image contents (not really)'); $record->parent = $discussion1reply1->id; $record->userid = $user3->id; $discussion1reply2 = $forumgenerator->create_post($record); // Enrol the user in the course. $enrol = enrol_get_plugin('manual'); // 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); // Delete one user, to test that we still receive posts by this user. delete_user($user3); $exporteduser3 = [ 'id' => (int) $user3->id, 'fullname' => get_string('deleteduser', 'mod_forum'), 'groups' => [], 'isdeleted' => true, 'urls' => [ 'profile' => $urlfactory->get_author_profile_url($user3entity, $course1->id)->out(false), 'profileimage' => $urlfactory->get_author_profile_image_url($user3entity), ] ]; // Create what we expect to be returned when querying the discussion. $expectedposts = array( 'posts' => array(), 'courseid' => $course1->id, 'forumid' => $forum1->id, 'ratinginfo' => array( 'contextid' => $forum1context->id, 'component' => 'mod_forum', 'ratingarea' => 'post', 'canviewall' => null, 'canviewany' => null, 'scales' => array(), 'ratings' => array(), ), 'warnings' => array(), ); // User pictures are initially empty, we should get the links once the external function is called. $isolatedurl = $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply2->discussion); $isolatedurl->params(['parent' => $discussion1reply2->id]); $message = file_rewrite_pluginfile_urls($discussion1reply2->message, 'pluginfile.php', $forum1context->id, 'mod_forum', 'post', $discussion1reply2->id); $expectedposts['posts'][] = array( 'id' => $discussion1reply2->id, 'discussionid' => $discussion1reply2->discussion, 'parentid' => $discussion1reply2->parent, 'hasparent' => true, 'timecreated' => $discussion1reply2->created, 'timemodified' => $discussion1reply2->modified, 'subject' => $discussion1reply2->subject, 'replysubject' => $discussion1reply2->subject, 'message' => $message, 'messageformat' => 1, // This value is usually changed by \core_external\util::format_text() function. 'unread' => null, 'isdeleted' => false, 'isprivatereply' => false, 'haswordcount' => true, 'wordcount' => count_words($message), 'charcount' => count_letters($message), 'author'=> $exporteduser3, 'attachments' => [], 'messageinlinefiles' => [], 'tags' => [], 'html' => [ 'rating' => null, 'taglist' => null, 'authorsubheading' => $forumgenerator->get_author_subheading_html((object)$exporteduser3, $discussion1reply2->created) ], 'capabilities' => [ 'view' => 1, 'edit' => 0, 'delete' => 0, 'split' => 0, 'reply' => 1, 'export' => 0, 'controlreadstatus' => 0, 'canreplyprivately' => 0, 'selfenrol' => 0 ], 'urls' => [ 'view' => $urlfactory->get_view_post_url_from_post_id($discussion1reply2->discussion, $discussion1reply2->id), 'viewisolated' => $isolatedurl->out(false), 'viewparent' => $urlfactory->get_view_post_url_from_post_id($discussion1reply2->discussion, $discussion1reply2->parent), 'edit' => null, 'delete' =>null, 'split' => null, 'reply' => (new \moodle_url('/mod/forum/post.php#mformforum', [ 'reply' => $discussion1reply2->id ]))->out(false), 'export' => null, 'markasread' => null, 'markasunread' => null, 'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply2->discussion), ], ); $isolatedurl = $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply1->discussion); $isolatedurl->params(['parent' => $discussion1reply1->id]); $message = file_rewrite_pluginfile_urls($discussion1reply1->message, 'pluginfile.php', $forum1context->id, 'mod_forum', 'post', $discussion1reply1->id); $expectedposts['posts'][] = array( 'id' => $discussion1reply1->id, 'discussionid' => $discussion1reply1->discussion, 'parentid' => $discussion1reply1->parent, 'hasparent' => true, 'timecreated' => $discussion1reply1->created, 'timemodified' => $discussion1reply1->modified, 'subject' => $discussion1reply1->subject, 'replysubject' => $discussion1reply1->subject, 'message' => $message, 'messageformat' => 1, // This value is usually changed by \core_external\util::format_text() function. 'unread' => null, 'isdeleted' => false, 'isprivatereply' => false, 'haswordcount' => true, 'wordcount' => count_words($message), 'charcount' => count_letters($message), 'author'=> $exporteduser2, 'attachments' => [], 'messageinlinefiles' => [ 0 => $this->get_expected_attachment($file) ], 'tags' => [], 'html' => [ 'rating' => null, 'taglist' => null, 'authorsubheading' => $forumgenerator->get_author_subheading_html((object)$exporteduser2, $discussion1reply1->created) ], 'capabilities' => [ 'view' => 1, 'edit' => 0, 'delete' => 0, 'split' => 0, 'reply' => 1, 'export' => 0, 'controlreadstatus' => 0, 'canreplyprivately' => 0, 'selfenrol' => 0 ], 'urls' => [ 'view' => $urlfactory->get_view_post_url_from_post_id($discussion1reply1->discussion, $discussion1reply1->id), 'viewisolated' => $isolatedurl->out(false), 'viewparent' => $urlfactory->get_view_post_url_from_post_id($discussion1reply1->discussion, $discussion1reply1->parent), 'edit' => null, 'delete' =>null, '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), ], ); // Test a discussion with two additional posts (total 3 posts). $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'DESC', true); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); $this->assertEquals(3, count($posts['posts'])); // Unset the initial discussion post. array_pop($posts['posts']); $this->assertEquals($expectedposts, $posts); // Check we receive the unread count correctly on tracked forum. forum_tp_count_forum_unread_posts($forum2cm, $course1, true); // Reset static cache. $result = mod_forum_external::get_forums_by_courses(array($course1->id)); $result = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $result); foreach ($result as $f) { if ($f['id'] == $forum2->id) { $this->assertEquals(1, $f['unreadpostscount']); } } // Test discussion without additional posts. There should be only one post (the one created by the discussion). $posts = mod_forum_external::get_discussion_posts($discussion2->id, 'modified', 'DESC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); $this->assertEquals(1, count($posts['posts'])); // Test discussion tracking on not tracked forum. $result = mod_forum_external::view_forum_discussion($discussion1->id); $result = external_api::clean_returnvalue(mod_forum_external::view_forum_discussion_returns(), $result); $this->assertTrue($result['status']); $this->assertEmpty($result['warnings']); // Test posts have not been marked as read. $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'DESC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); foreach ($posts['posts'] as $post) { $this->assertNull($post['unread']); } // Test discussion tracking on tracked forum. $result = mod_forum_external::view_forum_discussion($discussion3->id); $result = external_api::clean_returnvalue(mod_forum_external::view_forum_discussion_returns(), $result); $this->assertTrue($result['status']); $this->assertEmpty($result['warnings']); // Test posts have been marked as read. $posts = mod_forum_external::get_discussion_posts($discussion3->id, 'modified', 'DESC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); foreach ($posts['posts'] as $post) { $this->assertFalse($post['unread']); } // Check we receive 0 unread posts. forum_tp_count_forum_unread_posts($forum2cm, $course1, true); // Reset static cache. $result = mod_forum_external::get_forums_by_courses(array($course1->id)); $result = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $result); foreach ($result as $f) { if ($f['id'] == $forum2->id) { $this->assertEquals(0, $f['unreadpostscount']); } } } /** * Test get forum posts */ public function test_mod_forum_get_discussion_posts_deleted(): void { global $CFG, $PAGE; $this->resetAfterTest(true); $generator = self::getDataGenerator()->get_plugin_generator('mod_forum'); // Create a course and enrol some users in it. $course1 = self::getDataGenerator()->create_course(); // Create users. $user1 = self::getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course1->id); $user2 = self::getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user2->id, $course1->id); // Set the first created user to the test user. self::setUser($user1); // Create test data. $forum1 = self::getDataGenerator()->create_module('forum', (object) [ 'course' => $course1->id, ]); $forum1context = \context_module::instance($forum1->cmid); // Add discussions to the forum. $discussion = $generator->create_discussion((object) [ 'course' => $course1->id, 'userid' => $user1->id, 'forum' => $forum1->id, ]); $discussion2 = $generator->create_discussion((object) [ 'course' => $course1->id, 'userid' => $user2->id, 'forum' => $forum1->id, ]); // Add replies to the discussion. $discussionreply1 = $generator->create_post((object) [ 'discussion' => $discussion->id, 'parent' => $discussion->firstpost, 'userid' => $user2->id, ]); $discussionreply2 = $generator->create_post((object) [ 'discussion' => $discussion->id, 'parent' => $discussionreply1->id, 'userid' => $user2->id, 'subject' => '', 'message' => '', 'messageformat' => FORMAT_PLAIN, 'deleted' => 1, ]); $discussionreply3 = $generator->create_post((object) [ 'discussion' => $discussion->id, 'parent' => $discussion->firstpost, 'userid' => $user2->id, ]); // Test where some posts have been marked as deleted. $posts = mod_forum_external::get_discussion_posts($discussion->id, 'modified', 'DESC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); $deletedsubject = get_string('forumsubjectdeleted', 'mod_forum'); $deletedmessage = get_string('forumbodydeleted', 'mod_forum'); foreach ($posts['posts'] as $post) { if ($post['id'] == $discussionreply2->id) { $this->assertTrue($post['isdeleted']); $this->assertEquals($deletedsubject, $post['subject']); $this->assertEquals($deletedmessage, $post['message']); } else { $this->assertFalse($post['isdeleted']); $this->assertNotEquals($deletedsubject, $post['subject']); $this->assertNotEquals($deletedmessage, $post['message']); } } } /** * Test get forum posts returns inline attachments. */ public function test_mod_forum_get_discussion_posts_inline_attachments(): void { global $CFG; $this->resetAfterTest(true); // Create a course and enrol some users in it. $course = self::getDataGenerator()->create_course(); // Create users. $user = self::getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); // Set the first created user to the test user. self::setUser($user); // Create test data. $forum = self::getDataGenerator()->create_module('forum', (object) [ 'course' => $course->id, ]); // Create a file in a draft area for inline attachments. $draftidinlineattach = file_get_unused_draft_itemid(); $draftidattach = file_get_unused_draft_itemid(); self::setUser($user); $usercontext = \context_user::instance($user->id); $filepath = '/'; $filearea = 'draft'; $component = 'user'; $filenameimg = 'fakeimage.png'; $filerecordinline = [ 'contextid' => $usercontext->id, 'component' => $component, 'filearea' => $filearea, 'itemid' => $draftidinlineattach, 'filepath' => $filepath, 'filename' => $filenameimg, ]; $fs = get_file_storage(); $fs->create_file_from_string($filerecordinline, 'image contents (not really)'); // Create discussion. $dummytext = 'Here is an inline image: id}/user/draft/{$draftidinlineattach}/{$filenameimg}" . '" alt="inlineimage">.'; $options = [ [ 'name' => 'inlineattachmentsid', 'value' => $draftidinlineattach ], [ 'name' => 'attachmentsid', 'value' => $draftidattach ] ]; $discussion = mod_forum_external::add_discussion($forum->id, 'the inline attachment subject', $dummytext, -1, $options); $posts = mod_forum_external::get_discussion_posts($discussion['discussionid'], 'modified', 'DESC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); $post = $posts['posts'][0]; $this->assertCount(0, $post['messageinlinefiles']); $this->assertEmpty($post['messageinlinefiles']); $posts = mod_forum_external::get_discussion_posts($discussion['discussionid'], 'modified', 'DESC', true); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); $post = $posts['posts'][0]; $this->assertCount(1, $post['messageinlinefiles']); $this->assertEquals('fakeimage.png', $post['messageinlinefiles'][0]['filename']); } /** * Test get forum posts (qanda forum) */ public function test_mod_forum_get_discussion_posts_qanda(): void { global $CFG, $DB; $this->resetAfterTest(true); $record = new \stdClass(); $user1 = self::getDataGenerator()->create_user($record); $user2 = self::getDataGenerator()->create_user(); // Set the first created user to the test user. self::setUser($user1); // Create course to add the module. $course1 = self::getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($user1->id, $course1->id); $this->getDataGenerator()->enrol_user($user2->id, $course1->id); // Forum with tracking off. $record = new \stdClass(); $record->course = $course1->id; $record->type = 'qanda'; $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 = $user2->id; $record->forum = $forum1->id; $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // Add 1 reply (not the actual user). $record = new \stdClass(); $record->discussion = $discussion1->id; $record->parent = $discussion1->firstpost; $record->userid = $user2->id; $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); // We still see only the original post. $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'DESC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); $this->assertEquals(1, count($posts['posts'])); // Add a new reply, the user is going to be able to see only the original post and their new post. $record = new \stdClass(); $record->discussion = $discussion1->id; $record->parent = $discussion1->firstpost; $record->userid = $user1->id; $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'DESC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); $this->assertEquals(2, count($posts['posts'])); // Now, we can fake the time of the user post, so he can se the rest of the discussion posts. $discussion1reply2->created -= $CFG->maxeditingtime * 2; $DB->update_record('forum_posts', $discussion1reply2); $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'DESC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); $this->assertEquals(3, count($posts['posts'])); } /** * Test get forum discussions */ public function test_mod_forum_get_forum_discussions(): void { global $CFG, $DB, $PAGE; $this->resetAfterTest(true); // Set the CFG variable to allow track forums. $CFG->forum_trackreadposts = true; // Create a user who can track forums. $record = new \stdClass(); $record->trackforums = true; $user1 = self::getDataGenerator()->create_user($record); // Create a bunch of other users to post. $user2 = self::getDataGenerator()->create_user(); $user3 = self::getDataGenerator()->create_user(); $user4 = self::getDataGenerator()->create_user(); // Set the first created user to the test user. self::setUser($user1); // Create courses to add the modules. $course1 = self::getDataGenerator()->create_course(); // First forum with tracking off. $record = new \stdClass(); $record->course = $course1->id; $record->trackingtype = FORUM_TRACKING_OFF; $forum1 = self::getDataGenerator()->create_module('forum', $record); // Add discussions to the forums. $record = new \stdClass(); $record->course = $course1->id; $record->userid = $user1->id; $record->forum = $forum1->id; $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // Add three replies to the discussion 1 from different users. $record = new \stdClass(); $record->discussion = $discussion1->id; $record->parent = $discussion1->firstpost; $record->userid = $user2->id; $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $record->parent = $discussion1reply1->id; $record->userid = $user3->id; $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $record->userid = $user4->id; $discussion1reply3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); // Enrol the user in the first course. $enrol = enrol_get_plugin('manual'); // We don't use the dataGenerator as we need to get the $instance2 to unenrol later. $enrolinstances = enrol_get_instances($course1->id, true); foreach ($enrolinstances as $courseenrolinstance) { if ($courseenrolinstance->enrol == "manual") { $instance1 = $courseenrolinstance; break; } } $enrol->enrol_user($instance1, $user1->id); // Delete one user. delete_user($user4); // Assign capabilities to view discussions for forum 1. $cm = get_coursemodule_from_id('forum', $forum1->cmid, 0, false, MUST_EXIST); $context = \context_module::instance($cm->id); $newrole = create_role('Role 2', 'role2', 'Role 2 description'); $this->assignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole); // Create what we expect to be returned when querying the forums. $post1 = $DB->get_record('forum_posts', array('id' => $discussion1->firstpost), '*', MUST_EXIST); // User pictures are initially empty, we should get the links once the external function is called. $expecteddiscussions = array( 'id' => $discussion1->firstpost, 'name' => $discussion1->name, 'groupid' => (int) $discussion1->groupid, 'timemodified' => (int) $discussion1reply3->created, 'usermodified' => (int) $discussion1reply3->userid, 'timestart' => (int) $discussion1->timestart, 'timeend' => (int) $discussion1->timeend, 'discussion' => (int) $discussion1->id, 'parent' => 0, 'userid' => (int) $discussion1->userid, 'created' => (int) $post1->created, 'modified' => (int) $post1->modified, 'mailed' => (int) $post1->mailed, 'subject' => $post1->subject, 'message' => $post1->message, 'messageformat' => (int) $post1->messageformat, 'messagetrust' => (int) $post1->messagetrust, 'attachment' => $post1->attachment, 'totalscore' => (int) $post1->totalscore, 'mailnow' => (int) $post1->mailnow, 'userfullname' => fullname($user1), 'usermodifiedfullname' => fullname($user4), 'userpictureurl' => '', 'usermodifiedpictureurl' => '', 'numreplies' => 3, 'numunread' => 0, 'pinned' => (bool) FORUM_DISCUSSION_UNPINNED, 'locked' => false, 'canreply' => false, 'canlock' => false, 'starred' => false, 'canfavourite' => true ); // Call the external function passing forum id. $discussions = mod_forum_external::get_forum_discussions($forum1->id); $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions); $expectedreturn = array( 'discussions' => array($expecteddiscussions), 'warnings' => array() ); // Wait the theme to be loaded (the external_api call does that) to generate the user profiles. $userpicture = new \user_picture($user1); $userpicture->size = 2; // Size f2. $expectedreturn['discussions'][0]['userpictureurl'] = $userpicture->get_url($PAGE)->out(false); $userpicture = new \user_picture($user4); $userpicture->size = 2; // Size f2. $expectedreturn['discussions'][0]['usermodifiedpictureurl'] = $userpicture->get_url($PAGE)->out(false); $this->assertEquals($expectedreturn, $discussions); // Test the starring functionality return. $t = mod_forum_external::toggle_favourite_state($discussion1->id, 1); $expectedreturn['discussions'][0]['starred'] = true; $discussions = mod_forum_external::get_forum_discussions($forum1->id); $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions); $this->assertEquals($expectedreturn, $discussions); // Call without required view discussion capability. $this->unassignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole); try { mod_forum_external::get_forum_discussions($forum1->id); $this->fail('Exception expected due to missing capability.'); } catch (\moodle_exception $e) { $this->assertEquals('noviewdiscussionspermission', $e->errorcode); } // Unenrol user from second course. $enrol->unenrol_user($instance1, $user1->id); // Call for the second course we unenrolled the user from, make sure exception thrown. try { mod_forum_external::get_forum_discussions($forum1->id); $this->fail('Exception expected due to being unenrolled from the course.'); } catch (\moodle_exception $e) { $this->assertEquals('requireloginerror', $e->errorcode); } $this->setAdminUser(); $discussions = mod_forum_external::get_forum_discussions($forum1->id); $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions); $this->assertTrue($discussions['discussions'][0]['canlock']); } /** * Test the sorting in get forum discussions */ public function test_mod_forum_get_forum_discussions_sorting(): void { global $CFG, $DB, $PAGE; $this->resetAfterTest(true); $clock = $this->mock_clock_with_frozen(); // Set the CFG variable to allow track forums. $CFG->forum_trackreadposts = true; // Create a user who can track forums. $record = new \stdClass(); $record->trackforums = true; $user1 = self::getDataGenerator()->create_user($record); // Create a bunch of other users to post. $user2 = self::getDataGenerator()->create_user(); $user3 = self::getDataGenerator()->create_user(); $user4 = self::getDataGenerator()->create_user(); // Set the first created user to the test user. self::setUser($user1); // Create courses to add the modules. $course1 = self::getDataGenerator()->create_course(); // Enrol the user in the first course. $enrol = enrol_get_plugin('manual'); // We don't use the dataGenerator as we need to get the $instance2 to unenrol later. $enrolinstances = enrol_get_instances($course1->id, true); foreach ($enrolinstances as $courseenrolinstance) { if ($courseenrolinstance->enrol == "manual") { $instance1 = $courseenrolinstance; break; } } $enrol->enrol_user($instance1, $user1->id); // First forum with tracking off. $record = new \stdClass(); $record->course = $course1->id; $record->trackingtype = FORUM_TRACKING_OFF; $forum1 = self::getDataGenerator()->create_module('forum', $record); // Assign capabilities to view discussions for forum 1. $cm = get_coursemodule_from_id('forum', $forum1->cmid, 0, false, MUST_EXIST); $context = \context_module::instance($cm->id); $newrole = create_role('Role 2', 'role2', 'Role 2 description'); $this->assignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole); // Add discussions to the forums. $record = new \stdClass(); $record->course = $course1->id; $record->userid = $user1->id; $record->forum = $forum1->id; $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); $clock->bump(); // Add three replies to the discussion 1 from different users. $record = new \stdClass(); $record->discussion = $discussion1->id; $record->parent = $discussion1->firstpost; $record->userid = $user2->id; $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $clock->bump(); $record->parent = $discussion1reply1->id; $record->userid = $user3->id; $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $clock->bump(); $record->userid = $user4->id; $discussion1reply3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $clock->bump(); // Create discussion2. $record2 = new \stdClass(); $record2->course = $course1->id; $record2->userid = $user1->id; $record2->forum = $forum1->id; $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record2); $clock->bump(); // Add one reply to the discussion 2. $record2 = new \stdClass(); $record2->discussion = $discussion2->id; $record2->parent = $discussion2->firstpost; $record2->userid = $user2->id; $discussion2reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record2); $clock->bump(); // Create discussion 3. $record3 = new \stdClass(); $record3->course = $course1->id; $record3->userid = $user1->id; $record3->forum = $forum1->id; $discussion3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record3); $clock->bump(); // Add two replies to the discussion 3. $record3 = new \stdClass(); $record3->discussion = $discussion3->id; $record3->parent = $discussion3->firstpost; $record3->userid = $user2->id; $discussion3reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record3); $clock->bump(); $record3->parent = $discussion3reply1->id; $record3->userid = $user3->id; $discussion3reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record3); // Call the external function passing forum id. $discussions = mod_forum_external::get_forum_discussions($forum1->id); $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions); // Discussions should be ordered by last post date in descending order by default. $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion3->id); $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion2->id); $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion1->id); $vaultfactory = \mod_forum\local\container::get_vault_factory(); $discussionlistvault = $vaultfactory->get_discussions_in_forum_vault(); // Call the external function passing forum id and sort order parameter. $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_LASTPOST_ASC); $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions); // Discussions should be ordered by last post date in ascending order. $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion1->id); $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion2->id); $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion3->id); // Call the external function passing forum id and sort order parameter. $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_CREATED_DESC); $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions); // Discussions should be ordered by discussion creation date in descending order. $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion3->id); $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion2->id); $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion1->id); // Call the external function passing forum id and sort order parameter. $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_CREATED_ASC); $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions); // Discussions should be ordered by discussion creation date in ascending order. $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion1->id); $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion2->id); $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion3->id); // Call the external function passing forum id and sort order parameter. $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_REPLIES_DESC); $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions); // Discussions should be ordered by the number of replies in descending order. $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion1->id); $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion3->id); $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion2->id); // Call the external function passing forum id and sort order parameter. $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_REPLIES_ASC); $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions); // Discussions should be ordered by the number of replies in ascending order. $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion2->id); $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion3->id); $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion1->id); // Pin discussion2. $DB->update_record('forum_discussions', (object) array('id' => $discussion2->id, 'pinned' => FORUM_DISCUSSION_PINNED)); // Call the external function passing forum id. $discussions = mod_forum_external::get_forum_discussions($forum1->id); $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions); // Discussions should be ordered by last post date in descending order by default. // Pinned discussions should be at the top of the list. $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion2->id); $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion3->id); $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion1->id); // Call the external function passing forum id and sort order parameter. $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_LASTPOST_ASC); $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions); // Discussions should be ordered by last post date in ascending order. // Pinned discussions should be at the top of the list. $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion2->id); $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion1->id); $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion3->id); } /** * Test add_discussion_post */ public function test_add_discussion_post(): void { global $CFG; $this->resetAfterTest(true); $user = self::getDataGenerator()->create_user(); $otheruser = self::getDataGenerator()->create_user(); self::setAdminUser(); // Create course to add the module. $course = self::getDataGenerator()->create_course(array('groupmode' => VISIBLEGROUPS, 'groupmodeforce' => 0)); // Forum with tracking off. $record = new \stdClass(); $record->course = $course->id; $forum = self::getDataGenerator()->create_module('forum', $record); $cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST); $forumcontext = \context_module::instance($forum->cmid); // Add discussions to the forums. $record = new \stdClass(); $record->course = $course->id; $record->userid = $user->id; $record->forum = $forum->id; $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // Try to post (user not enrolled). self::setUser($user); try { mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...'); $this->fail('Exception expected due to being unenrolled from the course.'); } catch (\moodle_exception $e) { $this->assertEquals('requireloginerror', $e->errorcode); } $this->getDataGenerator()->enrol_user($user->id, $course->id); $this->getDataGenerator()->enrol_user($otheruser->id, $course->id); $createdpost = mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...'); $createdpost = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $createdpost); $posts = mod_forum_external::get_discussion_posts($discussion->id, 'modified', 'ASC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); // We receive the discussion and the post. $this->assertEquals(2, count($posts['posts'])); $tested = false; foreach ($posts['posts'] as $thispost) { if ($createdpost['postid'] == $thispost['id']) { $this->assertEquals('some subject', $thispost['subject']); $this->assertEquals('some text here...', $thispost['message']); $this->assertEquals(FORMAT_HTML, $thispost['messageformat']); // This is the default if format was not specified. $tested = true; } } $this->assertTrue($tested); // Let's simulate a call with any other format, it should be stored that way. global $DB; // Yes, we are going to use DB facilities too, because cannot rely on other functions for checking // the format. They eat it completely (going back to FORMAT_HTML. So we only can trust DB for further // processing. $formats = [FORMAT_PLAIN, FORMAT_MOODLE, FORMAT_MARKDOWN, FORMAT_HTML]; $options = []; foreach ($formats as $format) { $createdpost = mod_forum_external::add_discussion_post($discussion->firstpost, 'with some format', 'some formatted here...', $options, $format); $createdpost = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $createdpost); $dbformat = $DB->get_field('forum_posts', 'messageformat', ['id' => $createdpost['postid']]); $this->assertEquals($format, $dbformat); } // Now let's try the 'topreferredformat' option. That should end with the content // transformed and the format being FORMAT_HTML (when, like in this case, user preferred // format is HTML, inferred from editor in preferences). $options = [['name' => 'topreferredformat', 'value' => true]]; $createdpost = mod_forum_external::add_discussion_post($discussion->firstpost, 'interesting subject', 'with some https://example.com link', $options, FORMAT_MOODLE); $createdpost = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $createdpost); $dbpost = $DB->get_record('forum_posts', ['id' => $createdpost['postid']]); // Format HTML and content converted, we should get. $this->assertEquals(FORMAT_HTML, $dbpost->messageformat); $this->assertEquals('
with some https://example.com link
', $dbpost->message); // Test inline and regular attachment in post // Create a file in a draft area for inline attachments. $draftidinlineattach = file_get_unused_draft_itemid(); $draftidattach = file_get_unused_draft_itemid(); self::setUser($user); $usercontext = \context_user::instance($user->id); $filepath = '/'; $filearea = 'draft'; $component = 'user'; $filenameimg = 'shouldbeanimage.txt'; $filerecordinline = array( 'contextid' => $usercontext->id, 'component' => $component, 'filearea' => $filearea, 'itemid' => $draftidinlineattach, 'filepath' => $filepath, 'filename' => $filenameimg, ); $fs = get_file_storage(); // Create a file in a draft area for regular attachments. $filerecordattach = $filerecordinline; $attachfilename = 'attachment.txt'; $filerecordattach['filename'] = $attachfilename; $filerecordattach['itemid'] = $draftidattach; $fs->create_file_from_string($filerecordinline, 'image contents (not really)'); $fs->create_file_from_string($filerecordattach, 'simple text attachment'); $options = array(array('name' => 'inlineattachmentsid', 'value' => $draftidinlineattach), array('name' => 'attachmentsid', 'value' => $draftidattach)); $dummytext = 'Here is an inline image: id}/user/draft/{$draftidinlineattach}/{$filenameimg}" . '" alt="inlineimage">.'; $createdpost = mod_forum_external::add_discussion_post($discussion->firstpost, 'new post inline attachment', $dummytext, $options); $createdpost = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $createdpost); $posts = mod_forum_external::get_discussion_posts($discussion->id, 'modified', 'ASC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); // We receive the discussion and the post. // Can't guarantee order of posts during tests. $postfound = false; foreach ($posts['posts'] as $thispost) { if ($createdpost['postid'] == $thispost['id']) { $this->assertEquals($createdpost['postid'], $thispost['id']); $this->assertCount(1, $thispost['attachments']); $this->assertEquals('attachment.txt', $thispost['attachments'][0]['filename']); $this->assertEquals($thispost['attachments'][0]['filename'], $attachfilename, "There should be 1 attachment"); $this->assertStringContainsString('pluginfile.php', $thispost['message']); $postfound = true; break; } } $this->assertTrue($postfound); // Check not posting in groups the user is not member of. $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); groups_add_member($group->id, $otheruser->id); $forum = self::getDataGenerator()->create_module('forum', $record, array('groupmode' => SEPARATEGROUPS)); $record->forum = $forum->id; $record->userid = $otheruser->id; $record->groupid = $group->id; $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); try { mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...'); $this->fail('Exception expected due to invalid permissions for posting.'); } catch (\moodle_exception $e) { $this->assertEquals('nopostforum', $e->errorcode); } } /** * Test add_discussion_post and auto subscription to a discussion. */ public function test_add_discussion_post_subscribe_discussion(): void { global $USER; $this->resetAfterTest(true); self::setAdminUser(); $user = self::getDataGenerator()->create_user(); $admin = get_admin(); // Create course to add the module. $course = self::getDataGenerator()->create_course(array('groupmode' => VISIBLEGROUPS, 'groupmodeforce' => 0)); $this->getDataGenerator()->enrol_user($user->id, $course->id); // Forum with tracking off. $record = new \stdClass(); $record->course = $course->id; $forum = self::getDataGenerator()->create_module('forum', $record); $cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST); // Add discussions to the forums. $record = new \stdClass(); $record->course = $course->id; $record->userid = $admin->id; $record->forum = $forum->id; $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // Try to post as user. self::setUser($user); // Enable auto subscribe discussion. $USER->autosubscribe = true; // Add a discussion post in a forum discussion where the user is not subscribed (auto-subscribe preference enabled). mod_forum_external::add_discussion_post($discussion1->firstpost, 'some subject', 'some text here...'); $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'ASC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); // We receive the discussion and the post. $this->assertEquals(2, count($posts['posts'])); // The user should be subscribed to the discussion after adding a discussion post. $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion1->id, $cm)); // Disable auto subscribe discussion. $USER->autosubscribe = false; $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion1->id, $cm)); // Add a discussion post in a forum discussion where the user is subscribed (auto-subscribe preference disabled). mod_forum_external::add_discussion_post($discussion1->firstpost, 'some subject 1', 'some text here 1...'); $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'ASC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); // We receive the discussion and the post. $this->assertEquals(3, count($posts['posts'])); // The user should still be subscribed to the discussion after adding a discussion post. $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion1->id, $cm)); $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm)); // Add a discussion post in a forum discussion where the user is not subscribed (auto-subscribe preference disabled). mod_forum_external::add_discussion_post($discussion2->firstpost, 'some subject 2', 'some text here 2...'); $posts = mod_forum_external::get_discussion_posts($discussion2->id, 'modified', 'ASC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); // We receive the discussion and the post. $this->assertEquals(2, count($posts['posts'])); // The user should still not be subscribed to the discussion after adding a discussion post. $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm)); // Passing a value for the discussionsubscribe option parameter. $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm)); // Add a discussion post in a forum discussion where the user is not subscribed (auto-subscribe preference disabled), // and the option parameter 'discussionsubscribe' => true in the webservice. $option = array('name' => 'discussionsubscribe', 'value' => true); $options[] = $option; mod_forum_external::add_discussion_post($discussion2->firstpost, 'some subject 2', 'some text here 2...', $options); $posts = mod_forum_external::get_discussion_posts($discussion2->id, 'modified', 'ASC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); // We receive the discussion and the post. $this->assertEquals(3, count($posts['posts'])); // The user should now be subscribed to the discussion after adding a discussion post. $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm)); } /* * Test add_discussion. A basic test since all the API functions are already covered by unit tests. */ public function test_add_discussion(): void { global $CFG, $USER; $this->resetAfterTest(true); // Create courses to add the modules. $course = self::getDataGenerator()->create_course(); $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); // First forum with tracking off. $record = new \stdClass(); $record->course = $course->id; $record->type = 'news'; $forum = self::getDataGenerator()->create_module('forum', $record); self::setUser($user1); $this->getDataGenerator()->enrol_user($user1->id, $course->id); try { mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...'); $this->fail('Exception expected due to invalid permissions.'); } catch (\moodle_exception $e) { $this->assertEquals('cannotcreatediscussion', $e->errorcode); } self::setAdminUser(); $createddiscussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...'); $createddiscussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $createddiscussion); $discussions = mod_forum_external::get_forum_discussions($forum->id); $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions); $this->assertCount(1, $discussions['discussions']); $this->assertCount(0, $discussions['warnings']); $this->assertEquals($createddiscussion['discussionid'], $discussions['discussions'][0]['discussion']); $this->assertEquals(-1, $discussions['discussions'][0]['groupid']); $this->assertEquals('the subject', $discussions['discussions'][0]['subject']); $this->assertEquals('some text here...', $discussions['discussions'][0]['message']); $discussion2pinned = mod_forum_external::add_discussion($forum->id, 'the pinned subject', 'some 2 text here...', -1, array('options' => array('name' => 'discussionpinned', 'value' => true))); $discussion3 = mod_forum_external::add_discussion($forum->id, 'the non pinnedsubject', 'some 3 text here...'); $discussions = mod_forum_external::get_forum_discussions($forum->id); $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions); $this->assertCount(3, $discussions['discussions']); $this->assertEquals($discussion2pinned['discussionid'], $discussions['discussions'][0]['discussion']); // Test inline and regular attachment in new discussion // Create a file in a draft area for inline attachments. $fs = get_file_storage(); $draftidinlineattach = file_get_unused_draft_itemid(); $draftidattach = file_get_unused_draft_itemid(); $usercontext = \context_user::instance($USER->id); $filepath = '/'; $filearea = 'draft'; $component = 'user'; $filenameimg = 'shouldbeanimage.txt'; $filerecord = array( 'contextid' => $usercontext->id, 'component' => $component, 'filearea' => $filearea, 'itemid' => $draftidinlineattach, 'filepath' => $filepath, 'filename' => $filenameimg, ); // Create a file in a draft area for regular attachments. $filerecordattach = $filerecord; $attachfilename = 'attachment.txt'; $filerecordattach['filename'] = $attachfilename; $filerecordattach['itemid'] = $draftidattach; $fs->create_file_from_string($filerecord, 'image contents (not really)'); $fs->create_file_from_string($filerecordattach, 'simple text attachment'); $dummytext = 'Here is an inline image: id}/user/draft/{$draftidinlineattach}/{$filenameimg}" . '" alt="inlineimage">.'; $options = array(array('name' => 'inlineattachmentsid', 'value' => $draftidinlineattach), array('name' => 'attachmentsid', 'value' => $draftidattach)); $createddiscussion = mod_forum_external::add_discussion($forum->id, 'the inline attachment subject', $dummytext, -1, $options); $createddiscussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $createddiscussion); $discussions = mod_forum_external::get_forum_discussions($forum->id); $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions); $this->assertCount(4, $discussions['discussions']); $this->assertCount(0, $createddiscussion['warnings']); // Can't guarantee order of posts during tests. $postfound = false; foreach ($discussions['discussions'] as $thisdiscussion) { if ($createddiscussion['discussionid'] == $thisdiscussion['discussion']) { $this->assertEquals($thisdiscussion['attachment'], 1, "There should be a non-inline attachment"); $this->assertCount(1, $thisdiscussion['attachments'], "There should be 1 attachment"); $this->assertEquals($thisdiscussion['attachments'][0]['filename'], $attachfilename, "There should be 1 attachment"); $this->assertStringNotContainsString('draftfile.php', $thisdiscussion['message']); $this->assertStringContainsString('pluginfile.php', $thisdiscussion['message']); $postfound = true; break; } } $this->assertTrue($postfound); } /** * Test adding discussions in a course with gorups */ public function test_add_discussion_in_course_with_groups(): void { global $CFG; $this->resetAfterTest(true); // Create course to add the module. $course = self::getDataGenerator()->create_course(array('groupmode' => VISIBLEGROUPS, 'groupmodeforce' => 0)); $user = self::getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); // Forum forcing separate gropus. $record = new \stdClass(); $record->course = $course->id; $forum = self::getDataGenerator()->create_module('forum', $record, array('groupmode' => SEPARATEGROUPS)); // Try to post (user not enrolled). self::setUser($user); // The user is not enroled in any group, try to post in a forum with separate groups. try { mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...'); $this->fail('Exception expected due to invalid group permissions.'); } catch (\moodle_exception $e) { $this->assertEquals('cannotcreatediscussion', $e->errorcode); } try { mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', 0); $this->fail('Exception expected due to invalid group permissions.'); } catch (\moodle_exception $e) { $this->assertEquals('cannotcreatediscussion', $e->errorcode); } // Create a group. $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); // Try to post in a group the user is not enrolled. try { mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', $group->id); $this->fail('Exception expected due to invalid group permissions.'); } catch (\moodle_exception $e) { $this->assertEquals('cannotcreatediscussion', $e->errorcode); } // Add the user to a group. groups_add_member($group->id, $user->id); // Try to post in a group the user is not enrolled. try { mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', $group->id + 1); $this->fail('Exception expected due to invalid group.'); } catch (\moodle_exception $e) { $this->assertEquals('cannotcreatediscussion', $e->errorcode); } // Nost add the discussion using a valid group. $discussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', $group->id); $discussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $discussion); $discussions = mod_forum_external::get_forum_discussions($forum->id); $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions); $this->assertCount(1, $discussions['discussions']); $this->assertCount(0, $discussions['warnings']); $this->assertEquals($discussion['discussionid'], $discussions['discussions'][0]['discussion']); $this->assertEquals($group->id, $discussions['discussions'][0]['groupid']); // Now add a discussions without indicating a group. The function should guess the correct group. $discussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...'); $discussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $discussion); $discussions = mod_forum_external::get_forum_discussions($forum->id); $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions); $this->assertCount(2, $discussions['discussions']); $this->assertCount(0, $discussions['warnings']); $this->assertEquals($group->id, $discussions['discussions'][0]['groupid']); $this->assertEquals($group->id, $discussions['discussions'][1]['groupid']); // Enrol the same user in other group. $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); groups_add_member($group2->id, $user->id); // Now add a discussions without indicating a group. The function should guess the correct group (the first one). $discussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...'); $discussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $discussion); $discussions = mod_forum_external::get_forum_discussions($forum->id); $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions); $this->assertCount(3, $discussions['discussions']); $this->assertCount(0, $discussions['warnings']); $this->assertEquals($group->id, $discussions['discussions'][0]['groupid']); $this->assertEquals($group->id, $discussions['discussions'][1]['groupid']); $this->assertEquals($group->id, $discussions['discussions'][2]['groupid']); } /* * Test set_lock_state. * * @covers \mod_forum\event\discussion_lock_updated */ public function test_set_lock_state(): void { global $DB; $this->resetAfterTest(true); // Create courses to add the modules. $course = self::getDataGenerator()->create_course(); $user = self::getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); // First forum with tracking off. $record = new \stdClass(); $record->course = $course->id; $record->type = 'news'; $forum = self::getDataGenerator()->create_module('forum', $record); $context = \context_module::instance($forum->cmid); $record = new \stdClass(); $record->course = $course->id; $record->userid = $user->id; $record->forum = $forum->id; $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // User who is a student. self::setUser($user); $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id, 'manual'); // Only a teacher should be able to lock a discussion. try { $result = mod_forum_external::set_lock_state($forum->id, $discussion->id, 0); $this->fail('Exception expected due to missing capability.'); } catch (\moodle_exception $e) { $this->assertEquals('errorcannotlock', $e->errorcode); } // Set the lock. self::setAdminUser(); $sink = $this->redirectEvents(); // Capturing the event. $result = mod_forum_external::set_lock_state($forum->id, $discussion->id, 0); $result = external_api::clean_returnvalue(mod_forum_external::set_lock_state_returns(), $result); $this->assertTrue($result['locked']); $this->assertNotEquals(0, $result['times']['locked']); // Check that the event contains the expected values. $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\mod_forum\event\discussion_lock_updated', $event); $this->assertEquals($context, $event->get_context()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); $this->assertStringContainsString(' locked the discussion:', $event->get_description()); // Unset the lock. $sink = $this->redirectEvents(); // Capturing the event. $result = mod_forum_external::set_lock_state($forum->id, $discussion->id, time()); $result = external_api::clean_returnvalue(mod_forum_external::set_lock_state_returns(), $result); $this->assertFalse($result['locked']); $this->assertEquals('0', $result['times']['locked']); // Check that the event contains the expected values. $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\mod_forum\event\discussion_lock_updated', $event); $this->assertEquals($context, $event->get_context()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); $this->assertStringContainsString(' unlocked the discussion:', $event->get_description()); } /* * Test can_add_discussion. A basic test since all the API functions are already covered by unit tests. */ public function test_can_add_discussion(): void { global $DB; $this->resetAfterTest(true); // Create courses to add the modules. $course = self::getDataGenerator()->create_course(); $user = self::getDataGenerator()->create_user(); // First forum with tracking off. $record = new \stdClass(); $record->course = $course->id; $record->type = 'news'; $forum = self::getDataGenerator()->create_module('forum', $record); // User with no permissions to add in a news forum. self::setUser($user); $this->getDataGenerator()->enrol_user($user->id, $course->id); $result = mod_forum_external::can_add_discussion($forum->id); $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result); $this->assertFalse($result['status']); $this->assertFalse($result['canpindiscussions']); $this->assertTrue($result['cancreateattachment']); // Disable attachments. $DB->set_field('forum', 'maxattachments', 0, array('id' => $forum->id)); $result = mod_forum_external::can_add_discussion($forum->id); $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result); $this->assertFalse($result['status']); $this->assertFalse($result['canpindiscussions']); $this->assertFalse($result['cancreateattachment']); $DB->set_field('forum', 'maxattachments', 1, array('id' => $forum->id)); // Enable attachments again. self::setAdminUser(); $result = mod_forum_external::can_add_discussion($forum->id); $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result); $this->assertTrue($result['status']); $this->assertTrue($result['canpindiscussions']); $this->assertTrue($result['cancreateattachment']); } /* * A basic test to make sure users cannot post to forum after the cutoff date. */ public function test_can_add_discussion_after_cutoff(): void { $this->resetAfterTest(true); // Create courses to add the modules. $course = self::getDataGenerator()->create_course(); $user = self::getDataGenerator()->create_user(); // Create a forum with cutoff date set to a past date. $forum = self::getDataGenerator()->create_module('forum', ['course' => $course->id, 'cutoffdate' => time() - 1]); // User with no mod/forum:canoverridecutoff capability. self::setUser($user); $this->getDataGenerator()->enrol_user($user->id, $course->id); $result = mod_forum_external::can_add_discussion($forum->id); $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result); $this->assertFalse($result['status']); self::setAdminUser(); $result = mod_forum_external::can_add_discussion($forum->id); $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result); $this->assertTrue($result['status']); } /** * Test get posts discussions including rating information. */ public function test_mod_forum_get_discussion_rating_information(): void { global $DB, $CFG, $PAGE; require_once($CFG->dirroot . '/rating/lib.php'); $PAGE->set_url('/my/index.php'); // Need this because some internal API calls require the $PAGE url to be set. $this->resetAfterTest(true); $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); $user3 = self::getDataGenerator()->create_user(); $teacher = self::getDataGenerator()->create_user(); // Create course to add the module. $course = self::getDataGenerator()->create_course(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); $this->getDataGenerator()->enrol_user($user1->id, $course->id, $studentrole->id, 'manual'); $this->getDataGenerator()->enrol_user($user2->id, $course->id, $studentrole->id, 'manual'); $this->getDataGenerator()->enrol_user($user3->id, $course->id, $studentrole->id, 'manual'); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id, 'manual'); // Create the forum. $record = new \stdClass(); $record->course = $course->id; // Set Aggregate type = Average of ratings. $record->assessed = RATING_AGGREGATE_AVERAGE; $record->scale = 100; $forum = self::getDataGenerator()->create_module('forum', $record); $context = \context_module::instance($forum->cmid); // Add discussion to the forum. $record = new \stdClass(); $record->course = $course->id; $record->userid = $user1->id; $record->forum = $forum->id; $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // Retrieve the first post. $post = $DB->get_record('forum_posts', array('discussion' => $discussion->id)); // Rate the discussion as user2. $rating1 = new \stdClass(); $rating1->contextid = $context->id; $rating1->component = 'mod_forum'; $rating1->ratingarea = 'post'; $rating1->itemid = $post->id; $rating1->rating = 50; $rating1->scaleid = 100; $rating1->userid = $user2->id; $rating1->timecreated = time(); $rating1->timemodified = time(); $rating1->id = $DB->insert_record('rating', $rating1); // Rate the discussion as user3. $rating2 = new \stdClass(); $rating2->contextid = $context->id; $rating2->component = 'mod_forum'; $rating2->ratingarea = 'post'; $rating2->itemid = $post->id; $rating2->rating = 100; $rating2->scaleid = 100; $rating2->userid = $user3->id; $rating2->timecreated = time() + 1; $rating2->timemodified = time() + 1; $rating2->id = $DB->insert_record('rating', $rating2); // Retrieve the rating for the post as student. $this->setUser($user1); $posts = mod_forum_external::get_discussion_posts($discussion->id, 'id', 'DESC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); $this->assertCount(1, $posts['ratinginfo']['ratings']); $this->assertTrue($posts['ratinginfo']['ratings'][0]['canviewaggregate']); $this->assertFalse($posts['ratinginfo']['canviewall']); $this->assertFalse($posts['ratinginfo']['ratings'][0]['canrate']); $this->assertEquals(2, $posts['ratinginfo']['ratings'][0]['count']); $this->assertEquals(($rating1->rating + $rating2->rating) / 2, $posts['ratinginfo']['ratings'][0]['aggregate']); // Retrieve the rating for the post as teacher. $this->setUser($teacher); $posts = mod_forum_external::get_discussion_posts($discussion->id, 'id', 'DESC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); $this->assertCount(1, $posts['ratinginfo']['ratings']); $this->assertTrue($posts['ratinginfo']['ratings'][0]['canviewaggregate']); $this->assertTrue($posts['ratinginfo']['canviewall']); $this->assertTrue($posts['ratinginfo']['ratings'][0]['canrate']); $this->assertEquals(2, $posts['ratinginfo']['ratings'][0]['count']); $this->assertEquals(($rating1->rating + $rating2->rating) / 2, $posts['ratinginfo']['ratings'][0]['aggregate']); } /** * Test mod_forum_get_forum_access_information. */ public function test_mod_forum_get_forum_access_information(): void { global $DB; $this->resetAfterTest(true); $student = self::getDataGenerator()->create_user(); $course = self::getDataGenerator()->create_course(); // Create the forum. $record = new \stdClass(); $record->course = $course->id; $forum = self::getDataGenerator()->create_module('forum', $record); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id, 'manual'); self::setUser($student); $result = mod_forum_external::get_forum_access_information($forum->id); $result = external_api::clean_returnvalue(mod_forum_external::get_forum_access_information_returns(), $result); // Check default values for capabilities. $enabledcaps = array('canviewdiscussion', 'canstartdiscussion', 'canreplypost', 'canviewrating', 'cancreateattachment', 'canexportownpost', 'cancantogglefavourite', 'cancanmailnow', 'candeleteownpost', 'canallowforcesubscribe'); unset($result['warnings']); foreach ($result as $capname => $capvalue) { if (in_array($capname, $enabledcaps)) { $this->assertTrue($capvalue); } else { $this->assertFalse($capvalue); } } // Now, unassign some capabilities. unassign_capability('mod/forum:deleteownpost', $studentrole->id); unassign_capability('mod/forum:allowforcesubscribe', $studentrole->id); array_pop($enabledcaps); array_pop($enabledcaps); accesslib_clear_all_caches_for_unit_testing(); $result = mod_forum_external::get_forum_access_information($forum->id); $result = external_api::clean_returnvalue(mod_forum_external::get_forum_access_information_returns(), $result); unset($result['warnings']); foreach ($result as $capname => $capvalue) { if (in_array($capname, $enabledcaps)) { $this->assertTrue($capvalue); } else { $this->assertFalse($capvalue); } } } /** * Test add_discussion_post */ public function test_add_discussion_post_private(): void { global $DB; $this->resetAfterTest(true); self::setAdminUser(); // Create course to add the module. $course = self::getDataGenerator()->create_course(); // Standard forum. $record = new \stdClass(); $record->course = $course->id; $forum = self::getDataGenerator()->create_module('forum', $record); $cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST); $forumcontext = \context_module::instance($forum->cmid); $generator = self::getDataGenerator()->get_plugin_generator('mod_forum'); // Create an enrol users. $student1 = self::getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($student1->id, $course->id, 'student'); $student2 = self::getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($student2->id, $course->id, 'student'); $teacher1 = self::getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($teacher1->id, $course->id, 'editingteacher'); $teacher2 = self::getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($teacher2->id, $course->id, 'editingteacher'); // Add a new discussion to the forum. self::setUser($student1); $record = new \stdClass(); $record->course = $course->id; $record->userid = $student1->id; $record->forum = $forum->id; $discussion = $generator->create_discussion($record); // Have the teacher reply privately. self::setUser($teacher1); $post = mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...', [ [ 'name' => 'private', 'value' => true, ], ]); $post = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $post); $privatereply = $DB->get_record('forum_posts', array('id' => $post['postid'])); $this->assertEquals($student1->id, $privatereply->privatereplyto); // Bump the time of the private reply to ensure order. $privatereply->created++; $privatereply->modified = $privatereply->created; $DB->update_record('forum_posts', $privatereply); // The teacher will receive their private reply. self::setUser($teacher1); $posts = mod_forum_external::get_discussion_posts($discussion->id, 'id', 'DESC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); $this->assertEquals(2, count($posts['posts'])); $this->assertTrue($posts['posts'][0]['isprivatereply']); // Another teacher on the course will also receive the private reply. self::setUser($teacher2); $posts = mod_forum_external::get_discussion_posts($discussion->id, 'id', 'DESC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); $this->assertEquals(2, count($posts['posts'])); $this->assertTrue($posts['posts'][0]['isprivatereply']); // The student will receive the private reply. self::setUser($student1); $posts = mod_forum_external::get_discussion_posts($discussion->id, 'id', 'DESC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); $this->assertEquals(2, count($posts['posts'])); $this->assertTrue($posts['posts'][0]['isprivatereply']); // Another student will not receive the private reply. self::setUser($student2); $posts = mod_forum_external::get_discussion_posts($discussion->id, 'id', 'ASC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); $this->assertEquals(1, count($posts['posts'])); $this->assertFalse($posts['posts'][0]['isprivatereply']); // A user cannot reply to a private reply. self::setUser($teacher2); $this->expectException('coding_exception'); $post = mod_forum_external::add_discussion_post($privatereply->id, 'some subject', 'some text here...', [ 'options' => [ 'name' => 'private', 'value' => false, ], ]); } /** * Test trusted text enabled. */ public function test_trusted_text_enabled(): void { global $USER, $CFG; $this->resetAfterTest(true); $CFG->enabletrusttext = 1; $dangeroustext = ''; $cleantext = 'Untrusted text'; // Create courses to add the modules. $course = self::getDataGenerator()->create_course(); $user1 = self::getDataGenerator()->create_user(); // First forum with tracking off. $record = new \stdClass(); $record->course = $course->id; $record->type = 'qanda'; $forum = self::getDataGenerator()->create_module('forum', $record); $context = \context_module::instance($forum->cmid); // Add discussions to the forums. $discussionrecord = new \stdClass(); $discussionrecord->course = $course->id; $discussionrecord->userid = $user1->id; $discussionrecord->forum = $forum->id; $discussionrecord->message = $dangeroustext; $discussionrecord->messagetrust = trusttext_trusted($context); $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($discussionrecord); self::setAdminUser(); $discussionrecord->userid = $USER->id; $discussionrecord->messagetrust = trusttext_trusted($context); $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($discussionrecord); $discussions = mod_forum_external::get_forum_discussions($forum->id); $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions); $this->assertCount(2, $discussions['discussions']); $this->assertCount(0, $discussions['warnings']); // Admin message is fully trusted. $this->assertEquals(1, $discussions['discussions'][0]['messagetrust']); $this->assertEquals($dangeroustext, $discussions['discussions'][0]['message']); // Student message is not trusted. $this->assertEquals(0, $discussions['discussions'][1]['messagetrust']); $this->assertEquals($cleantext, $discussions['discussions'][1]['message']); // Get posts now. $posts = mod_forum_external::get_discussion_posts($discussion2->id, 'modified', 'DESC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); // Admin message is fully trusted. $this->assertEquals($dangeroustext, $posts['posts'][0]['message']); $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'ASC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); // Student message is not trusted. $this->assertEquals($cleantext, $posts['posts'][0]['message']); } /** * Test trusted text disabled. */ public function test_trusted_text_disabled(): void { global $USER, $CFG; $this->resetAfterTest(true); $CFG->enabletrusttext = 0; $dangeroustext = ''; $cleantext = 'Untrusted text'; // Create courses to add the modules. $course = self::getDataGenerator()->create_course(); $user1 = self::getDataGenerator()->create_user(); // First forum with tracking off. $record = new \stdClass(); $record->course = $course->id; $record->type = 'qanda'; $forum = self::getDataGenerator()->create_module('forum', $record); $context = \context_module::instance($forum->cmid); // Add discussions to the forums. $discussionrecord = new \stdClass(); $discussionrecord->course = $course->id; $discussionrecord->userid = $user1->id; $discussionrecord->forum = $forum->id; $discussionrecord->message = $dangeroustext; $discussionrecord->messagetrust = trusttext_trusted($context); $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($discussionrecord); self::setAdminUser(); $discussionrecord->userid = $USER->id; $discussionrecord->messagetrust = trusttext_trusted($context); $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($discussionrecord); $discussions = mod_forum_external::get_forum_discussions($forum->id); $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions); $this->assertCount(2, $discussions['discussions']); $this->assertCount(0, $discussions['warnings']); // Admin message is not trusted because enabletrusttext is disabled. $this->assertEquals(0, $discussions['discussions'][0]['messagetrust']); $this->assertEquals($cleantext, $discussions['discussions'][0]['message']); // Student message is not trusted. $this->assertEquals(0, $discussions['discussions'][1]['messagetrust']); $this->assertEquals($cleantext, $discussions['discussions'][1]['message']); // Get posts now. $posts = mod_forum_external::get_discussion_posts($discussion2->id, 'modified', 'ASC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); // Admin message is not trusted because enabletrusttext is disabled. $this->assertEquals($cleantext, $posts['posts'][0]['message']); $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'ASC'); $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts); // Student message is not trusted. $this->assertEquals($cleantext, $posts['posts'][0]['message']); } /** * Test delete a discussion. */ public function test_delete_post_discussion(): void { 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::delete_post($discussion->firstpost); $result = external_api::clean_returnvalue(mod_forum_external::delete_post_returns(), $result); $this->assertTrue($result['status']); $this->assertEquals(0, $DB->count_records('forum_posts', array('id' => $discussion->firstpost))); $this->assertEquals(0, $DB->count_records('forum_discussions', array('id' => $discussion->id))); } /** * Test delete a post. */ public function test_delete_post_post(): void { 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::delete_post($post->id); $result = external_api::clean_returnvalue(mod_forum_external::delete_post_returns(), $result); $this->assertTrue($result['status']); $this->assertEquals(1, $DB->count_records('forum_posts', array('discussion' => $discussion->id))); $this->assertEquals(1, $DB->count_records('forum_discussions', array('id' => $discussion->id))); } /** * Test delete a different user post. */ public function test_delete_post_other_user_post(): void { 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); $this->setUser($otheruser); $this->expectExceptionMessage(get_string('cannotdeletepost', 'forum')); mod_forum_external::delete_post($post->id); } /* * Test get forum posts by user id. */ public function test_mod_forum_get_discussion_posts_by_userid(): void { global $DB; $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(); // Create course to add the module. $course1 = self::getDataGenerator()->create_course(); $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, $course1->id)->out(false), '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, $course1->id)->out(false), '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); // 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. $time = time(); $record = new \stdClass(); $record->course = $course1->id; $record->userid = $user1->id; $record->forum = $forum1->id; $record->timemodified = $time + 100; $discussion1 = $forumgenerator->create_discussion($record); $discussion1firstpost = $postvault->get_first_post_for_discussion_ids([$discussion1->id]); $discussion1firstpost = $discussion1firstpost[$discussion1->firstpost]; $discussion1firstpostobject = $legacypostmapper->to_legacy_object($discussion1firstpost); $record = new \stdClass(); $record->course = $course1->id; $record->userid = $user1->id; $record->forum = $forum1->id; $record->timemodified = $time + 200; $discussion2 = $forumgenerator->create_discussion($record); $discussion2firstpost = $postvault->get_first_post_for_discussion_ids([$discussion2->id]); $discussion2firstpost = $discussion2firstpost[$discussion2->firstpost]; $discussion2firstpostobject = $legacypostmapper->to_legacy_object($discussion2firstpost); // 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(); $file1 = $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(); $file2 = $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, 'teacher'); $this->getDataGenerator()->enrol_user($user2->id, $course1->id); // Changed display period for the discussions in past. $discussion = new \stdClass(); $discussion->id = $discussion1->id; $discussion->timestart = $time - 200; $discussion->timeend = $time - 100; $DB->update_record('forum_discussions', $discussion); $discussion = new \stdClass(); $discussion->id = $discussion2->id; $discussion->timestart = $time - 200; $discussion->timeend = $time - 100; $DB->update_record('forum_discussions', $discussion); // 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, 'timecreated' => $discussion1firstpost->get_time_created(), 'authorfullname' => $user1entity->get_full_name(), 'posts' => [ 'userposts' => [ [ 'id' => $discussion1reply1->id, 'discussionid' => $discussion1reply1->discussion, 'parentid' => $discussion1reply1->parent, 'hasparent' => true, 'timecreated' => $discussion1reply1->created, 'timemodified' => $discussion1reply1->modified, 'subject' => $discussion1reply1->subject, 'replysubject' => $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 \core_external\util::format_text() function. 'unread' => null, 'isdeleted' => false, 'isprivatereply' => false, 'haswordcount' => false, 'wordcount' => null, 'author' => $exporteduser2, 'attachments' => [], 'messageinlinefiles' => [], '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' => true, 'reply' => true, 'export' => false, 'controlreadstatus' => false, 'canreplyprivately' => true, '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' => (new \moodle_url('/mod/forum/post.php', [ 'prune' => $discussion1reply1->id ]))->out(false), '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, 'timemodified' => $discussion1firstpostobject->modified, 'subject' => $discussion1firstpostobject->subject, 'replysubject' => $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 \core_external\util::format_text() function. 'unread' => null, 'isdeleted' => false, 'isprivatereply' => false, 'haswordcount' => false, 'wordcount' => null, 'author' => $exporteduser1, 'attachments' => [], 'messageinlinefiles' => [], 'tags' => [], 'html' => [ 'rating' => null, 'taglist' => null, 'authorsubheading' => $forumgenerator->get_author_subheading_html( (object)$exporteduser1, $discussion1firstpostobject->created) ], 'charcount' => null, 'capabilities' => [ 'view' => true, 'edit' => true, 'delete' => true, 'split' => false, 'reply' => true, 'export' => false, 'controlreadstatus' => false, 'canreplyprivately' => true, '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' => (new \moodle_url('/mod/forum/post.php', [ 'edit' => $discussion1firstpostobject->id ]))->out(false), 'delete' => (new \moodle_url('/mod/forum/post.php', [ 'delete' => $discussion1firstpostobject->id ]))->out(false), '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, 'timecreated' => $discussion2firstpost->get_time_created(), 'authorfullname' => $user1entity->get_full_name(), 'posts' => [ 'userposts' => [ [ 'id' => $discussion2reply1->id, 'discussionid' => $discussion2reply1->discussion, 'parentid' => $discussion2reply1->parent, 'hasparent' => true, 'timecreated' => $discussion2reply1->created, 'timemodified' => $discussion2reply1->modified, 'subject' => $discussion2reply1->subject, 'replysubject' => $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 \core_external\util::format_text() function. 'unread' => null, 'isdeleted' => false, 'isprivatereply' => false, 'haswordcount' => false, 'wordcount' => null, 'author' => $exporteduser2, 'attachments' => [], 'messageinlinefiles' => [], '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' => true, 'reply' => true, 'export' => false, 'controlreadstatus' => false, 'canreplyprivately' => true, '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' => (new \moodle_url('/mod/forum/post.php', [ 'prune' => $discussion2reply1->id ]))->out(false), '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, 'timemodified' => $discussion2firstpostobject->modified, 'subject' => $discussion2firstpostobject->subject, 'replysubject' => $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 \core_external\util::format_text() function. 'unread' => null, 'isdeleted' => false, 'isprivatereply' => false, 'haswordcount' => false, 'wordcount' => null, 'author' => $exporteduser1, 'attachments' => [], 'messageinlinefiles' => [], 'tags' => [], 'html' => [ 'rating' => null, 'taglist' => null, 'authorsubheading' => $forumgenerator->get_author_subheading_html( (object)$exporteduser1, $discussion2firstpostobject->created) ], 'charcount' => null, 'capabilities' => [ 'view' => true, 'edit' => true, 'delete' => true, 'split' => false, 'reply' => true, 'export' => false, 'controlreadstatus' => false, 'canreplyprivately' => true, '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' => (new \moodle_url('/mod/forum/post.php', [ 'edit' => $discussion2firstpostobject->id ]))->out(false), 'delete' => (new \moodle_url('/mod/forum/post.php', [ 'delete' => $discussion2firstpostobject->id ]))->out(false), '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); // When groupmode is SEPARATEGROUPS, even there is no groupid specified, the post not for the user shouldn't be seen. $group1 = self::getDataGenerator()->create_group(['courseid' => $course1->id]); $group2 = self::getDataGenerator()->create_group(['courseid' => $course1->id]); // Update discussion with group. $discussion = new \stdClass(); $discussion->id = $discussion1->id; $discussion->groupid = $group1->id; $DB->update_record('forum_discussions', $discussion); $discussion = new \stdClass(); $discussion->id = $discussion2->id; $discussion->groupid = $group2->id; $DB->update_record('forum_discussions', $discussion); $cm = get_coursemodule_from_id('forum', $forum1->cmid); $cm->groupmode = SEPARATEGROUPS; $DB->update_record('course_modules', $cm); $teacher = self::getDataGenerator()->create_user(); $role = $DB->get_record('role', array('shortname' => 'teacher'), '*', MUST_EXIST); self::getDataGenerator()->enrol_user($teacher->id, $course1->id, $role->id); groups_add_member($group2->id, $teacher->id); self::setUser($teacher); $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); // Discussion is only 1 record (group 2). $this->assertEquals(1, count($discussions['discussions'])); $this->assertEquals($expectedposts['discussions'][1], $discussions['discussions'][0]); } /** * Test get_discussion_post a discussion. */ public function test_get_discussion_post_discussion(): void { 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(): void { 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(): void { 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(): void { 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(): void { 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(): void { 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)); // Test inline and regular attachment in post // Create a file in a draft area for inline attachments. $draftidinlineattach = file_get_unused_draft_itemid(); $draftidattach = file_get_unused_draft_itemid(); self::setUser($user); $usercontext = \context_user::instance($user->id); $filepath = '/'; $filearea = 'draft'; $component = 'user'; $filenameimg = 'fakeimage.png'; $filerecordinline = array( 'contextid' => $usercontext->id, 'component' => $component, 'filearea' => $filearea, 'itemid' => $draftidinlineattach, 'filepath' => $filepath, 'filename' => $filenameimg, ); $fs = get_file_storage(); // Create a file in a draft area for regular attachments. $filerecordattach = $filerecordinline; $attachfilename = 'faketxt.txt'; $filerecordattach['filename'] = $attachfilename; $filerecordattach['itemid'] = $draftidattach; $fs->create_file_from_string($filerecordinline, 'image contents (not really)'); $fs->create_file_from_string($filerecordattach, 'simple text attachment'); // 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_discussion_posts($discussion->id, 'modified', 'DESC', true); $result = external_api::clean_returnvalue(mod_forum_external::get_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(): void { 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); } /** * Test that we can update the subject of a post to the string '0' */ public function test_update_discussion_post_set_subject_to_zero(): void { global $DB, $USER; $this->resetAfterTest(true); $this->setAdminUser(); // Setup test data. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion((object) [ 'userid' => $USER->id, 'course' => $course->id, 'forum' => $forum->id, 'name' => 'Test discussion subject', ]); // Update discussion post subject. $result = external_api::clean_returnvalue( mod_forum_external::update_discussion_post_returns(), mod_forum_external::update_discussion_post($discussion->firstpost, '0') ); $this->assertTrue($result['status']); // Get updated discussion post subject from DB. $postsubject = $DB->get_field('forum_posts', 'subject', ['id' => $discussion->firstpost]); $this->assertEquals('0', $postsubject); } /** * Test that we can update the message of a post to the string '0' */ public function test_update_discussion_post_set_message_to_zero(): void { global $DB, $USER; $this->resetAfterTest(true); $this->setAdminUser(); // Setup test data. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion((object) [ 'userid' => $USER->id, 'course' => $course->id, 'forum' => $forum->id, 'message' => 'Test discussion message', 'messageformat' => FORMAT_HTML, ]); // Update discussion post message. $result = external_api::clean_returnvalue( mod_forum_external::update_discussion_post_returns(), mod_forum_external::update_discussion_post($discussion->firstpost, '', '0', FORMAT_HTML) ); $this->assertTrue($result['status']); // Get updated discussion post subject from DB. $postmessage = $DB->get_field('forum_posts', 'message', ['id' => $discussion->firstpost]); $this->assertEquals('0', $postmessage); } /** * Test that we can update the message format of a post to {@see FORMAT_MOODLE} */ public function test_update_discussion_post_set_message_format_moodle(): void { global $DB, $USER; $this->resetAfterTest(true); $this->setAdminUser(); // Setup test data. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion((object) [ 'userid' => $USER->id, 'course' => $course->id, 'forum' => $forum->id, 'message' => 'Test discussion message', 'messageformat' => FORMAT_HTML, ]); // Update discussion post message & messageformat. $result = external_api::clean_returnvalue( mod_forum_external::update_discussion_post_returns(), mod_forum_external::update_discussion_post($discussion->firstpost, '', 'Update discussion message', FORMAT_MOODLE) ); $this->assertTrue($result['status']); // Get updated discussion post from DB. $updatedpost = $DB->get_record('forum_posts', ['id' => $discussion->firstpost], 'message,messageformat'); $this->assertEquals((object) [ 'message' => 'Update discussion message', 'messageformat' => FORMAT_MOODLE, ], $updatedpost); } } PK-B\qj}''tests/private_replies_test.phpnu[. namespace mod_forum; use mod_forum_tests_generator_trait; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/forum/lib.php'); require_once($CFG->dirroot . '/mod/forum/locallib.php'); require_once(__DIR__ . '/generator_trait.php'); /** * Tests for private reply functionality. * * @package mod_forum * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class private_replies_test extends \advanced_testcase { use mod_forum_tests_generator_trait; /** * Setup before tests. */ public function setUp(): void { parent::setUp(); // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); } /** * Tear down after tests. */ public function tearDown(): void { // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); parent::tearDown(); } /** * Ensure that the forum_post_is_visible_privately function reports that a post is visible to a user when another * user wrote the post, and it is not private. */ public function test_forum_post_is_visible_privately_not_private(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, ]); [$student] = $this->helper_create_users($course, 1, 'student'); [$teacher] = $this->helper_create_users($course, 1, 'teacher'); [$discussion] = $this->helper_post_to_forum($forum, $teacher); $post = $this->helper_post_to_discussion($forum, $discussion, $teacher); $this->setUser($student); $cm = get_coursemodule_from_instance('forum', $forum->id); $this->assertTrue(forum_post_is_visible_privately($post, $cm)); } /** * Ensure that the forum_post_is_visible_privately function reports that a post is visible to a user when another * user wrote the post, and the user under test is the intended recipient. */ public function test_forum_post_is_visible_privately_private_to_user(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, ]); [$student] = $this->helper_create_users($course, 1, 'student'); [$teacher] = $this->helper_create_users($course, 1, 'teacher'); [$discussion] = $this->helper_post_to_forum($forum, $teacher); $post = $this->helper_post_to_discussion($forum, $discussion, $teacher, [ 'privatereplyto' => $student->id, ]); $this->setUser($student); $cm = get_coursemodule_from_instance('forum', $forum->id); $this->assertTrue(forum_post_is_visible_privately($post, $cm)); } /** * Ensure that the forum_post_is_visible_privately function reports that a post is visible to a user when another * user wrote the post, and the user under test is a role with the view capability. */ public function test_forum_post_is_visible_privately_private_to_user_view_as_teacher(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, ]); [$student] = $this->helper_create_users($course, 1, 'student'); [$teacher, $otherteacher] = $this->helper_create_users($course, 2, 'teacher'); [$discussion] = $this->helper_post_to_forum($forum, $teacher); $post = $this->helper_post_to_discussion($forum, $discussion, $teacher, [ 'privatereplyto' => $student->id, ]); $this->setUser($otherteacher); $cm = get_coursemodule_from_instance('forum', $forum->id); $this->assertTrue(forum_post_is_visible_privately($post, $cm)); } /** * Ensure that the forum_post_is_visible_privately function reports that a post is not visible to a user when * another user wrote the post, and the user under test is a role without the view capability. */ public function test_forum_post_is_visible_privately_private_to_user_view_as_other_student(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, ]); [$student, $otherstudent] = $this->helper_create_users($course, 2, 'student'); [$teacher] = $this->helper_create_users($course, 1, 'teacher'); [$discussion] = $this->helper_post_to_forum($forum, $teacher); $post = $this->helper_post_to_discussion($forum, $discussion, $teacher, [ 'privatereplyto' => $student->id, ]); $this->setUser($otherstudent); $cm = get_coursemodule_from_instance('forum', $forum->id); $this->assertFalse(forum_post_is_visible_privately($post, $cm)); } /** * Ensure that the forum_post_is_visible_privately function reports that a post is visible to a user who wrote a * private reply, but not longer holds the view capability. */ public function test_forum_post_is_visible_privately_private_to_user_view_as_author(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, ]); [$student] = $this->helper_create_users($course, 1, 'student'); [$teacher] = $this->helper_create_users($course, 1, 'teacher'); [$discussion] = $this->helper_post_to_forum($forum, $teacher); $post = $this->helper_post_to_discussion($forum, $discussion, $teacher, [ 'privatereplyto' => $student->id, ]); unassign_capability('mod/forum:readprivatereplies', $this->get_role_id('teacher')); $this->setUser($teacher); $cm = get_coursemodule_from_instance('forum', $forum->id); $this->assertTrue(forum_post_is_visible_privately($post, $cm)); } /** * Ensure that the forum_user_can_reply_privately returns true for a teacher replying to a forum post. */ public function test_forum_user_can_reply_privately_as_teacher(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, ]); [$student] = $this->helper_create_users($course, 1, 'student'); [$teacher] = $this->helper_create_users($course, 1, 'teacher'); [, $post] = $this->helper_post_to_forum($forum, $student); $this->setUser($teacher); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); $this->assertTrue(forum_user_can_reply_privately($context, $post)); } /** * Ensure that the forum_user_can_reply_privately returns true for a teacher replying to a forum post. */ public function test_forum_user_can_reply_privately_as_student(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, ]); [$student, $otherstudent] = $this->helper_create_users($course, 2, 'student'); [, $post] = $this->helper_post_to_forum($forum, $student); $this->setUser($otherstudent); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); $this->assertFalse(forum_user_can_reply_privately($context, $post)); } /** * Ensure that the forum_user_can_reply_privately returns false where the parent post is already a private reply. */ public function test_forum_user_can_reply_privately_parent_is_already_private(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, ]); [$student] = $this->helper_create_users($course, 1, 'student'); [$teacher] = $this->helper_create_users($course, 1, 'teacher'); [$discussion] = $this->helper_post_to_forum($forum, $student); $post = $this->helper_post_to_discussion($forum, $discussion, $teacher, ['privatereplyto' => $student->id]); $this->setUser($teacher); $cm = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($cm->id); $this->assertFalse(forum_user_can_reply_privately($context, $post)); } } PK-B\E4Dtests/generator_trait.phpnu[. /** * The forum module trait with additional generator helpers. * * @package mod_forum * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); trait mod_forum_tests_generator_trait { /** * Helper to create the required number of users in the specified course. * Users are enrolled as students by default. * * @param stdClass $course The course object * @param integer $count The number of users to create * @param string $role The role to assign users as * @return array The users created */ protected function helper_create_users($course, $count, $role = null) { $users = array(); for ($i = 0; $i < $count; $i++) { $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id, $role); $users[] = $user; } return $users; } /** * Create a new discussion and post within the specified forum, as the * specified author. * * @param stdClass $forum The forum to post in * @param stdClass $author The author to post as * @param array $fields any other fields in discussion (name, message, messageformat, ...) * @return array An array containing the discussion object, and the post object */ protected function helper_post_to_forum($forum, $author, $fields = array()) { global $DB; $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum'); // Create a discussion in the forum, and then add a post to that discussion. $record = (object)$fields; $record->course = $forum->course; $record->userid = $author->id; $record->forum = $forum->id; $discussion = $generator->create_discussion($record); // Retrieve the post which was created by create_discussion. $post = $DB->get_record('forum_posts', array('discussion' => $discussion->id)); return [$discussion, $post]; } /** * Update the post time for the specified post by $factor. * * @param stdClass $post The post to update * @param int $factor The amount to update by */ protected function helper_update_post_time($post, $factor) { global $DB; // Update the post to have a created in the past. $DB->set_field('forum_posts', 'created', $post->created + $factor, array('id' => $post->id)); } /** * Update the subscription time for the specified user/discussion by $factor. * * @param stdClass $user The user to update * @param stdClass $discussion The discussion to update for this user * @param int $factor The amount to update by */ protected function helper_update_subscription_time($user, $discussion, $factor) { global $DB; $sub = $DB->get_record('forum_discussion_subs', array('userid' => $user->id, 'discussion' => $discussion->id)); // Update the subscription to have a preference in the past. $DB->set_field('forum_discussion_subs', 'preference', $sub->preference + $factor, array('id' => $sub->id)); } /** * Create a new post within an existing discussion, as the specified author. * * @param stdClass $forum The forum to post in * @param stdClass $discussion The discussion to post in * @param stdClass $author The author to post as * @param array $options Additional options to pass to `create_post` * @return stdClass The forum post */ protected function helper_post_to_discussion($forum, $discussion, $author, array $options = []) { global $DB; $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum'); // Add a post to the discussion. $record = array_merge([ 'course' => $forum->course, 'subject' => $discussion->subject, 'userid' => $author->id, 'forum' => $forum->id, 'discussion' => $discussion->id, 'mailnow' => 1, ], $options); $post = $generator->create_post((object) $record); return $post; } /** * Create a new post within an existing discussion, as the specified author. * * @param stdClass $parent The post being replied to * @param stdClass $author The author to post as * @param array $options Additional options to pass to `create_post` * @return stdClass The forum post */ protected function helper_reply_to_post($parent, $author, array $options = []) { global $DB; $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum'); // Add a post to the discussion. $record = (object) array_merge([ 'discussion' => $parent->discussion, 'parent' => $parent->id, 'userid' => $author->id, 'mailnow' => 1, 'subject' => $parent->subject, ], $options); $post = $generator->create_post($record); return $post; } /** * Gets the role id from it's shortname. * * @param string $roleshortname * @return int */ protected function get_role_id($roleshortname) { global $DB; return $DB->get_field('role', 'id', ['shortname' => $roleshortname]); } } PK-B\Tsstests/cron_trait.phpnu[. /** * The forum module cron trait. * * @package mod_forum * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); trait mod_forum_tests_cron_trait { /** * Run the main cron task to queue all tasks, and ensure that posts * were sent to the correct users. * * @param \stdClass[] $expectations The list of users, along with their expected count of messages and digests. */ protected function queue_tasks_and_assert($expectations = []) { global $DB; // Note, we cannot use expectOutputRegex because it only allows for a single RegExp. ob_start(); \core\cron::setup_user(); $cron = new \mod_forum\task\cron_task(); $cron->execute(); $output = ob_get_contents(); ob_end_clean(); $uniqueusers = 0; foreach ($expectations as $expect) { $expect->digests = isset($expect->digests) ? $expect->digests : 0; $expect->messages = isset($expect->messages) ? $expect->messages : 0; $expect->mentioned = isset($expect->mentioned) ? $expect->mentioned : false; if ($expect->digests || $expect->messages) { $expect->mentioned = true; } if (!$expect->mentioned) { $this->assertDoesNotMatchRegularExpression("/Queued 0 for {$expect->userid}/", $output); } else { $uniqueusers++; $this->assertMatchesRegularExpression( "/Queued {$expect->digests} digests and {$expect->messages} messages for {$expect->userid}/", $output ); } } if (empty($expectations)) { $this->assertMatchesRegularExpression("/No posts found./", $output); } else { $this->assertMatchesRegularExpression("/Unique users: {$uniqueusers}/", $output); } // Update the forum queue for digests. $DB->execute("UPDATE {forum_queue} SET timemodified = timemodified - 1"); } /** * Run any send_user_notifications tasks for the specified user, and * ensure that the posts specified were sent. * * @param \stdClass $user * @param \stdClass[] $posts * @param bool $ignoreemptyposts */ protected function send_notifications_and_assert($user, $posts = [], $ignoreemptyposts = false) { ob_start(); $this->runAdhocTasks(\mod_forum\task\send_user_notifications::class, $user->id); $output = ob_get_contents(); ob_end_clean(); if (empty($posts) && !$ignoreemptyposts) { $this->assertEquals('', $output); } else { $this->assertMatchesRegularExpression("/Sending messages to {$user->username}/", $output); foreach ($posts as $post) { $this->assertMatchesRegularExpression("/Post {$post->id} sent/", $output); } $count = count($posts); $this->assertMatchesRegularExpression("/Sent {$count} messages with 0 failures/", $output); } } /** * Run any send_user_digests tasks for the specified user, and * ensure that the posts specified were sent. * * @param \stdClass $user * @param \stdClass[] $fullposts * @param \stdClass[] $shortposts */ protected function send_digests_and_assert($user, $fullposts = [], $shortposts = []) { ob_start(); $this->runAdhocTasks(\mod_forum\task\send_user_digests::class, $user->id); $output = ob_get_contents(); ob_end_clean(); if (empty($shortposts) && empty($fullposts)) { $this->assertEquals('', $output); $this->assertMatchesRegularExpression("/Digest sent with 0 messages./", $output); } else { $this->assertMatchesRegularExpression("/Sending forum digests for {$user->username}/", $output); foreach ($fullposts as $post) { $this->assertMatchesRegularExpression("/Adding post {$post->id} in format 1/", $output); } foreach ($shortposts as $post) { $this->assertMatchesRegularExpression("/Adding post {$post->id} in format 2/", $output); } $count = count($fullposts) + count($shortposts); $this->assertMatchesRegularExpression("/Digest sent with {$count} messages./", $output); } } } PK-B\ ݹݹtests/event/events_test.phpnu[. /** * Tests for forum events. * * @package mod_forum * @category test * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\event; /** * Tests for forum events. * * @package mod_forum * @category test * @copyright 2014 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class events_test extends \advanced_testcase { /** * Tests set up. */ public function setUp(): void { parent::setUp(); // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); $this->resetAfterTest(); } public function tearDown(): void { // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); parent::tearDown(); } /** * Ensure course_searched event validates that searchterm is set. */ public function test_course_searched_searchterm_validation(): void { $course = $this->getDataGenerator()->create_course(); $coursectx = \context_course::instance($course->id); $params = array( 'context' => $coursectx, ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'searchterm' value must be set in other."); \mod_forum\event\course_searched::create($params); } /** * Ensure course_searched event validates that context is the correct level. */ public function test_course_searched_context_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $context = \context_module::instance($forum->cmid); $params = array( 'context' => $context, 'other' => array('searchterm' => 'testing'), ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Context level must be CONTEXT_COURSE.'); \mod_forum\event\course_searched::create($params); } /** * Test course_searched event. */ public function test_course_searched(): void { // Setup test data. $course = $this->getDataGenerator()->create_course(); $coursectx = \context_course::instance($course->id); $searchterm = 'testing123'; $params = array( 'context' => $coursectx, 'other' => array('searchterm' => $searchterm), ); // Create event. $event = \mod_forum\event\course_searched::create($params); // Trigger and capture the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\course_searched', $event); $this->assertEquals($coursectx, $event->get_context()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Ensure discussion_created event validates that forumid is set. */ public function test_discussion_created_forumid_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $context = \context_module::instance($forum->cmid); $params = array( 'context' => $context, ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'forumid' value must be set in other."); \mod_forum\event\discussion_created::create($params); } /** * Ensure discussion_created event validates that the context is the correct level. */ public function test_discussion_created_context_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $params = array( 'context' => \context_system::instance(), 'other' => array('forumid' => $forum->id), ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Context level must be CONTEXT_MODULE.'); \mod_forum\event\discussion_created::create($params); } /** * Test discussion_created event. */ public function test_discussion_created(): void { // Setup test data. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); // 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); $context = \context_module::instance($forum->cmid); $params = array( 'context' => $context, 'objectid' => $discussion->id, 'other' => array('forumid' => $forum->id), ); // Create the event. $event = \mod_forum\event\discussion_created::create($params); // Trigger and capturing the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Check that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\discussion_created', $event); $this->assertEquals($context, $event->get_context()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Ensure discussion_updated event validates that forumid is set. */ public function test_discussion_updated_forumid_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $context = \context_module::instance($forum->cmid); $params = array( 'context' => $context, ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'forumid' value must be set in other."); \mod_forum\event\discussion_updated::create($params); } /** * Ensure discussion_created event validates that the context is the correct level. */ public function test_discussion_updated_context_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $params = array( 'context' => \context_system::instance(), 'other' => array('forumid' => $forum->id), ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Context level must be CONTEXT_MODULE.'); \mod_forum\event\discussion_updated::create($params); } /** * Test discussion_created event. */ public function test_discussion_updated(): void { // Setup test data. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); // 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); $context = \context_module::instance($forum->cmid); $params = array( 'context' => $context, 'objectid' => $discussion->id, 'other' => array('forumid' => $forum->id), ); // Create the event. $event = \mod_forum\event\discussion_updated::create($params); // Trigger and capturing the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Check that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\discussion_updated', $event); $this->assertEquals($context, $event->get_context()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Ensure discussion_deleted event validates that forumid is set. */ public function test_discussion_deleted_forumid_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $context = \context_module::instance($forum->cmid); $params = array( 'context' => $context, ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'forumid' value must be set in other."); \mod_forum\event\discussion_deleted::create($params); } /** * Ensure discussion_deleted event validates that context is of the correct level. */ public function test_discussion_deleted_context_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $params = array( 'context' => \context_system::instance(), 'other' => array('forumid' => $forum->id), ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Context level must be CONTEXT_MODULE.'); \mod_forum\event\discussion_deleted::create($params); } /** * Test discussion_deleted event. */ public function test_discussion_deleted(): void { // Setup test data. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); // 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); $context = \context_module::instance($forum->cmid); $params = array( 'context' => $context, 'objectid' => $discussion->id, 'other' => array('forumid' => $forum->id), ); $event = \mod_forum\event\discussion_deleted::create($params); // Trigger and capture the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\discussion_deleted', $event); $this->assertEquals($context, $event->get_context()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Ensure discussion_moved event validates that fromforumid is set. */ public function test_discussion_moved_fromforumid_validation(): void { $course = $this->getDataGenerator()->create_course(); $toforum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $context = \context_module::instance($toforum->cmid); $params = array( 'context' => $context, 'other' => array('toforumid' => $toforum->id) ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'fromforumid' value must be set in other."); \mod_forum\event\discussion_moved::create($params); } /** * Ensure discussion_moved event validates that toforumid is set. */ public function test_discussion_moved_toforumid_validation(): void { $course = $this->getDataGenerator()->create_course(); $fromforum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $toforum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $context = \context_module::instance($toforum->cmid); $params = array( 'context' => $context, 'other' => array('fromforumid' => $fromforum->id) ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'toforumid' value must be set in other."); \mod_forum\event\discussion_moved::create($params); } /** * Ensure discussion_moved event validates that the context level is correct. */ public function test_discussion_moved_context_validation(): void { $course = $this->getDataGenerator()->create_course(); $fromforum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $toforum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); // Add a discussion. $record = array(); $record['course'] = $course->id; $record['forum'] = $fromforum->id; $record['userid'] = $user->id; $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); $params = array( 'context' => \context_system::instance(), 'objectid' => $discussion->id, 'other' => array('fromforumid' => $fromforum->id, 'toforumid' => $toforum->id) ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Context level must be CONTEXT_MODULE.'); \mod_forum\event\discussion_moved::create($params); } /** * Test discussion_moved event. */ public function test_discussion_moved(): void { // Setup test data. $course = $this->getDataGenerator()->create_course(); $fromforum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $toforum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); // Add a discussion. $record = array(); $record['course'] = $course->id; $record['forum'] = $fromforum->id; $record['userid'] = $user->id; $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); $context = \context_module::instance($toforum->cmid); $params = array( 'context' => $context, 'objectid' => $discussion->id, 'other' => array('fromforumid' => $fromforum->id, 'toforumid' => $toforum->id) ); $event = \mod_forum\event\discussion_moved::create($params); // Trigger and capture the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\discussion_moved', $event); $this->assertEquals($context, $event->get_context()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Ensure discussion_viewed event validates that the contextlevel is correct. */ public function test_discussion_viewed_context_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); // 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); $params = array( 'context' => \context_system::instance(), 'objectid' => $discussion->id, ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Context level must be CONTEXT_MODULE.'); \mod_forum\event\discussion_viewed::create($params); } /** * Test discussion_viewed event. */ public function test_discussion_viewed(): void { // Setup test data. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); // 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); $context = \context_module::instance($forum->cmid); $params = array( 'context' => $context, 'objectid' => $discussion->id, ); $event = \mod_forum\event\discussion_viewed::create($params); // Trigger and capture the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\discussion_viewed', $event); $this->assertEquals($context, $event->get_context()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Ensure course_module_viewed event validates that the contextlevel is correct. */ public function test_course_module_viewed_context_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $params = array( 'context' => \context_system::instance(), 'objectid' => $forum->id, ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Context level must be CONTEXT_MODULE.'); \mod_forum\event\course_module_viewed::create($params); } /** * Test the course_module_viewed event. */ public function test_course_module_viewed(): void { // Setup test data. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $context = \context_module::instance($forum->cmid); $params = array( 'context' => $context, 'objectid' => $forum->id, ); $event = \mod_forum\event\course_module_viewed::create($params); // Trigger and capture the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\course_module_viewed', $event); $this->assertEquals($context, $event->get_context()); $url = new \moodle_url('/mod/forum/view.php', array('f' => $forum->id)); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Ensure subscription_created event validates that the forumid is set. */ public function test_subscription_created_forumid_validation(): void { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $params = array( 'context' => \context_module::instance($forum->cmid), 'relateduserid' => $user->id, ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'forumid' value must be set in other."); \mod_forum\event\subscription_created::create($params); } /** * Ensure subscription_created event validates that the relateduserid is set. */ public function test_subscription_created_relateduserid_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $params = array( 'context' => \context_module::instance($forum->cmid), 'objectid' => $forum->id, ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'relateduserid' must be set."); \mod_forum\event\subscription_created::create($params); } /** * Ensure subscription_created event validates that the contextlevel is correct. */ public function test_subscription_created_contextlevel_validation(): void { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $params = array( 'context' => \context_system::instance(), 'other' => array('forumid' => $forum->id), 'relateduserid' => $user->id, ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Context level must be CONTEXT_MODULE.'); \mod_forum\event\subscription_created::create($params); } /** * Test the subscription_created event. */ public function test_subscription_created(): void { // Setup test data. $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); $context = \context_module::instance($forum->cmid); // Add a subscription. $record = array(); $record['course'] = $course->id; $record['forum'] = $forum->id; $record['userid'] = $user->id; $subscription = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_subscription($record); $params = array( 'context' => $context, 'objectid' => $subscription->id, 'other' => array('forumid' => $forum->id), 'relateduserid' => $user->id, ); $event = \mod_forum\event\subscription_created::create($params); // Trigger and capturing the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\subscription_created', $event); $this->assertEquals($context, $event->get_context()); $url = new \moodle_url('/mod/forum/subscribers.php', array('id' => $forum->id)); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Ensure subscription_deleted event validates that the forumid is set. */ public function test_subscription_deleted_forumid_validation(): void { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $params = array( 'context' => \context_module::instance($forum->cmid), 'relateduserid' => $user->id, ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'forumid' value must be set in other."); \mod_forum\event\subscription_deleted::create($params); } /** * Ensure subscription_deleted event validates that the relateduserid is set. */ public function test_subscription_deleted_relateduserid_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $params = array( 'context' => \context_module::instance($forum->cmid), 'objectid' => $forum->id, ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'relateduserid' must be set."); \mod_forum\event\subscription_deleted::create($params); } /** * Ensure subscription_deleted event validates that the contextlevel is correct. */ public function test_subscription_deleted_contextlevel_validation(): void { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $params = array( 'context' => \context_system::instance(), 'other' => array('forumid' => $forum->id), 'relateduserid' => $user->id, ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Context level must be CONTEXT_MODULE.'); \mod_forum\event\subscription_deleted::create($params); } /** * Test the subscription_deleted event. */ public function test_subscription_deleted(): void { // Setup test data. $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); $context = \context_module::instance($forum->cmid); // Add a subscription. $record = array(); $record['course'] = $course->id; $record['forum'] = $forum->id; $record['userid'] = $user->id; $subscription = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_subscription($record); $params = array( 'context' => $context, 'objectid' => $subscription->id, 'other' => array('forumid' => $forum->id), 'relateduserid' => $user->id, ); $event = \mod_forum\event\subscription_deleted::create($params); // Trigger and capturing the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\subscription_deleted', $event); $this->assertEquals($context, $event->get_context()); $url = new \moodle_url('/mod/forum/subscribers.php', array('id' => $forum->id)); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Ensure readtracking_enabled event validates that the forumid is set. */ public function test_readtracking_enabled_forumid_validation(): void { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $params = array( 'context' => \context_module::instance($forum->cmid), 'relateduserid' => $user->id, ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'forumid' value must be set in other."); \mod_forum\event\readtracking_enabled::create($params); } /** * Ensure readtracking_enabled event validates that the relateduserid is set. */ public function test_readtracking_enabled_relateduserid_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $params = array( 'context' => \context_module::instance($forum->cmid), 'objectid' => $forum->id, ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'relateduserid' must be set."); \mod_forum\event\readtracking_enabled::create($params); } /** * Ensure readtracking_enabled event validates that the contextlevel is correct. */ public function test_readtracking_enabled_contextlevel_validation(): void { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $params = array( 'context' => \context_system::instance(), 'other' => array('forumid' => $forum->id), 'relateduserid' => $user->id, ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Context level must be CONTEXT_MODULE.'); \mod_forum\event\readtracking_enabled::create($params); } /** * Test the readtracking_enabled event. */ public function test_readtracking_enabled(): void { // Setup test data. $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $context = \context_module::instance($forum->cmid); $params = array( 'context' => $context, 'other' => array('forumid' => $forum->id), 'relateduserid' => $user->id, ); $event = \mod_forum\event\readtracking_enabled::create($params); // Trigger and capture the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\readtracking_enabled', $event); $this->assertEquals($context, $event->get_context()); $url = new \moodle_url('/mod/forum/view.php', array('f' => $forum->id)); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Ensure readtracking_disabled event validates that the forumid is set. */ public function test_readtracking_disabled_forumid_validation(): void { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $params = array( 'context' => \context_module::instance($forum->cmid), 'relateduserid' => $user->id, ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'forumid' value must be set in other."); \mod_forum\event\readtracking_disabled::create($params); } /** * Ensure readtracking_disabled event validates that the relateduserid is set. */ public function test_readtracking_disabled_relateduserid_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $params = array( 'context' => \context_module::instance($forum->cmid), 'objectid' => $forum->id, ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'relateduserid' must be set."); \mod_forum\event\readtracking_disabled::create($params); } /** * Ensure readtracking_disabled event validates that the contextlevel is correct */ public function test_readtracking_disabled_contextlevel_validation(): void { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $params = array( 'context' => \context_system::instance(), 'other' => array('forumid' => $forum->id), 'relateduserid' => $user->id, ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Context level must be CONTEXT_MODULE.'); \mod_forum\event\readtracking_disabled::create($params); } /** * Test the readtracking_disabled event. */ public function test_readtracking_disabled(): void { // Setup test data. $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $context = \context_module::instance($forum->cmid); $params = array( 'context' => $context, 'other' => array('forumid' => $forum->id), 'relateduserid' => $user->id, ); $event = \mod_forum\event\readtracking_disabled::create($params); // Trigger and capture the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\readtracking_disabled', $event); $this->assertEquals($context, $event->get_context()); $url = new \moodle_url('/mod/forum/view.php', array('f' => $forum->id)); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Ensure subscribers_viewed event validates that the forumid is set. */ public function test_subscribers_viewed_forumid_validation(): void { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $params = array( 'context' => \context_module::instance($forum->cmid), 'relateduserid' => $user->id, ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'forumid' value must be set in other."); \mod_forum\event\subscribers_viewed::create($params); } /** * Ensure subscribers_viewed event validates that the contextlevel is correct. */ public function test_subscribers_viewed_contextlevel_validation(): void { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $params = array( 'context' => \context_system::instance(), 'other' => array('forumid' => $forum->id), 'relateduserid' => $user->id, ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Context level must be CONTEXT_MODULE.'); \mod_forum\event\subscribers_viewed::create($params); } /** * Test the subscribers_viewed event. */ public function test_subscribers_viewed(): void { // Setup test data. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $context = \context_module::instance($forum->cmid); $params = array( 'context' => $context, 'other' => array('forumid' => $forum->id), ); $event = \mod_forum\event\subscribers_viewed::create($params); // Trigger and capture the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\subscribers_viewed', $event); $this->assertEquals($context, $event->get_context()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Ensure user_report_viewed event validates that the reportmode is set. */ public function test_user_report_viewed_reportmode_validation(): void { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $params = array( 'context' => \context_course::instance($course->id), 'relateduserid' => $user->id, ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'reportmode' value must be set in other."); \mod_forum\event\user_report_viewed::create($params); } /** * Ensure user_report_viewed event validates that the contextlevel is correct. */ public function test_user_report_viewed_contextlevel_validation(): void { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $params = array( 'context' => \context_module::instance($forum->cmid), 'other' => array('reportmode' => 'posts'), 'relateduserid' => $user->id, ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Context level must be either CONTEXT_SYSTEM, CONTEXT_COURSE or CONTEXT_USER.'); \mod_forum\event\user_report_viewed::create($params); } /** * Ensure user_report_viewed event validates that the relateduserid is set. */ public function test_user_report_viewed_relateduserid_validation(): void { $params = array( 'context' => \context_system::instance(), 'other' => array('reportmode' => 'posts'), ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'relateduserid' must be set."); \mod_forum\event\user_report_viewed::create($params); } /** * Test the user_report_viewed event. */ public function test_user_report_viewed(): void { // Setup test data. $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); $params = array( 'context' => $context, 'relateduserid' => $user->id, 'other' => array('reportmode' => 'discussions'), ); $event = \mod_forum\event\user_report_viewed::create($params); // Trigger and capture the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\user_report_viewed', $event); $this->assertEquals($context, $event->get_context()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Ensure post_created event validates that the postid is set. */ public function test_post_created_postid_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); // 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); $params = array( 'context' => \context_module::instance($forum->cmid), 'other' => array('forumid' => $forum->id, 'forumtype' => $forum->type, 'discussionid' => $discussion->id) ); \mod_forum\event\post_created::create($params); } /** * Ensure post_created event validates that the discussionid is set. */ public function test_post_created_discussionid_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $params = array( 'context' => \context_module::instance($forum->cmid), 'objectid' => $post->id, 'other' => array('forumid' => $forum->id, 'forumtype' => $forum->type) ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'discussionid' value must be set in other."); \mod_forum\event\post_created::create($params); } /** * Ensure post_created event validates that the forumid is set. */ public function test_post_created_forumid_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $params = array( 'context' => \context_module::instance($forum->cmid), 'objectid' => $post->id, 'other' => array('discussionid' => $discussion->id, 'forumtype' => $forum->type) ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'forumid' value must be set in other."); \mod_forum\event\post_created::create($params); } /** * Ensure post_created event validates that the forumtype is set. */ public function test_post_created_forumtype_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $params = array( 'context' => \context_module::instance($forum->cmid), 'objectid' => $post->id, 'other' => array('discussionid' => $discussion->id, 'forumid' => $forum->id) ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'forumtype' value must be set in other."); \mod_forum\event\post_created::create($params); } /** * Ensure post_created event validates that the contextlevel is correct. */ public function test_post_created_context_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $params = array( 'context' => \context_system::instance(), 'objectid' => $post->id, 'other' => array('discussionid' => $discussion->id, 'forumid' => $forum->id, 'forumtype' => $forum->type) ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Context level must be CONTEXT_MODULE.'); \mod_forum\event\post_created::create($params); } /** * Test the post_created event. */ public function test_post_created(): void { // Setup test data. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $context = \context_module::instance($forum->cmid); $params = array( 'context' => $context, 'objectid' => $post->id, 'other' => array('discussionid' => $discussion->id, 'forumid' => $forum->id, 'forumtype' => $forum->type) ); $event = \mod_forum\event\post_created::create($params); // Trigger and capturing the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\post_created', $event); $this->assertEquals($context, $event->get_context()); $url = new \moodle_url('/mod/forum/discuss.php', array('d' => $discussion->id)); $url->set_anchor('p'.$event->objectid); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Test the post_created event for a single discussion forum. */ public function test_post_created_single(): void { // Setup test data. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id, 'type' => 'single')); $user = $this->getDataGenerator()->create_user(); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $context = \context_module::instance($forum->cmid); $params = array( 'context' => $context, 'objectid' => $post->id, 'other' => array('discussionid' => $discussion->id, 'forumid' => $forum->id, 'forumtype' => $forum->type) ); $event = \mod_forum\event\post_created::create($params); // Trigger and capturing the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\post_created', $event); $this->assertEquals($context, $event->get_context()); $url = new \moodle_url('/mod/forum/view.php', array('f' => $forum->id)); $url->set_anchor('p'.$event->objectid); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Ensure post_deleted event validates that the postid is set. */ public function test_post_deleted_postid_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); // 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); $params = array( 'context' => \context_module::instance($forum->cmid), 'other' => array('forumid' => $forum->id, 'forumtype' => $forum->type, 'discussionid' => $discussion->id) ); \mod_forum\event\post_deleted::create($params); } /** * Ensure post_deleted event validates that the discussionid is set. */ public function test_post_deleted_discussionid_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $params = array( 'context' => \context_module::instance($forum->cmid), 'objectid' => $post->id, 'other' => array('forumid' => $forum->id, 'forumtype' => $forum->type) ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'discussionid' value must be set in other."); \mod_forum\event\post_deleted::create($params); } /** * Ensure post_deleted event validates that the forumid is set. */ public function test_post_deleted_forumid_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $params = array( 'context' => \context_module::instance($forum->cmid), 'objectid' => $post->id, 'other' => array('discussionid' => $discussion->id, 'forumtype' => $forum->type) ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'forumid' value must be set in other."); \mod_forum\event\post_deleted::create($params); } /** * Ensure post_deleted event validates that the forumtype is set. */ public function test_post_deleted_forumtype_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $params = array( 'context' => \context_module::instance($forum->cmid), 'objectid' => $post->id, 'other' => array('discussionid' => $discussion->id, 'forumid' => $forum->id) ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'forumtype' value must be set in other."); \mod_forum\event\post_deleted::create($params); } /** * Ensure post_deleted event validates that the contextlevel is correct. */ public function test_post_deleted_context_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $params = array( 'context' => \context_system::instance(), 'objectid' => $post->id, 'other' => array('discussionid' => $discussion->id, 'forumid' => $forum->id, 'forumtype' => $forum->type) ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Context level must be CONTEXT_MODULE.'); \mod_forum\event\post_deleted::create($params); } /** * Test post_deleted event. */ public function test_post_deleted(): void { global $DB; // Setup test data. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); $cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course); // 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); // When creating a discussion we also create a post, so get the post. $discussionpost = $DB->get_records('forum_posts'); // Will only be one here. $discussionpost = reset($discussionpost); // Add a few posts. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $posts = array(); $posts[$discussionpost->id] = $discussionpost; for ($i = 0; $i < 3; $i++) { $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $posts[$post->id] = $post; } // Delete the last post and capture the event. $lastpost = end($posts); $sink = $this->redirectEvents(); forum_delete_post($lastpost, true, $course, $cm, $forum); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Check that the events contain the expected values. $this->assertInstanceOf('\mod_forum\event\post_deleted', $event); $this->assertEquals(\context_module::instance($forum->cmid), $event->get_context()); $url = new \moodle_url('/mod/forum/discuss.php', array('d' => $discussion->id)); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); // Delete the whole discussion and capture the events. $sink = $this->redirectEvents(); forum_delete_discussion($discussion, true, $course, $cm, $forum); $events = $sink->get_events(); // We will have 4 events. One for the discussion, another one for the discussion topic post, and two for the posts. $this->assertCount(4, $events); // Loop through the events and check they are valid. foreach ($events as $event) { if ($event instanceof \mod_forum\event\discussion_deleted) { // Check that the event contains the expected values. $this->assertEquals($event->objectid, $discussion->id); $this->assertEquals(\context_module::instance($forum->cmid), $event->get_context()); $url = new \moodle_url('/mod/forum/view.php', array('id' => $forum->cmid)); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } else { $post = $posts[$event->objectid]; // Check that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\post_deleted', $event); $this->assertEquals($event->objectid, $post->id); $this->assertEquals(\context_module::instance($forum->cmid), $event->get_context()); $url = new \moodle_url('/mod/forum/discuss.php', array('d' => $discussion->id)); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } } } /** * Test post_deleted event for a single discussion forum. */ public function test_post_deleted_single(): void { // Setup test data. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id, 'type' => 'single')); $user = $this->getDataGenerator()->create_user(); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $context = \context_module::instance($forum->cmid); $params = array( 'context' => $context, 'objectid' => $post->id, 'other' => array('discussionid' => $discussion->id, 'forumid' => $forum->id, 'forumtype' => $forum->type) ); $event = \mod_forum\event\post_deleted::create($params); // Trigger and capture the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\post_deleted', $event); $this->assertEquals($context, $event->get_context()); $url = new \moodle_url('/mod/forum/view.php', array('f' => $forum->id)); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Ensure post_updated event validates that the discussionid is set. */ public function test_post_updated_discussionid_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $params = array( 'context' => \context_module::instance($forum->cmid), 'objectid' => $post->id, 'other' => array('forumid' => $forum->id, 'forumtype' => $forum->type) ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'discussionid' value must be set in other."); \mod_forum\event\post_updated::create($params); } /** * Ensure post_updated event validates that the forumid is set. */ public function test_post_updated_forumid_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $params = array( 'context' => \context_module::instance($forum->cmid), 'objectid' => $post->id, 'other' => array('discussionid' => $discussion->id, 'forumtype' => $forum->type) ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'forumid' value must be set in other."); \mod_forum\event\post_updated::create($params); } /** * Ensure post_updated event validates that the forumtype is set. */ public function test_post_updated_forumtype_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $params = array( 'context' => \context_module::instance($forum->cmid), 'objectid' => $post->id, 'other' => array('discussionid' => $discussion->id, 'forumid' => $forum->id) ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'forumtype' value must be set in other."); \mod_forum\event\post_updated::create($params); } /** * Ensure post_updated event validates that the contextlevel is correct. */ public function test_post_updated_context_validation(): void { $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $params = array( 'context' => \context_system::instance(), 'objectid' => $post->id, 'other' => array('discussionid' => $discussion->id, 'forumid' => $forum->id, 'forumtype' => $forum->type) ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Context level must be CONTEXT_MODULE.'); \mod_forum\event\post_updated::create($params); } /** * Test post_updated event. */ public function test_post_updated(): void { // Setup test data. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); $user = $this->getDataGenerator()->create_user(); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $context = \context_module::instance($forum->cmid); $params = array( 'context' => $context, 'objectid' => $post->id, 'other' => array('discussionid' => $discussion->id, 'forumid' => $forum->id, 'forumtype' => $forum->type) ); $event = \mod_forum\event\post_updated::create($params); // Trigger and capturing the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\post_updated', $event); $this->assertEquals($context, $event->get_context()); $url = new \moodle_url('/mod/forum/discuss.php', array('d' => $discussion->id)); $url->set_anchor('p'.$event->objectid); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Test post_updated event. */ public function test_post_updated_single(): void { // Setup test data. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id, 'type' => 'single')); $user = $this->getDataGenerator()->create_user(); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $context = \context_module::instance($forum->cmid); $params = array( 'context' => $context, 'objectid' => $post->id, 'other' => array('discussionid' => $discussion->id, 'forumid' => $forum->id, 'forumtype' => $forum->type) ); $event = \mod_forum\event\post_updated::create($params); // Trigger and capturing the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\post_updated', $event); $this->assertEquals($context, $event->get_context()); $url = new \moodle_url('/mod/forum/view.php', array('f' => $forum->id)); $url->set_anchor('p'.$post->id); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Test discussion_subscription_created event. */ public function test_discussion_subscription_created(): void { global $CFG; require_once($CFG->dirroot . '/mod/forum/lib.php'); // Setup test data. $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); // Trigger and capturing the event. $sink = $this->redirectEvents(); // Trigger the event by subscribing the user to the forum discussion. \mod_forum\subscriptions::subscribe_user_to_discussion($user->id, $discussion); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\discussion_subscription_created', $event); $cm = get_coursemodule_from_instance('forum', $discussion->forum); $context = \context_module::instance($cm->id); $this->assertEquals($context, $event->get_context()); $url = new \moodle_url('/mod/forum/subscribe.php', array( 'id' => $forum->id, 'd' => $discussion->id )); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Test validation of discussion_subscription_created event. */ public function test_discussion_subscription_created_validation(): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/forum/lib.php'); // Setup test data. $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); // The user is not subscribed to the forum. Insert a new discussion subscription. $subscription = new \stdClass(); $subscription->userid = $user->id; $subscription->forum = $forum->id; $subscription->discussion = $discussion->id; $subscription->preference = time(); $subscription->id = $DB->insert_record('forum_discussion_subs', $subscription); $context = \context_module::instance($forum->cmid); $params = array( 'context' => $context, 'objectid' => $subscription->id, 'relateduserid' => $user->id, 'other' => array( 'forumid' => $forum->id, 'discussion' => $discussion->id, ) ); $event = \mod_forum\event\discussion_subscription_created::create($params); // Trigger and capturing the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); } /** * Test contextlevel validation of discussion_subscription_created event. */ public function test_discussion_subscription_created_validation_contextlevel(): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/forum/lib.php'); // Setup test data. $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); // The user is not subscribed to the forum. Insert a new discussion subscription. $subscription = new \stdClass(); $subscription->userid = $user->id; $subscription->forum = $forum->id; $subscription->discussion = $discussion->id; $subscription->preference = time(); $subscription->id = $DB->insert_record('forum_discussion_subs', $subscription); $context = \context_module::instance($forum->cmid); $params = array( 'context' => \context_course::instance($course->id), 'objectid' => $subscription->id, 'relateduserid' => $user->id, 'other' => array( 'forumid' => $forum->id, 'discussion' => $discussion->id, ) ); // Without an invalid context. $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Context level must be CONTEXT_MODULE.'); \mod_forum\event\discussion_subscription_created::create($params); } /** * Test discussion validation of discussion_subscription_created event. */ public function test_discussion_subscription_created_validation_discussion(): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/forum/lib.php'); // Setup test data. $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); // The user is not subscribed to the forum. Insert a new discussion subscription. $subscription = new \stdClass(); $subscription->userid = $user->id; $subscription->forum = $forum->id; $subscription->discussion = $discussion->id; $subscription->preference = time(); $subscription->id = $DB->insert_record('forum_discussion_subs', $subscription); // Without the discussion. $params = array( 'context' => \context_module::instance($forum->cmid), 'objectid' => $subscription->id, 'relateduserid' => $user->id, 'other' => array( 'forumid' => $forum->id, ) ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'discussion' value must be set in other."); \mod_forum\event\discussion_subscription_created::create($params); } /** * Test forumid validation of discussion_subscription_created event. */ public function test_discussion_subscription_created_validation_forumid(): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/forum/lib.php'); // Setup test data. $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); // The user is not subscribed to the forum. Insert a new discussion subscription. $subscription = new \stdClass(); $subscription->userid = $user->id; $subscription->forum = $forum->id; $subscription->discussion = $discussion->id; $subscription->preference = time(); $subscription->id = $DB->insert_record('forum_discussion_subs', $subscription); // Without the forumid. $params = array( 'context' => \context_module::instance($forum->cmid), 'objectid' => $subscription->id, 'relateduserid' => $user->id, 'other' => array( 'discussion' => $discussion->id, ) ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'forumid' value must be set in other."); \mod_forum\event\discussion_subscription_created::create($params); } /** * Test relateduserid validation of discussion_subscription_created event. */ public function test_discussion_subscription_created_validation_relateduserid(): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/forum/lib.php'); // Setup test data. $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); // The user is not subscribed to the forum. Insert a new discussion subscription. $subscription = new \stdClass(); $subscription->userid = $user->id; $subscription->forum = $forum->id; $subscription->discussion = $discussion->id; $subscription->preference = time(); $subscription->id = $DB->insert_record('forum_discussion_subs', $subscription); $context = \context_module::instance($forum->cmid); // Without the relateduserid. $params = array( 'context' => \context_module::instance($forum->cmid), 'objectid' => $subscription->id, 'other' => array( 'forumid' => $forum->id, 'discussion' => $discussion->id, ) ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'relateduserid' must be set."); \mod_forum\event\discussion_subscription_created::create($params); } /** * Test discussion_subscription_deleted event. */ public function test_discussion_subscription_deleted(): void { global $CFG; require_once($CFG->dirroot . '/mod/forum/lib.php'); // Setup test data. $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); // Trigger and capturing the event. $sink = $this->redirectEvents(); // Trigger the event by unsubscribing the user to the forum discussion. \mod_forum\subscriptions::unsubscribe_user_from_discussion($user->id, $discussion); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\discussion_subscription_deleted', $event); $cm = get_coursemodule_from_instance('forum', $discussion->forum); $context = \context_module::instance($cm->id); $this->assertEquals($context, $event->get_context()); $url = new \moodle_url('/mod/forum/subscribe.php', array( 'id' => $forum->id, 'd' => $discussion->id )); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Test validation of discussion_subscription_deleted event. */ public function test_discussion_subscription_deleted_validation(): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/forum/lib.php'); // Setup test data. $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); // The user is not subscribed to the forum. Insert a new discussion subscription. $subscription = new \stdClass(); $subscription->userid = $user->id; $subscription->forum = $forum->id; $subscription->discussion = $discussion->id; $subscription->preference = \mod_forum\subscriptions::FORUM_DISCUSSION_UNSUBSCRIBED; $subscription->id = $DB->insert_record('forum_discussion_subs', $subscription); $context = \context_module::instance($forum->cmid); $params = array( 'context' => $context, 'objectid' => $subscription->id, 'relateduserid' => $user->id, 'other' => array( 'forumid' => $forum->id, 'discussion' => $discussion->id, ) ); $event = \mod_forum\event\discussion_subscription_deleted::create($params); // Trigger and capturing the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Without an invalid context. $params['context'] = \context_course::instance($course->id); $this->expectException('coding_exception'); $this->expectExceptionMessage('Context level must be CONTEXT_MODULE.'); \mod_forum\event\discussion_deleted::create($params); // Without the discussion. unset($params['discussion']); $this->expectException('coding_exception'); $this->expectExceptionMessage('The \'discussion\' value must be set in other.'); \mod_forum\event\discussion_deleted::create($params); // Without the forumid. unset($params['forumid']); $this->expectException('coding_exception'); $this->expectExceptionMessage('The \'forumid\' value must be set in other.'); \mod_forum\event\discussion_deleted::create($params); // Without the relateduserid. unset($params['relateduserid']); $this->expectException('coding_exception'); $this->expectExceptionMessage('The \'relateduserid\' value must be set in other.'); \mod_forum\event\discussion_deleted::create($params); } /** * Test contextlevel validation of discussion_subscription_deleted event. */ public function test_discussion_subscription_deleted_validation_contextlevel(): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/forum/lib.php'); // Setup test data. $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); // The user is not subscribed to the forum. Insert a new discussion subscription. $subscription = new \stdClass(); $subscription->userid = $user->id; $subscription->forum = $forum->id; $subscription->discussion = $discussion->id; $subscription->preference = time(); $subscription->id = $DB->insert_record('forum_discussion_subs', $subscription); $context = \context_module::instance($forum->cmid); $params = array( 'context' => \context_course::instance($course->id), 'objectid' => $subscription->id, 'relateduserid' => $user->id, 'other' => array( 'forumid' => $forum->id, 'discussion' => $discussion->id, ) ); // Without an invalid context. $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Context level must be CONTEXT_MODULE.'); \mod_forum\event\discussion_subscription_deleted::create($params); } /** * Test discussion validation of discussion_subscription_deleted event. */ public function test_discussion_subscription_deleted_validation_discussion(): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/forum/lib.php'); // Setup test data. $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); // The user is not subscribed to the forum. Insert a new discussion subscription. $subscription = new \stdClass(); $subscription->userid = $user->id; $subscription->forum = $forum->id; $subscription->discussion = $discussion->id; $subscription->preference = time(); $subscription->id = $DB->insert_record('forum_discussion_subs', $subscription); // Without the discussion. $params = array( 'context' => \context_module::instance($forum->cmid), 'objectid' => $subscription->id, 'relateduserid' => $user->id, 'other' => array( 'forumid' => $forum->id, ) ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'discussion' value must be set in other."); \mod_forum\event\discussion_subscription_deleted::create($params); } /** * Test forumid validation of discussion_subscription_deleted event. */ public function test_discussion_subscription_deleted_validation_forumid(): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/forum/lib.php'); // Setup test data. $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); // The user is not subscribed to the forum. Insert a new discussion subscription. $subscription = new \stdClass(); $subscription->userid = $user->id; $subscription->forum = $forum->id; $subscription->discussion = $discussion->id; $subscription->preference = time(); $subscription->id = $DB->insert_record('forum_discussion_subs', $subscription); // Without the forumid. $params = array( 'context' => \context_module::instance($forum->cmid), 'objectid' => $subscription->id, 'relateduserid' => $user->id, 'other' => array( 'discussion' => $discussion->id, ) ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'forumid' value must be set in other."); \mod_forum\event\discussion_subscription_deleted::create($params); } /** * Test relateduserid validation of discussion_subscription_deleted event. */ public function test_discussion_subscription_deleted_validation_relateduserid(): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/forum/lib.php'); // Setup test data. $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); // The user is not subscribed to the forum. Insert a new discussion subscription. $subscription = new \stdClass(); $subscription->userid = $user->id; $subscription->forum = $forum->id; $subscription->discussion = $discussion->id; $subscription->preference = time(); $subscription->id = $DB->insert_record('forum_discussion_subs', $subscription); $context = \context_module::instance($forum->cmid); // Without the relateduserid. $params = array( 'context' => \context_module::instance($forum->cmid), 'objectid' => $subscription->id, 'other' => array( 'forumid' => $forum->id, 'discussion' => $discussion->id, ) ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("The 'relateduserid' must be set."); \mod_forum\event\discussion_subscription_deleted::create($params); } /** * Test that the correct context is used in the events when subscribing * users. */ public function test_forum_subscription_page_context_valid(): void { global $CFG, $PAGE; require_once($CFG->dirroot . '/mod/forum/lib.php'); // Setup test data. $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); $quiz = $this->getDataGenerator()->create_module('quiz', $options); // 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); // Add a post. $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); // Set up the default page event to use this forum. $PAGE = new \moodle_page(); $cm = get_coursemodule_from_instance('forum', $discussion->forum); $context = \context_module::instance($cm->id); $PAGE->set_context($context); $PAGE->set_cm($cm, $course, $forum); // Trigger and capturing the event. $sink = $this->redirectEvents(); // Trigger the event by subscribing the user to the forum. \mod_forum\subscriptions::subscribe_user($user->id, $forum); $events = $sink->get_events(); $sink->clear(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\subscription_created', $event); $this->assertEquals($context, $event->get_context()); // Trigger the event by unsubscribing the user to the forum. \mod_forum\subscriptions::unsubscribe_user($user->id, $forum); $events = $sink->get_events(); $sink->clear(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\subscription_deleted', $event); $this->assertEquals($context, $event->get_context()); // Trigger the event by subscribing the user to the discussion. \mod_forum\subscriptions::subscribe_user_to_discussion($user->id, $discussion); $events = $sink->get_events(); $sink->clear(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\discussion_subscription_created', $event); $this->assertEquals($context, $event->get_context()); // Trigger the event by unsubscribing the user from the discussion. \mod_forum\subscriptions::unsubscribe_user_from_discussion($user->id, $discussion); $events = $sink->get_events(); $sink->clear(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\discussion_subscription_deleted', $event); $this->assertEquals($context, $event->get_context()); // Now try with the context for a different module (quiz). $PAGE = new \moodle_page(); $cm = get_coursemodule_from_instance('quiz', $quiz->id); $quizcontext = \context_module::instance($cm->id); $PAGE->set_context($quizcontext); $PAGE->set_cm($cm, $course, $quiz); // Trigger and capturing the event. $sink = $this->redirectEvents(); // Trigger the event by subscribing the user to the forum. \mod_forum\subscriptions::subscribe_user($user->id, $forum); $events = $sink->get_events(); $sink->clear(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\subscription_created', $event); $this->assertEquals($context, $event->get_context()); // Trigger the event by unsubscribing the user to the forum. \mod_forum\subscriptions::unsubscribe_user($user->id, $forum); $events = $sink->get_events(); $sink->clear(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\subscription_deleted', $event); $this->assertEquals($context, $event->get_context()); // Trigger the event by subscribing the user to the discussion. \mod_forum\subscriptions::subscribe_user_to_discussion($user->id, $discussion); $events = $sink->get_events(); $sink->clear(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\discussion_subscription_created', $event); $this->assertEquals($context, $event->get_context()); // Trigger the event by unsubscribing the user from the discussion. \mod_forum\subscriptions::unsubscribe_user_from_discussion($user->id, $discussion); $events = $sink->get_events(); $sink->clear(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\discussion_subscription_deleted', $event); $this->assertEquals($context, $event->get_context()); // Now try with the course context - the module context should still be used. $PAGE = new \moodle_page(); $coursecontext = \context_course::instance($course->id); $PAGE->set_context($coursecontext); // Trigger the event by subscribing the user to the forum. \mod_forum\subscriptions::subscribe_user($user->id, $forum); $events = $sink->get_events(); $sink->clear(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\subscription_created', $event); $this->assertEquals($context, $event->get_context()); // Trigger the event by unsubscribing the user to the forum. \mod_forum\subscriptions::unsubscribe_user($user->id, $forum); $events = $sink->get_events(); $sink->clear(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\subscription_deleted', $event); $this->assertEquals($context, $event->get_context()); // Trigger the event by subscribing the user to the discussion. \mod_forum\subscriptions::subscribe_user_to_discussion($user->id, $discussion); $events = $sink->get_events(); $sink->clear(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\discussion_subscription_created', $event); $this->assertEquals($context, $event->get_context()); // Trigger the event by unsubscribing the user from the discussion. \mod_forum\subscriptions::unsubscribe_user_from_discussion($user->id, $discussion); $events = $sink->get_events(); $sink->clear(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_forum\event\discussion_subscription_deleted', $event); $this->assertEquals($context, $event->get_context()); } /** * Test mod_forum_observer methods. */ public function test_observers(): void { global $DB, $CFG; require_once($CFG->dirroot . '/mod/forum/lib.php'); $forumgen = $this->getDataGenerator()->get_plugin_generator('mod_forum'); $course = $this->getDataGenerator()->create_course(); $trackedrecord = array('course' => $course->id, 'type' => 'general', 'forcesubscribe' => FORUM_INITIALSUBSCRIBE); $untrackedrecord = array('course' => $course->id, 'type' => 'general'); $trackedforum = $this->getDataGenerator()->create_module('forum', $trackedrecord); $untrackedforum = $this->getDataGenerator()->create_module('forum', $untrackedrecord); // Used functions don't require these settings; adding // them just in case there are APIs changes in future. $user = $this->getDataGenerator()->create_user(array( 'maildigest' => 1, 'trackforums' => 1 )); $manplugin = enrol_get_plugin('manual'); $manualenrol = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'manual')); $student = $DB->get_record('role', array('shortname' => 'student')); // The role_assign observer does it's job adding the forum_subscriptions record. $manplugin->enrol_user($manualenrol, $user->id, $student->id); // They are not required, but in a real environment they are supposed to be required; // adding them just in case there are APIs changes in future. set_config('forum_trackingtype', 1); set_config('forum_trackreadposts', 1); $record = array(); $record['course'] = $course->id; $record['forum'] = $trackedforum->id; $record['userid'] = $user->id; $discussion = $forumgen->create_discussion($record); $record = array(); $record['discussion'] = $discussion->id; $record['userid'] = $user->id; $post = $forumgen->create_post($record); forum_tp_add_read_record($user->id, $post->id); forum_set_user_maildigest($trackedforum, 2, $user); forum_tp_stop_tracking($untrackedforum->id, $user->id); $this->assertEquals(1, $DB->count_records('forum_subscriptions')); $this->assertEquals(1, $DB->count_records('forum_digests')); $this->assertEquals(1, $DB->count_records('forum_track_prefs')); $this->assertEquals(1, $DB->count_records('forum_read')); // The course_module_created observer does it's job adding a subscription. $forumrecord = array('course' => $course->id, 'type' => 'general', 'forcesubscribe' => FORUM_INITIALSUBSCRIBE); $extraforum = $this->getDataGenerator()->create_module('forum', $forumrecord); $this->assertEquals(2, $DB->count_records('forum_subscriptions')); $manplugin->unenrol_user($manualenrol, $user->id); $this->assertEquals(0, $DB->count_records('forum_digests')); $this->assertEquals(0, $DB->count_records('forum_subscriptions')); $this->assertEquals(0, $DB->count_records('forum_track_prefs')); $this->assertEquals(0, $DB->count_records('forum_read')); } } PK-B\b+tests/task/send_user_notifications_test.phpnu[. namespace mod_forum\task; use Exception; use InvalidArgumentException; use RuntimeException; /** * Unit tests for the send_user_notifications task in the forum module. * * This class contains test cases to ensure that the forum module's * send_user_notifications task functions as expected, particularly * when handling email notifications to users after forum posts. * * It tests different scenarios related to user email configurations, * such as when a user has an empty email address, when a user has exceeded * the bounce threshold, and how the system behaves when posts are attempted * to be sent under these conditions. * * Each test verifies that the appropriate exceptions are thrown, that * messages are correctly sent (or skipped), and that the task requeues * appropriately based on the user's email settings and other related conditions. * * @package mod_forum * @copyright 2024 Waleed ul hassan * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class send_user_notifications_test extends \advanced_testcase { /** * Testcase to check send notification for post via email * * @covers \mod_forum\task\send_user_notifications * @dataProvider send_user_notifications_cases * @param array $userdata Test user for the case. * @param string $expectedstring Expected string during the test case. * @param array $expecteddebuggingstrings Expected debugging strings array. * @param bool $expectedassertion Expected adhoc task to be re queued or not. * @param array $userpreferences (optional) User preferences for the test case. * @throws InvalidArgumentException If the user data is invalid. * @throws RuntimeException If the notification fails to send. * @throws Exception For any other general errors. */ public function test_send_user_notifications( array $userdata, string $expectedstring, array $expecteddebuggingstrings, bool $expectedassertion, array $userpreferences = [], ): void { global $CFG; require_once($CFG->dirroot . '/mod/forum/lib.php'); $CFG->handlebounces = true; $this->resetAfterTest(true); $this->preventResetByRollback(); $this->redirectEmails(); // Creating a user. $user = $this->getDataGenerator()->create_user($userdata); // Set user preferences. foreach ($userpreferences as $name => $value) { set_user_preference($name, $value, $user); } // Create a course and a forum. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, 'forcesubscribe' => \FORUM_FORCESUBSCRIBE, ]); // Create a discussion in the forum. $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion([ 'course' => $course->id, 'forum' => $forum->id, 'userid' => $user->id, 'message' => 'Test discussion', ]); // Create a post in the discussion. $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post([ 'course' => $course->id, 'discussion' => $discussion->id, 'userid' => $user->id, 'message' => 'Test post', ]); // Setting placeholders for user id and post id. $expectedstring = sprintf($expectedstring, $user->id, $post->id, $user->id); $expecteddebuggingstrings = array_map(function($expecteddebuggingstring) use ($user) { return sprintf($expecteddebuggingstring, $user->id, $user->firstname . " " . $user->lastname); }, $expecteddebuggingstrings); // Enroll the user in the course. $this->getDataGenerator()->enrol_user($user->id, $course->id); // Trigger the send_user_notifications task. $task = new send_user_notifications(); $task->set_userid($user->id); $task->set_custom_data($post->id); $this->expectOutputString($expectedstring); // Testing if an exception is thrown because the task is re queued if an exception is thrown in the adhoc task. $expectedexception = 'Error sending posts.'; try { $task->execute(); } catch (\Exception $ex) { $this->assertEquals($expectedexception, $ex->errorcode); } if (count($expecteddebuggingstrings)) { $this->assertdebuggingcalledcount(count($expecteddebuggingstrings), $expecteddebuggingstrings); } } /** * Data provider for test cases related to sending user notifications. * * This data provider generates various test cases for the `test_send_user_notifications` function. * Each test case consists of a user configuration, expected output strings, debugging messages, and assertions. * * @return array[] Array of test cases. */ public static function send_user_notifications_cases(): array { return [ [ // Create a user with an empty email address. [ 'email' => '', 'username' => 'testuser', ], "Sending messages to testuser (%d)\n" . " Failed to send post %d\n" . "Sent 0 messages with 1 failures\n" . "Failed to send emails for the user with ID %d" . " due to an empty email address. Skipping re-queuing of the task.\n", [ "Can not send email to user without email: %d", "Error calling message processor email", ], false, ], [ // Create a user with bounce threshold. [ 'email' => 'bounce@example.com', 'username' => 'bounceuser', ], "Sending messages to bounceuser (%d)\n" . " Failed to send post %d\n" . "Sent 0 messages with 1 failures\n", [ "email_to_user: User %d (%s) is over bounce threshold! Not sending.", "Error calling message processor email", ], true, [ 'email_bounce_count' => 20, 'email_send_count' => 20, ], ], ]; } } PK-B\&RRtests/mail_test.phpnu[. namespace mod_forum; use mod_forum_tests_generator_trait; use mod_forum_tests_cron_trait; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/forum/lib.php'); require_once(__DIR__ . '/cron_trait.php'); require_once(__DIR__ . '/generator_trait.php'); /** * The forum module mail generation tests. * * @package mod_forum * @category test * @copyright 2013 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * */ final class mail_test extends \advanced_testcase { // Make use of the cron tester trait. use mod_forum_tests_cron_trait; // Make use of the test generator trait. use mod_forum_tests_generator_trait; /** * @var \phpunit_message_sink */ protected $messagesink; /** * @var \phpunit_mailer_sink */ protected $mailsink; /** @var \phpunit_event_sink */ protected $eventsink; public function setUp(): void { global $CFG; parent::setUp(); // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); \mod_forum\subscriptions::reset_discussion_cache(); // Messaging is not compatible with transactions... $this->preventResetByRollback(); // Catch all messages. $this->messagesink = $this->redirectMessages(); $this->mailsink = $this->redirectEmails(); // Forcibly reduce the maxeditingtime to a second in the past to // ensure that messages are sent out. $CFG->maxeditingtime = -1; } public function tearDown(): void { // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); $this->messagesink->clear(); $this->messagesink->close(); unset($this->messagesink); $this->mailsink->clear(); $this->mailsink->close(); unset($this->mailsink); parent::tearDown(); } /** * Perform message inbound setup for the mod_forum reply handler. */ protected function helper_spoof_message_inbound_setup() { global $CFG, $DB; // Setup the default Inbound Message mailbox settings. $CFG->messageinbound_domain = 'example.com'; $CFG->messageinbound_enabled = true; // Must be no longer than 15 characters. $CFG->messageinbound_mailbox = 'moodlemoodle123'; $record = $DB->get_record('messageinbound_handlers', array('classname' => '\mod_forum\message\inbound\reply_handler')); $record->enabled = true; $record->id = $DB->update_record('messageinbound_handlers', $record); } public function test_cron_message_includes_courseid(): void { $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_FORCESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create two users enrolled in the course as students. list($author, $recipient) = $this->helper_create_users($course, 2); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); $expect = [ 'author' => (object) [ 'userid' => $author->id, 'messages' => 1, ], 'recipient' => (object) [ 'userid' => $recipient->id, 'messages' => 1, ], ]; $this->queue_tasks_and_assert($expect); $this->messagesink->close(); $this->eventsink = $this->redirectEvents(); $this->send_notifications_and_assert($author, [$post]); $events = $this->eventsink->get_events(); $event = reset($events); $this->assertEquals($course->id, $event->other['courseid']); $this->send_notifications_and_assert($recipient, [$post]); } public function test_forced_subscription(): void { global $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_FORCESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create users enrolled in the course as students. list($author, $recipient, $unconfirmed, $deleted) = $this->helper_create_users($course, 4); // Make the third user unconfirmed (thence inactive) to make sure it does not break the notifications. $DB->set_field('user', 'confirmed', 0, ['id' => $unconfirmed->id]); // Mark the fourth user as deleted to make sure it does not break the notifications. $DB->set_field('user', 'deleted', 1, ['id' => $deleted->id]); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); $expect = [ (object) [ 'userid' => $author->id, 'messages' => 1, ], (object) [ 'userid' => $recipient->id, 'messages' => 1, ], (object) [ 'userid' => $unconfirmed->id, 'messages' => 0, ], (object) [ 'userid' => $deleted->id, 'messages' => 0, ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($author, [$post]); $this->send_notifications_and_assert($recipient, [$post]); $this->send_notifications_and_assert($unconfirmed, []); $this->send_notifications_and_assert($deleted, []); } /** * Ensure that for a forum with subscription disabled that standard users will not receive posts. */ public function test_subscription_disabled_standard_users(): void { global $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_DISALLOWSUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create two users enrolled in the course as students. list($author, $recipient) = $this->helper_create_users($course, 2); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); // Run cron and check that the expected number of users received the notification. $expect = [ (object) [ 'userid' => $author->id, 'messages' => 0, ], (object) [ 'userid' => $recipient->id, 'messages' => 0, ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($author, []); $this->send_notifications_and_assert($recipient, []); } /** * Ensure that for a forum with subscription disabled that a user subscribed to the forum will receive the post. */ public function test_subscription_disabled_user_subscribed_forum(): void { global $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_DISALLOWSUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create two users enrolled in the course as students. list($author, $recipient) = $this->helper_create_users($course, 2); // A user with the manageactivities capability within the course can subscribe. $roleids = $DB->get_records_menu('role', null, '', 'shortname, id'); assign_capability('moodle/course:manageactivities', CAP_ALLOW, $roleids['student'], \context_course::instance($course->id)); // Suscribe the recipient only. \mod_forum\subscriptions::subscribe_user($recipient->id, $forum); $this->assertEquals(1, $DB->count_records('forum_subscriptions', array( 'userid' => $recipient->id, 'forum' => $forum->id, ))); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); // Run cron and check that the expected number of users received the notification. $expect = [ 'author' => (object) [ 'userid' => $author->id, ], 'recipient' => (object) [ 'userid' => $recipient->id, 'messages' => 1, ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($author, []); $this->send_notifications_and_assert($recipient, [$post]); } /** * Ensure that for a forum with subscription disabled that a user subscribed to the discussion will receive the * post. */ public function test_subscription_disabled_user_subscribed_discussion(): void { global $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_DISALLOWSUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create two users enrolled in the course as students. list($author, $recipient) = $this->helper_create_users($course, 2); // A user with the manageactivities capability within the course can subscribe. $roleids = $DB->get_records_menu('role', null, '', 'shortname, id'); assign_capability('moodle/course:manageactivities', CAP_ALLOW, $roleids['student'], \context_course::instance($course->id)); // Run cron and check that the expected number of users received the notification. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); // Subscribe the user to the discussion. \mod_forum\subscriptions::subscribe_user_to_discussion($recipient->id, $discussion); $this->helper_update_subscription_time($recipient, $discussion, -60); // Run cron and check that the expected number of users received the notification. $expect = [ 'author' => (object) [ 'userid' => $author->id, ], 'recipient' => (object) [ 'userid' => $recipient->id, 'messages' => 1, ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($author, []); $this->send_notifications_and_assert($recipient, [$post]); } /** * Ensure that for a forum with automatic subscription that users receive posts. */ public function test_automatic(): void { $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create two users enrolled in the course as students. list($author, $recipient) = $this->helper_create_users($course, 2); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); $expect = [ (object) [ 'userid' => $author->id, 'messages' => 1, ], (object) [ 'userid' => $recipient->id, 'messages' => 1, ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($author, [$post]); $this->send_notifications_and_assert($recipient, [$post]); } /** * Ensure that private replies are not sent to users with an automatic subscription unless they are an expected * recipient. */ public function test_automatic_with_private_reply(): void { $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE, ]); [$student, $otherstudent] = $this->helper_create_users($course, 2, 'student'); [$teacher, $otherteacher] = $this->helper_create_users($course, 2, 'teacher'); [$discussion, $post] = $this->helper_post_to_forum($forum, $student); $reply = $this->helper_post_to_discussion($forum, $discussion, $teacher, [ 'privatereplyto' => $student->id, ]); // The private reply is queued to all messages as reply visibility may change between queueing, and sending. $expect = [ (object) [ 'userid' => $student->id, 'messages' => 2, ], (object) [ 'userid' => $otherstudent->id, 'messages' => 2, ], (object) [ 'userid' => $teacher->id, 'messages' => 2, ], (object) [ 'userid' => $otherteacher->id, 'messages' => 2, ], ]; $this->queue_tasks_and_assert($expect); // The actual messages sent will respect private replies. $this->send_notifications_and_assert($student, [$post, $reply]); $this->send_notifications_and_assert($teacher, [$post, $reply]); $this->send_notifications_and_assert($otherteacher, [$post, $reply]); $this->send_notifications_and_assert($otherstudent, [$post]); } public function test_optional(): void { $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create two users enrolled in the course as students. list($author, $recipient) = $this->helper_create_users($course, 2); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); $expect = [ (object) [ 'userid' => $author->id, 'messages' => 0, ], (object) [ 'userid' => $recipient->id, 'messages' => 0, ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($author, []); $this->send_notifications_and_assert($recipient, []); } public function test_automatic_with_unsubscribed_user(): void { $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create two users enrolled in the course as students. list($author, $recipient) = $this->helper_create_users($course, 2); // Unsubscribe the 'author' user from the forum. \mod_forum\subscriptions::unsubscribe_user($author->id, $forum); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); $expect = [ (object) [ 'userid' => $author->id, 'messages' => 0, ], (object) [ 'userid' => $recipient->id, 'messages' => 1, ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($author, []); $this->send_notifications_and_assert($recipient, [$post]); } public function test_optional_with_subscribed_user(): void { $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create two users enrolled in the course as students. list($author, $recipient) = $this->helper_create_users($course, 2); // Subscribe the 'recipient' user from the forum. \mod_forum\subscriptions::subscribe_user($recipient->id, $forum); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); $expect = [ (object) [ 'userid' => $author->id, 'messages' => 0, ], (object) [ 'userid' => $recipient->id, 'messages' => 1, ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($author, []); $this->send_notifications_and_assert($recipient, [$post]); } public function test_automatic_with_unsubscribed_discussion(): void { $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create two users enrolled in the course as students. list($author, $recipient) = $this->helper_create_users($course, 2); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); // Unsubscribe the 'author' user from the discussion. \mod_forum\subscriptions::unsubscribe_user_from_discussion($author->id, $discussion); $this->assertFalse(\mod_forum\subscriptions::is_subscribed($author->id, $forum, $discussion->id)); $this->assertTrue(\mod_forum\subscriptions::is_subscribed($recipient->id, $forum, $discussion->id)); $expect = [ (object) [ 'userid' => $author->id, 'messages' => 0, ], (object) [ 'userid' => $recipient->id, 'messages' => 1, ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($author, []); $this->send_notifications_and_assert($recipient, [$post]); } public function test_optional_with_subscribed_discussion(): void { $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create two users enrolled in the course as students. list($author, $recipient) = $this->helper_create_users($course, 2); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); $this->helper_update_post_time($post, -90); // Subscribe the 'recipient' user to the discussion. \mod_forum\subscriptions::subscribe_user_to_discussion($recipient->id, $discussion); $this->helper_update_subscription_time($recipient, $discussion, -60); // Initially we don't expect any user to receive this post as you cannot subscribe to a discussion until after // you have read it. $expect = [ (object) [ 'userid' => $author->id, 'messages' => 0, ], (object) [ 'userid' => $recipient->id, 'messages' => 0, ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($author, []); $this->send_notifications_and_assert($recipient, []); // Have a user reply to the discussion. $reply = $this->helper_post_to_discussion($forum, $discussion, $author); $this->helper_update_post_time($reply, -30); // We expect only one user to receive this post. $expect = [ (object) [ 'userid' => $author->id, 'messages' => 0, ], (object) [ 'userid' => $recipient->id, 'messages' => 1, ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($author, []); $this->send_notifications_and_assert($recipient, [$reply]); } public function test_optional_with_subscribed_discussion_and_post(): void { $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create two users enrolled in the course as students. list($author, $recipient) = $this->helper_create_users($course, 2); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); $this->helper_update_post_time($post, -90); // Have a user reply to the discussion before we subscribed. $reply = $this->helper_post_to_discussion($forum, $discussion, $author); $this->helper_update_post_time($reply, -75); // Subscribe the 'recipient' user to the discussion. \mod_forum\subscriptions::subscribe_user_to_discussion($recipient->id, $discussion); $this->helper_update_subscription_time($recipient, $discussion, -60); // Have a user reply to the discussion. $reply = $this->helper_post_to_discussion($forum, $discussion, $author); $this->helper_update_post_time($reply, -30); // We expect only one user to receive this post. // The original post won't be received as it was written before the user subscribed. $expect = [ (object) [ 'userid' => $author->id, 'messages' => 0, ], (object) [ 'userid' => $recipient->id, 'messages' => 1, ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($author, []); $this->send_notifications_and_assert($recipient, [$reply]); } public function test_automatic_with_subscribed_discussion_in_unsubscribed_forum(): void { $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create two users enrolled in the course as students. list($author, $recipient) = $this->helper_create_users($course, 2); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); $this->helper_update_post_time($post, -90); // Unsubscribe the 'author' user from the forum. \mod_forum\subscriptions::unsubscribe_user($author->id, $forum); // Then re-subscribe them to the discussion. \mod_forum\subscriptions::subscribe_user_to_discussion($author->id, $discussion); $this->helper_update_subscription_time($author, $discussion, -60); $expect = [ (object) [ 'userid' => $author->id, 'messages' => 0, ], (object) [ 'userid' => $recipient->id, 'messages' => 1, ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($author, []); $this->send_notifications_and_assert($recipient, [$post]); // Now post a reply to the original post. $reply = $this->helper_post_to_discussion($forum, $discussion, $author); $this->helper_update_post_time($reply, -30); $expect = [ (object) [ 'userid' => $author->id, 'messages' => 1, ], (object) [ 'userid' => $recipient->id, 'messages' => 1, ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($author, [$reply]); $this->send_notifications_and_assert($recipient, [$reply]); } public function test_optional_with_unsubscribed_discussion_in_subscribed_forum(): void { $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create two users enrolled in the course as students. list($author, $recipient) = $this->helper_create_users($course, 2); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); // Unsubscribe the 'recipient' user from the discussion. \mod_forum\subscriptions::subscribe_user($recipient->id, $forum); // Then unsubscribe them from the discussion. \mod_forum\subscriptions::unsubscribe_user_from_discussion($recipient->id, $discussion); // We don't expect any users to receive this post. $expect = [ (object) [ 'userid' => $author->id, 'messages' => 0, ], (object) [ 'userid' => $recipient->id, 'messages' => 0, ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($author, []); $this->send_notifications_and_assert($recipient, []); } /** * Test that a user unsubscribed from a forum who has subscribed to a discussion, only receives posts made after * they subscribed to the discussion. */ public function test_forum_discussion_subscription_forum_unsubscribed_discussion_subscribed_after_post(): void { $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); $expectedmessages = array(); // Create a user enrolled in the course as a student. list($author) = $this->helper_create_users($course, 1); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); $this->helper_update_post_time($post, -90); $expectedmessages[] = array( 'id' => $post->id, 'subject' => $post->subject, 'count' => 0, ); // Then subscribe the user to the discussion. $this->assertTrue(\mod_forum\subscriptions::subscribe_user_to_discussion($author->id, $discussion)); $this->helper_update_subscription_time($author, $discussion, -60); // Then post a reply to the first discussion. $reply = $this->helper_post_to_discussion($forum, $discussion, $author); $this->helper_update_post_time($reply, -30); $expect = [ (object) [ 'userid' => $author->id, 'messages' => 1, ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($author, [$reply]); } public function test_subscription_by_inactive_users(): void { global $DB; $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_CHOOSESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create two users enrolled in the course as students. list($author, $u1, $u2, $u3) = $this->helper_create_users($course, 4); // Subscribe the three users to the forum. \mod_forum\subscriptions::subscribe_user($u1->id, $forum); \mod_forum\subscriptions::subscribe_user($u2->id, $forum); \mod_forum\subscriptions::subscribe_user($u3->id, $forum); // Make the first user inactive - suspended. $DB->set_field('user', 'suspended', 1, ['id' => $u1->id]); // Make the second user inactive - unable to log in. $DB->set_field('user', 'auth', 'nologin', ['id' => $u2->id]); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); $expect = [ (object) [ 'userid' => $u1->id, 'messages' => 0, ], (object) [ 'userid' => $u2->id, 'messages' => 0, ], (object) [ 'userid' => $u3->id, 'messages' => 1, ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($u1, []); $this->send_notifications_and_assert($u2, []); $this->send_notifications_and_assert($u3, [$post]); } public function test_forum_message_inbound_multiple_posts(): void { $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_FORCESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create a user enrolled in the course as a student. list($author) = $this->helper_create_users($course, 1); $expectedmessages = array(); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); $this->helper_update_post_time($post, -90); $expectedmessages[] = (object) [ 'id' => $post->id, 'subject' => $post->subject, 'count' => 0, ]; // Then post a reply to the first discussion. $reply = $this->helper_post_to_discussion($forum, $discussion, $author); $this->helper_update_post_time($reply, -60); $expectedmessages[] = (object) [ 'id' => $reply->id, 'subject' => $reply->subject, 'count' => 1, ]; // Ensure that messageinbound is enabled and configured for the forum handler. $this->helper_spoof_message_inbound_setup(); $author->emailstop = '0'; set_user_preference('message_provider_mod_forum_posts_enabled', 'email', $author); // Run cron and check that the expected number of users received the notification. // Clear the mailsink, and close the messagesink. $this->mailsink->clear(); $this->messagesink->close(); $expect = [ 'author' => (object) [ 'userid' => $author->id, 'messages' => count($expectedmessages), ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($author, $expectedmessages); $messages = $this->mailsink->get_messages(); // There should be the expected number of messages. $this->assertEquals(2, count($messages)); foreach ($messages as $message) { $this->assertMatchesRegularExpression('/Reply-To: moodlemoodle123\+[^@]*@example.com/', $message->header); } } public function test_long_subject(): void { $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_FORCESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create a user enrolled in the course as student. list($author) = $this->helper_create_users($course, 1); // Post a discussion to the forum. $subject = 'This is the very long forum post subject that somebody was very kind of leaving, it is intended to check if long subject comes in mail correctly. Thank you.'; $a = (object)array('courseshortname' => $course->shortname, 'forumname' => $forum->name, 'subject' => $subject); $expectedsubject = get_string('postmailsubject', 'forum', $a); list($discussion, $post) = $this->helper_post_to_forum($forum, $author, array('name' => $subject)); // Run cron and check that the expected number of users received the notification. $expect = [ 'author' => (object) [ 'userid' => $author->id, 'messages' => 1, ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($author, [$post]); $messages = $this->messagesink->get_messages_by_component('mod_forum'); $message = reset($messages); $this->assertEquals($author->id, $message->useridfrom); $this->assertEquals($expectedsubject, $message->subject); } /** * dataProvider for test_forum_post_email_templates(). */ public static function forum_post_email_templates_provider(): array { // Base information, we'll build variations based on it. $base = array( 'user' => array('firstname' => 'Love', 'lastname' => 'Moodle', 'mailformat' => 0, 'maildigest' => 0), 'course' => array('shortname' => '101', 'fullname' => 'Moodle 101'), 'forums' => array( array( 'name' => 'Moodle Forum', 'forumposts' => array( array( 'name' => 'Hello Moodle', 'message' => 'Welcome to Moodle', 'messageformat' => FORMAT_MOODLE, 'attachments' => array( array( 'filename' => 'example.txt', 'filecontents' => 'Basic information about the course' ), ), ), ), ), ), 'expectations' => array( array( 'subject' => '.*101.*Hello', 'contents' => array( '~{$a', '~&(amp|lt|gt|quot|\#039);(?!course)', 'Attachment example.txt:' . '\r*\n' . 'https://www.example.com/moodle/pluginfile.php/\d*/mod_forum/attachment/\d*/example.txt' . '\r*\n', 'Hello Moodle', 'Moodle Forum', 'Welcome.*Moodle', 'Love Moodle', '1\d1' ), ), ), ); // Build the text cases. $textcases = array('Text mail without ampersands, quotes or lt/gt' => array('data' => $base)); // Single and double quotes everywhere. $newcase = $base; $newcase['user']['lastname'] = 'Moodle\'"'; $newcase['course']['shortname'] = '101\'"'; $newcase['forums'][0]['name'] = 'Moodle Forum\'"'; $newcase['forums'][0]['forumposts'][0]['name'] = 'Hello Moodle\'"'; $newcase['forums'][0]['forumposts'][0]['message'] = 'Welcome to Moodle\'"'; $newcase['expectations'][0]['contents'] = array( 'Attachment example.txt:', '~{\$a', '~&(quot|\#039);', 'Love Moodle\'', '101\'', 'Moodle Forum\'"', 'Hello Moodle\'"', 'Welcome to Moodle\'"'); $textcases['Text mail with quotes everywhere'] = array('data' => $newcase); // Lt and gt everywhere. This case is completely borked because format_string() // strips tags with $CFG->formatstringstriptags and also escapes < and > (correct // for web presentation but not for text email). See MDL-19829. $newcase = $base; $newcase['user']['lastname'] = 'Moodle>'; $newcase['course']['shortname'] = '101>'; $newcase['forums'][0]['name'] = 'Moodle Forum>'; $newcase['forums'][0]['forumposts'][0]['name'] = 'Hello Moodle>'; $newcase['forums'][0]['forumposts'][0]['message'] = 'Welcome to Moodle>'; $newcase['expectations'][0]['contents'] = array( 'Attachment example.txt:', '~{\$a', '~&gt;', 'Love Moodle>', '101>', 'Moodle Forum>', 'Hello Moodle>', 'Welcome to Moodle>'); $textcases['Text mail with gt and lt everywhere'] = array('data' => $newcase); // Ampersands everywhere. This case is completely borked because format_string() // escapes ampersands (correct for web presentation but not for text email). See MDL-19829. $newcase = $base; $newcase['user']['lastname'] = 'Moodle&'; $newcase['course']['shortname'] = '101&'; $newcase['forums'][0]['name'] = 'Moodle Forum&'; $newcase['forums'][0]['forumposts'][0]['name'] = 'Hello Moodle&'; $newcase['forums'][0]['forumposts'][0]['message'] = 'Welcome to Moodle&'; $newcase['expectations'][0]['contents'] = array( 'Attachment example.txt:', '~{\$a', '~&amp;', 'Love Moodle&', '101&', 'Moodle Forum&', 'Hello Moodle&', 'Welcome to Moodle&'); $textcases['Text mail with ampersands everywhere'] = array('data' => $newcase); // Text+image message i.e. @@PLUGINFILE@@ token handling. $newcase = $base; $newcase['forums'][0]['forumposts'][0]['name'] = 'Text and image'; $newcase['forums'][0]['forumposts'][0]['message'] = 'Welcome to Moodle, ' .'@@PLUGINFILE@@/Screen%20Shot%202016-03-22%20at%205.54.36%20AM%20%281%29.png !'; $newcase['expectations'][0]['subject'] = '.*101.*Text and image'; $newcase['expectations'][0]['contents'] = array( '~{$a', '~&(amp|lt|gt|quot|\#039);(?!course)', 'Attachment example.txt:' . '\r*\n' . 'https://www.example.com/moodle/pluginfile.php/\d*/mod_forum/attachment/\d*/example.txt' . '\r*\n' , 'Text and image', 'Moodle Forum', 'Welcome to Moodle, *' . '\r*\n' . '.*' .'https://www.example.com/moodle/pluginfile.php/\d+/mod_forum/post/\d+/' .'Screen%20Shot%202016-03-22%20at%205\.54\.36%20AM%20%281%29\.png *' . '\r*\n' . '.*!', 'Love Moodle', '1\d1'); $textcases['Text mail with text+image message i.e. @@PLUGINFILE@@ token handling'] = array('data' => $newcase); // Now the html cases. $htmlcases = array(); // New base for html cases, no quotes, lts, gts or ampersands. $htmlbase = $base; $htmlbase['user']['mailformat'] = 1; $htmlbase['expectations'][0]['contents'] = array( '~{\$a', '~&(amp|lt|gt|quot|\#039);(?!course|lang|version|iosappid|androidappid)', '
( *\n *)?\n.*Hello Moodle', '>Moodle Forum', '>Welcome.*Moodle', '>Love Moodle', '>1\d1'); $htmlcases['HTML mail without ampersands, quotes or lt/gt'] = array('data' => $htmlbase); // Single and double quotes, lt and gt, ampersands everywhere. $newcase = $htmlbase; $newcase['user']['lastname'] = 'Moodle\'">&'; $newcase['course']['shortname'] = '101\'">&'; $newcase['forums'][0]['name'] = 'Moodle Forum\'">&'; $newcase['forums'][0]['forumposts'][0]['name'] = 'Hello Moodle\'">&'; $newcase['forums'][0]['forumposts'][0]['message'] = 'Welcome to Moodle\'">&'; $newcase['expectations'][0]['contents'] = array( '~{\$a', '~&(amp|lt|gt|quot|\#039);', '
( *\n *)?\n.*Hello Moodle\'">&', '>Moodle Forum\'">&', '>Welcome.*Moodle\'">&', '>Love Moodle&\#039;">&', '>101\'">&'); $htmlcases['HTML mail with quotes, gt, lt and ampersand everywhere'] = array('data' => $newcase); // Text+image message i.e. @@PLUGINFILE@@ token handling. $newcase = $htmlbase; $newcase['forums'][0]['forumposts'][0]['name'] = 'HTML text and image'; $newcase['forums'][0]['forumposts'][0]['message'] = '

Welcome to Moodle, ' .'!

'; $newcase['expectations'][0]['subject'] = '.*101.*HTML text and image'; $newcase['expectations'][0]['contents'] = array( '~{\$a', '~&(amp|lt|gt|quot|\#039);(?!course|lang|version|iosappid|androidappid)', '
( *\n *)?\n.*HTML text and image', '>Moodle Forum', '

Welcome to Moodle, ' .'!

', '>Love Moodle', '>1\d1'); $htmlcases['HTML mail with text+image message i.e. @@PLUGINFILE@@ token handling'] = array('data' => $newcase); return $textcases + $htmlcases; } /** * Verify forum emails body using templates to generate the expected results. * * @dataProvider forum_post_email_templates_provider * @param array $data provider samples. */ public function test_forum_post_email_templates($data): void { global $DB; $this->resetAfterTest(); // Create the course, with the specified options. $options = array(); foreach ($data['course'] as $option => $value) { $options[$option] = $value; } $course = $this->getDataGenerator()->create_course($options); // Create the user, with the specified options and enrol in the course. $options = array(); foreach ($data['user'] as $option => $value) { $options[$option] = $value; } $user = $this->getDataGenerator()->create_user($options); $this->getDataGenerator()->enrol_user($user->id, $course->id); // Create forums, always force susbscribed (for easy), with the specified options. $posts = array(); foreach ($data['forums'] as $dataforum) { $forumposts = isset($dataforum['forumposts']) ? $dataforum['forumposts'] : array(); unset($dataforum['forumposts']); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_FORCESUBSCRIBE); foreach ($dataforum as $option => $value) { $options[$option] = $value; } $forum = $this->getDataGenerator()->create_module('forum', $options); // Create posts, always for immediate delivery (for easy), with the specified options. foreach ($forumposts as $forumpost) { $attachments = isset($forumpost['attachments']) ? $forumpost['attachments'] : array(); unset($forumpost['attachments']); $postoptions = array('course' => $course->id, 'forum' => $forum->id, 'userid' => $user->id, 'mailnow' => 1, 'attachment' => !empty($attachments)); foreach ($forumpost as $option => $value) { $postoptions[$option] = $value; } list($discussion, $post) = $this->helper_post_to_forum($forum, $user, $postoptions); $posts[$post->subject] = $post; // Need this to verify cron output. // Add the attachments to the post. if ($attachments) { $fs = get_file_storage(); foreach ($attachments as $attachment) { $filerecord = array( 'contextid' => \context_module::instance($forum->cmid)->id, 'component' => 'mod_forum', 'filearea' => 'attachment', 'itemid' => $post->id, 'filepath' => '/', 'filename' => $attachment['filename'] ); $fs->create_file_from_string($filerecord, $attachment['filecontents']); } $DB->set_field('forum_posts', 'attachment', '1', array('id' => $post->id)); } } } // Clear the mailsink and close the messagesink. // (surely setup should provide us this cleared but...) $this->mailsink->clear(); $this->messagesink->close(); $expect = [ 'author' => (object) [ 'userid' => $user->id, 'messages' => count($posts), ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($user, $posts); // Get the mails. $mails = $this->mailsink->get_messages(); // Start testing the expectations. $expectations = $data['expectations']; // Assert the number is the expected. $this->assertSame(count($expectations), count($mails)); // Start processing mails, first localizing its expectations, then checking them. foreach ($mails as $mail) { // Find the corresponding expectation. $foundexpectation = null; foreach ($expectations as $key => $expectation) { // All expectations must have a subject for matching. if (!isset($expectation['subject'])) { $this->fail('Provider expectation missing mandatory subject'); } if (preg_match('!' . $expectation['subject'] . '!', $mail->subject)) { // If we already had found the expectation, there are non-unique subjects. Fail. if (isset($foundexpectation)) { $this->fail('Multiple expectations found (by subject matching). Please make them unique.'); } $foundexpectation = $expectation; unset($expectations[$key]); } } // Arrived here, we should have found the expectations. $this->assertNotEmpty($foundexpectation, 'Expectation not found for the mail'); // If we have found the expectation and have contents to match, let's do it. if (isset($foundexpectation) and isset($foundexpectation['contents'])) { $mail->body = quoted_printable_decode($mail->body); if (!is_array($foundexpectation['contents'])) { // Accept both string and array. $foundexpectation['contents'] = array($foundexpectation['contents']); } foreach ($foundexpectation['contents'] as $content) { if (strpos($content, '~') !== 0) { $this->assertMatchesRegularExpression('#' . $content . '#m', $mail->body); } else { preg_match('#' . substr($content, 1) . '#m', $mail->body, $matches); $this->assertDoesNotMatchRegularExpression('#' . substr($content, 1) . '#m', $mail->body); } } } } // Finished, there should not be remaining expectations. $this->assertCount(0, $expectations); } /** * Ensure that posts already mailed are not re-sent. */ public function test_already_mailed(): void { global $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create two users enrolled in the course as students. list($author, $recipient) = $this->helper_create_users($course, 2); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); $DB->set_field('forum_posts', 'mailed', 1); // No posts shoudl be considered. $this->queue_tasks_and_assert([]); // No notifications should be queued. $this->send_notifications_and_assert($author, []); $this->send_notifications_and_assert($recipient, []); } /** * Ensure that posts marked mailnow are not suspect to the maxeditingtime. */ public function test_mailnow(): void { global $CFG, $DB; // Update the maxeditingtime to 1 day so that posts won't be sent. $CFG->maxeditingtime = DAYSECS; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create two users enrolled in the course as students. list($author, $recipient) = $this->helper_create_users($course, 2); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); // Post a discussion to the forum. list($discussion, $postmailednow) = $this->helper_post_to_forum($forum, $author, ['mailnow' => 1]); // Only the mailnow post should be considered. $expect = [ 'author' => (object) [ 'userid' => $author->id, 'messages' => 1, ], 'recipient' => (object) [ 'userid' => $recipient->id, 'messages' => 1, ], ]; $this->queue_tasks_and_assert($expect); // No notifications should be queued. $this->send_notifications_and_assert($author, [$postmailednow]); $this->send_notifications_and_assert($recipient, [$postmailednow]); } /** * Ensure that if a user has no permission to view a post, then it is not sent. */ public function test_access_coursemodule_hidden(): void { global $CFG, $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create two users enrolled in the course as students. list($author, $recipient) = $this->helper_create_users($course, 2); // Create one users enrolled in the course as an editing teacher. list($editor) = $this->helper_create_users($course, 1, 'editingteacher'); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); // Hide the coursemodule. set_coursemodule_visible($forum->cmid, 0); // Only the mailnow post should be considered. $expect = [ 'author' => (object) [ 'userid' => $author->id, 'messages' => 1, ], 'recipient' => (object) [ 'userid' => $recipient->id, 'messages' => 1, ], 'editor' => (object) [ 'userid' => $editor->id, 'messages' => 1, ], ]; $this->queue_tasks_and_assert($expect); // No notifications should be queued. $this->send_notifications_and_assert($author, [], true); $this->send_notifications_and_assert($recipient, [], true); $this->send_notifications_and_assert($editor, [$post], true); } /** * Ensure that if a user loses permission to view a post after it is queued, that it is not sent. */ public function test_access_coursemodule_hidden_after_queue(): void { global $CFG, $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create two users enrolled in the course as students. list($author, $recipient) = $this->helper_create_users($course, 2); // Create one users enrolled in the course as an editing teacher. list($editor) = $this->helper_create_users($course, 1, 'editingteacher'); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); // Only the mailnow post should be considered. $expect = [ 'author' => (object) [ 'userid' => $author->id, 'messages' => 1, ], 'recipient' => (object) [ 'userid' => $recipient->id, 'messages' => 1, ], 'editor' => (object) [ 'userid' => $editor->id, 'messages' => 1, ], ]; $this->queue_tasks_and_assert($expect); // Hide the coursemodule. set_coursemodule_visible($forum->cmid, 0); // No notifications should be queued for the students. $this->send_notifications_and_assert($author, [], true); $this->send_notifications_and_assert($recipient, [], true); // The editing teacher should still receive the post. $this->send_notifications_and_assert($editor, [$post]); } /** * Ensure that messages are not sent until the timestart. */ public function test_access_before_timestart(): void { global $CFG, $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create two users enrolled in the course as students. list($author, $recipient) = $this->helper_create_users($course, 2); // Create one users enrolled in the course as an editing teacher. list($editor) = $this->helper_create_users($course, 1, 'editingteacher'); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); // Update the discussion to have a timestart in the future. $DB->set_field('forum_discussions', 'timestart', time() + DAYSECS); // None should be sent. $this->queue_tasks_and_assert([]); // No notifications should be queued for any user. $this->send_notifications_and_assert($author, []); $this->send_notifications_and_assert($recipient, []); $this->send_notifications_and_assert($editor, []); // Update the discussion to have a timestart in the past. $DB->set_field('forum_discussions', 'timestart', time() - DAYSECS); // Now should be sent to all. $expect = [ 'author' => (object) [ 'userid' => $author->id, 'messages' => 1, ], 'recipient' => (object) [ 'userid' => $recipient->id, 'messages' => 1, ], 'editor' => (object) [ 'userid' => $editor->id, 'messages' => 1, ], ]; $this->queue_tasks_and_assert($expect); // No notifications should be queued for any user. $this->send_notifications_and_assert($author, [$post]); $this->send_notifications_and_assert($recipient, [$post]); $this->send_notifications_and_assert($editor, [$post]); } /** * Ensure that messages are not sent after the timeend. */ public function test_access_after_timeend(): void { global $CFG, $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); // Create two users enrolled in the course as students. list($author, $recipient) = $this->helper_create_users($course, 2); // Create one users enrolled in the course as an editing teacher. list($editor) = $this->helper_create_users($course, 1, 'editingteacher'); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); // Update the discussion to have a timestart in the past. $DB->set_field('forum_discussions', 'timeend', time() - DAYSECS); // None should be sent. $this->queue_tasks_and_assert([]); // No notifications should be queued for any user. $this->send_notifications_and_assert($author, []); $this->send_notifications_and_assert($recipient, []); $this->send_notifications_and_assert($editor, []); // Update the discussion to have a timestart in the past. $DB->set_field('forum_discussions', 'timeend', time() + DAYSECS); // Now should be sent to all. $expect = [ 'author' => (object) [ 'userid' => $author->id, 'messages' => 1, ], 'recipient' => (object) [ 'userid' => $recipient->id, 'messages' => 1, ], 'editor' => (object) [ 'userid' => $editor->id, 'messages' => 1, ], ]; $this->queue_tasks_and_assert($expect); // No notifications should be queued for any user. $this->send_notifications_and_assert($author, [$post]); $this->send_notifications_and_assert($recipient, [$post]); $this->send_notifications_and_assert($editor, [$post]); } /** * Test notification comes with customdata. */ public function test_notification_customdata(): void { $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $options = array('course' => $course->id, 'forcesubscribe' => FORUM_FORCESUBSCRIBE); $forum = $this->getDataGenerator()->create_module('forum', $options); list($author) = $this->helper_create_users($course, 1); list($commenter) = $this->helper_create_users($course, 1); // New posts should not have Re: in the subject. list($discussion, $post) = $this->helper_post_to_forum($forum, $author); $expect = [ 'author' => (object) [ 'userid' => $author->id, 'messages' => 1, ], 'commenter' => (object) [ 'userid' => $commenter->id, 'messages' => 1, ], ]; $this->queue_tasks_and_assert($expect); $this->send_notifications_and_assert($author, [$post]); $this->send_notifications_and_assert($commenter, [$post]); $messages = $this->messagesink->get_messages_by_component('mod_forum'); $messages = reset($messages); $customdata = json_decode($messages->customdata); $this->assertEquals($forum->id, $customdata->instance); $this->assertEquals($forum->cmid, $customdata->cmid); $this->assertEquals($post->id, $customdata->postid); $this->assertEquals($discussion->id, $customdata->discussionid); $this->assertObjectHasProperty('notificationiconurl', $customdata); $this->assertObjectHasProperty('actionbuttons', $customdata); $this->assertCount(1, (array) $customdata->actionbuttons); } } PK-B\0  tests/exporters_forum_test.phpnu[. namespace mod_forum; use mod_forum\local\entities\forum as forum_entity; use mod_forum\local\exporters\forum as forum_exporter; /** * The discussion forum tests. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class exporters_forum_test extends \advanced_testcase { /** * Test the export function returns expected values. */ public function test_export(): void { global $PAGE; $this->resetAfterTest(); $renderer = $PAGE->get_renderer('core'); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', [ 'course' => $course->id, 'groupmode' => VISIBLEGROUPS ]); $coursemodule = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($coursemodule->id); $entityfactory = \mod_forum\local\container::get_entity_factory(); $forum = $entityfactory->get_forum_from_stdClass($forum, $context, $coursemodule, $course); $exporter = new forum_exporter($forum, [ 'legacydatamapperfactory' => \mod_forum\local\container::get_legacy_data_mapper_factory(), 'urlfactory' => \mod_forum\local\container::get_url_factory(), 'capabilitymanager' => (\mod_forum\local\container::get_manager_factory())->get_capability_manager($forum), 'user' => $user, 'currentgroup' => null, 'vaultfactory' => \mod_forum\local\container::get_vault_factory() ]); $exportedforum = $exporter->export($renderer); $this->assertEquals($forum->get_id(), $exportedforum->id); $this->assertEquals(VISIBLEGROUPS, $exportedforum->state['groupmode']); $this->assertEquals(false, $exportedforum->userstate['tracked']); $this->assertEquals(false, $exportedforum->capabilities['viewdiscussions']); $this->assertEquals(false, $exportedforum->capabilities['create']); $this->assertEquals(false, $exportedforum->capabilities['subscribe']); $this->assertNotEquals(null, $exportedforum->urls['create']); $this->assertNotEquals(null, $exportedforum->urls['markasread']); } } PK-B\D-- tests/vaults_discussion_test.phpnu[. namespace mod_forum; use mod_forum_tests_generator_trait; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/generator_trait.php'); /** * The discussion vault tests. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class vaults_discussion_test extends \advanced_testcase { // Make use of the test generator trait. use mod_forum_tests_generator_trait; /** @var \mod_forum\local\vaults\discussion */ private $vault; /** * Set up function for tests. */ public function setUp(): void { parent::setUp(); $vaultfactory = \mod_forum\local\container::get_vault_factory(); $this->vault = $vaultfactory->get_discussion_vault(); } /** * Test get_from_id. */ public function test_get_from_id(): void { $this->resetAfterTest(); $vault = $this->vault; $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); [$discussionrecord, $post] = $this->helper_post_to_forum($forum, $user); $discussion = $vault->get_from_id($discussionrecord->id); $this->assertEquals($discussionrecord->id, $discussion->get_id()); } /** * Test get_first_discussion_in_forum. */ public function test_get_first_discussion_in_forum(): void { $this->resetAfterTest(); $vault = $this->vault; $entityfactory = \mod_forum\local\container::get_entity_factory(); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); $coursemodule = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($coursemodule->id); $forumentity = $entityfactory->get_forum_from_stdClass($forum, $context, $coursemodule, $course); $this->assertEquals(null, $vault->get_first_discussion_in_forum($forumentity)); [$discussion1, $post] = $this->helper_post_to_forum($forum, $user, ['timemodified' => 2]); [$discussion2, $post] = $this->helper_post_to_forum($forum, $user, ['timemodified' => 1]); [$discussion3, $post] = $this->helper_post_to_forum($forum, $user, ['timemodified' => 3]); $discussionentity = $vault->get_first_discussion_in_forum($forumentity); $this->assertEquals($discussion2->id, $discussionentity->get_id()); } /** * Test get_all_discussions_in_forum */ public function test_get_all_discussions_in_forum(): void { $this->resetAfterTest(); $vault = $this->vault; $entityfactory = \mod_forum\local\container::get_entity_factory(); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); $coursemodule = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($coursemodule->id); $forumentity = $entityfactory->get_forum_from_stdClass($forum, $context, $coursemodule, $course); $this->assertEquals([], $vault->get_all_discussions_in_forum($forumentity)); [$discussion1, $post] = $this->helper_post_to_forum($forum, $user, ['timemodified' => 2]); [$discussion2, $post] = $this->helper_post_to_forum($forum, $user, ['timemodified' => 1]); [$discussion3, $post] = $this->helper_post_to_forum($forum, $user, ['timemodified' => 3]); $discussionentity = $vault->get_all_discussions_in_forum($forumentity); $this->assertArrayHasKey($discussion1->id, $discussionentity); // Order is not guaranteed, so just verify element existence. $this->assertArrayHasKey($discussion2->id, $discussionentity); $this->assertArrayHasKey($discussion3->id, $discussionentity); } } PK-B\%2tests/vaults_post_read_receipt_collection_test.phpnu[. namespace mod_forum; use mod_forum_tests_generator_trait; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/generator_trait.php'); /** * The post read receipt collection vault tests. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class vaults_post_read_receipt_collection_test extends \advanced_testcase { // Make use of the test generator trait. use mod_forum_tests_generator_trait; /** * Test get_from_user_id_and_post_ids. */ public function test_get_from_user_id_and_post_ids(): void { global $DB; $this->resetAfterTest(); $entityfactory = \mod_forum\local\container::get_entity_factory(); $vaultfactory = \mod_forum\local\container::get_vault_factory(); $vault = $vaultfactory->get_post_read_receipt_collection_vault(); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $otheruser = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); [$discussion1, $post1] = $this->helper_post_to_forum($forum, $user); [$discussion2, $post2] = $this->helper_post_to_forum($forum, $user); $post3 = $this->helper_reply_to_post($post2, $user); $DB->insert_record('forum_read', [ 'userid' => $user->id, 'forumid' => $forum->id, 'discussionid' => $discussion1->id, 'postid' => $post1->id, 'firstread' => time(), 'lastread' => time() ]); // Receipt for other user. $DB->insert_record('forum_read', [ 'userid' => $otheruser->id, 'forumid' => $forum->id, 'discussionid' => $discussion1->id, 'postid' => $post1->id, 'firstread' => time(), 'lastread' => time() ]); $DB->insert_record('forum_read', [ 'userid' => $user->id, 'forumid' => $forum->id, 'discussionid' => $discussion2->id, 'postid' => $post3->id, 'firstread' => time(), 'lastread' => time() ]); $post1 = $entityfactory->get_post_from_stdClass($post1); $post2 = $entityfactory->get_post_from_stdClass($post2); $post3 = $entityfactory->get_post_from_stdClass($post3); $collection = $vault->get_from_user_id_and_post_ids($user->id, [$post1->get_id(), $post2->get_id()]); // True because there is a read receipt for this user. $this->assertTrue($collection->has_user_read_post($user, $post1)); // False because other user wasn't included in the fetch. $this->assertFalse($collection->has_user_read_post($otheruser, $post1)); // No read receipt. $this->assertFalse($collection->has_user_read_post($user, $post2)); // Wasn't included in fetch. $this->assertFalse($collection->has_user_read_post($user, $post3)); } } PK-B\Udy,y,$tests/grade/forum_gradeitem_test.phpnu[. /** * Tests for the the Forum gradeitem. * * @package mod_forum * @copyright Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\grade; use core_grades\component_gradeitem; use mod_forum\grades\forum_gradeitem as gradeitem; use mod_forum\local\entities\forum as forum_entity; use gradingform_controller; use mod_forum\grades\forum_gradeitem; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/../generator_trait.php'); /** * Tests for the the Forum gradeitem. * * @package mod_forum * @copyright Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class forum_gradeitem_test extends \advanced_testcase { use \mod_forum_tests_generator_trait; /** * Test fetching of a grade for a user when the grade has been created. */ public function test_get_grade_for_user_exists(): void { $forum = $this->get_forum_instance([ 'grade_forum' => 0, ]); $course = $forum->get_course_record(); [$student] = $this->helper_create_users($course, 1); [$grader] = $this->helper_create_users($course, 1, 'editingteacher'); $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); // Create the grade record. $grade = $gradeitem->create_empty_grade($student, $grader); $this->assertIsObject($grade); $this->assertEquals($student->id, $grade->userid); } /** * Test fetching of a grade for a user when the grade has been created. */ public function test_user_has_grade(): void { $forum = $this->get_forum_instance([ 'grade_forum' => 100, ]); $course = $forum->get_course_record(); [$student] = $this->helper_create_users($course, 1); [$grader] = $this->helper_create_users($course, 1, 'editingteacher'); $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); $hasgrade = $gradeitem->user_has_grade($student); $this->assertEquals(false, $hasgrade); // Create the grade record. $gradeitem->create_empty_grade($student, $grader); $hasgrade = $gradeitem->user_has_grade($student); $this->assertEquals(false, $hasgrade); // Store a new value. $gradeitem->store_grade_from_formdata($student, $grader, (object) ['grade' => 97]); $hasgrade = $gradeitem->user_has_grade($student); $this->assertEquals(true, $hasgrade); } /** * Ensure that it is possible to get, and update, a grade for a user when simple direct grading is in use. */ public function test_get_and_store_grade_for_user_with_simple_direct_grade(): void { $forum = $this->get_forum_instance([ 'grade_forum' => 100, ]); $course = $forum->get_course_record(); [$student] = $this->helper_create_users($course, 1); [$grader] = $this->helper_create_users($course, 1, 'editingteacher'); $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); // Create the grade record. $grade = $gradeitem->create_empty_grade($student, $grader); $this->assertIsObject($grade); $this->assertEquals($student->id, $grade->userid); // Store a new value. $gradeitem->store_grade_from_formdata($student, $grader, (object) ['grade' => 97]); } /** * Ensure that it is possible to get, and update, a grade for a user when a rubric is in use. */ public function test_get_and_store_grade_for_user_with_rubric(): void { global $DB; $this->resetAfterTest(); $generator = \testing_util::get_data_generator(); $gradinggenerator = $generator->get_plugin_generator('core_grading'); $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric'); $forum = $this->get_forum_instance([ 'grade_forum' => 100, ]); $course = $forum->get_course_record(); $context = $forum->get_context(); [$student] = $this->helper_create_users($course, 1); [$grader] = $this->helper_create_users($course, 1, 'editingteacher'); [$editor] = $this->helper_create_users($course, 1, 'editingteacher'); // Note: This must be run as a user because it messes with file uploads and drafts. $this->setUser($editor); $controller = $rubricgenerator->get_test_rubric($context, 'mod_forum', 'forum'); // Create the forum_gradeitem object that we'll be testing. $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); // The current grade should be null initially. $this->assertCount(0, $DB->get_records('forum_grades')); $grade = $gradeitem->get_grade_for_user($student, $grader); $instance = $gradeitem->get_advanced_grading_instance($grader, $grade); $this->assertIsObject($grade); $this->assertEquals($student->id, $grade->userid); $this->assertEquals($forum->get_id(), $grade->forum); $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric'); $data = $rubricgenerator->get_submitted_form_data($controller, $grade->id, [ 'Spelling is important' => [ 'score' => 2, 'remark' => 'Abracadabra', ], 'Pictures' => [ 'score' => 1, 'remark' => 'More than one picture', ], ]); // Store a new value. $gradeitem->store_grade_from_formdata($student, $grader, (object) [ 'instanceid' => $instance->get_id(), 'advancedgrading' => $data, ]); } /** * Test the logic in the should_grade_only_active_users() method. * * @covers ::should_grade_only_active_users * @dataProvider should_grade_only_active_users_provider * @param bool $showonlyactiveenrolconfig Whether the 'grade_report_showonlyactiveenrol' is enabled or not. * @param bool $showonlyactiveenrolpreference Whether the 'grade_report_showonlyactiveenrol' preference is enabled or not. * @param bool $viewsuspendeduserscapability Whether the 'moodle/course:viewsuspendedusers' capability is allowed or not. * @param bool $expected The expected result. */ public function test_should_grade_only_active_users(bool $showonlyactiveenrolconfig, bool $showonlyactiveenrolpreference, bool $viewsuspendeduserscapability, bool $expected): void { global $DB; $this->resetAfterTest(); $forum = $this->get_forum_instance([ 'grade_forum' => 100, ]); $course = $forum->get_course_record(); $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'editingteacher'); $editingteacherroleid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher']); // Set the 'grade_report_showonlyactiveenrol' config setting. set_config('grade_report_showonlyactiveenrol', $showonlyactiveenrolconfig); $this->setUser($user); // Set the 'grade_report_showonlyactiveenrol' user preference. set_user_preference('grade_report_showonlyactiveenrol', $showonlyactiveenrolpreference); // Set the 'moodle/course:viewsuspendedusers' user capability. assign_capability('moodle/course:viewsuspendedusers', $viewsuspendeduserscapability ? CAP_ALLOW : CAP_PROHIBIT, $editingteacherroleid, \context_course::instance($course->id)); $this->assertEquals($expected, $gradeitem->should_grade_only_active_users()); } /** * Data provider for test_should_grade_only_active_users. * * @return array */ public static function should_grade_only_active_users_provider(): array { return [ 'Enabled showonlyactiveenrol setting; enabled showonlyactiveenrol preference; view suspended users capability' => [ 'showonlyactiveenrolconfig' => true, 'showonlyactiveenrolpreference' => true, 'viewsuspendeduserscapability' => true, 'expected' => true, ], 'Disabled showonlyactiveenrol setting; enabled showonlyactiveenrol preference; view suspended users capability' => [ 'showonlyactiveenrolconfig' => false, 'showonlyactiveenrolpreference' => true, 'viewsuspendeduserscapability' => true, 'expected' => true, ], 'Disabled showonlyactiveenrol setting; disabled showonlyactiveenrol preference; view suspended users capability' => [ 'showonlyactiveenrolconfig' => false, 'showonlyactiveenrolpreference' => false, 'viewsuspendeduserscapability' => true, 'expected' => false, ], 'Disabled showonlyactiveenrol setting; disabled showonlyactiveenrol preference; no view suspended users capability' => [ 'showonlyactiveenrolconfig' => false, 'showonlyactiveenrolpreference' => false, 'viewsuspendeduserscapability' => false, 'expected' => true, ], 'Enabled showonlyactiveenrol setting; enabled showonlyactiveenrol preference; no view suspended users capability' => [ 'showonlyactiveenrolconfig' => true, 'showonlyactiveenrolpreference' => true, 'viewsuspendeduserscapability' => false, 'expected' => true, ], ]; } /** * Get a forum instance. * * @param array $config * @return forum_entity */ protected function get_forum_instance(array $config = []): forum_entity { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', array_merge($config, ['course' => $course->id])); $vaultfactory = \mod_forum\local\container::get_vault_factory(); $vault = $vaultfactory->get_forum_vault(); return $vault->get_from_id((int) $forum->id); } } PK-B\$ $ tests/grade/gradeitems_test.phpnu[. declare(strict_types = 1); namespace mod_forum\grade; use advanced_testcase; use core_grades\component_gradeitems; use coding_exception; /** * Unit tests for mod_forum\grades\gradeitems. * * @package mod_forum * @category test * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class gradeitems_test extends advanced_testcase { /** * Ensure that the mappings are present and correct. */ public function test_get_itemname_mapping_for_component(): void { $mappings = component_gradeitems::get_itemname_mapping_for_component('mod_forum'); $this->assertIsArray($mappings); $this->assertCount(2, $mappings); $expected = [0 => 'rating', 1 => 'forum']; // Verify each expected element exists and its value matches. foreach ($expected as $key => $value) { $this->assertArrayHasKey($key, $mappings); $this->assertSame($value, $mappings[$key]); } } /** * Ensure that the advanced grading only applies to the relevant items. */ public function test_get_advancedgrading_itemnames_for_component(): void { $mappings = component_gradeitems::get_advancedgrading_itemnames_for_component('mod_forum'); $this->assertIsArray($mappings); $this->assertCount(1, $mappings); $this->assertContains('forum', $mappings); $this->assertNotContains('rating', $mappings); } /** * Ensure that the correct items are identified by is_advancedgrading_itemname. * * @dataProvider is_advancedgrading_itemname_provider * @param string $itemname * @param bool $expected */ public function test_is_advancedgrading_itemname(string $itemname, bool $expected): void { $this->assertEquals( $expected, component_gradeitems::is_advancedgrading_itemname('mod_forum', $itemname) ); } /** * Data provider for tests of is_advancedgrading_itemname. * * @return array */ public static function is_advancedgrading_itemname_provider(): array { return [ 'rating is not advanced' => [ 'rating', false, ], 'Whole forum grading is advanced' => [ 'forum', true, ], ]; } } PK-B\ZH--tests/generator_test.phpnu[. namespace mod_forum; /** * PHPUnit data generator testcase * * @package mod_forum * @category test * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \mod_forum_generator */ final class generator_test extends \advanced_testcase { public function setUp(): void { parent::setUp(); // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); } public function tearDown(): void { // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); parent::tearDown(); } public function test_generator(): void { global $DB; $this->resetAfterTest(true); $this->assertEquals(0, $DB->count_records('forum')); $course = $this->getDataGenerator()->create_course(); /** @var mod_forum_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum'); $this->assertInstanceOf('mod_forum_generator', $generator); $this->assertEquals('forum', $generator->get_modulename()); $generator->create_instance(['course' => $course->id]); $generator->create_instance(['course' => $course->id]); $forum = $generator->create_instance(['course' => $course->id]); $this->assertEquals(3, $DB->count_records('forum')); $cm = get_coursemodule_from_instance('forum', $forum->id); $this->assertEquals($forum->id, $cm->instance); $this->assertEquals('forum', $cm->modname); $this->assertEquals($course->id, $cm->course); $context = \context_module::instance($cm->id); $this->assertEquals($forum->cmid, $context->instanceid); // Test gradebook integration using low level DB access - DO NOT USE IN PLUGIN CODE. $forum = $generator->create_instance(['course' => $course->id, 'assessed' => 1, 'scale' => 100]); $gitem = $DB->get_record( 'grade_items', ['courseid' => $course->id, 'itemtype' => 'mod', 'itemmodule' => 'forum', 'iteminstance' => $forum->id] ); $this->assertNotEmpty($gitem); $this->assertEquals(100, $gitem->grademax); $this->assertEquals(0, $gitem->grademin); $this->assertEquals(GRADE_TYPE_VALUE, $gitem->gradetype); } /** * Test create_discussion. */ public function test_create_discussion(): void { global $DB; $this->resetAfterTest(true); // User that will create the forum. $user = self::getDataGenerator()->create_user(); // Create course to add the forum to. $course = self::getDataGenerator()->create_course(); // The forum. $record = new \stdClass(); $record->course = $course->id; $forum = self::getDataGenerator()->create_module('forum', $record); // Add a few discussions. $record = []; $record['course'] = $course->id; $record['forum'] = $forum->id; $record['userid'] = $user->id; $record['pinned'] = FORUM_DISCUSSION_PINNED; // Pin one discussion. self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); $record['pinned'] = FORUM_DISCUSSION_UNPINNED; // No pin for others. self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // Check the discussions were correctly created. $this->assertEquals(3, $DB->count_records_select( 'forum_discussions', 'forum = :forum', ['forum' => $forum->id] )); $record['tags'] = ['Cats', 'mice']; $record = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); $this->assertEquals( ['Cats', 'mice'], array_values(\core_tag_tag::get_item_tags_array('mod_forum', 'forum_posts', $record->firstpost)) ); } /** * Test create_post. */ public function test_create_post(): void { global $DB; $this->resetAfterTest(true); // Create a bunch of users. $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); $user3 = self::getDataGenerator()->create_user(); $user4 = self::getDataGenerator()->create_user(); // Create course to add the forum. $course = self::getDataGenerator()->create_course(); // The forum. $record = new \stdClass(); $record->course = $course->id; $forum = self::getDataGenerator()->create_module('forum', $record); // Add a discussion. $record->forum = $forum->id; $record->userid = $user1->id; $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // Add a bunch of replies, changing the userid. $record = new \stdClass(); $record->discussion = $discussion->id; $record->userid = $user2->id; self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $record->userid = $user3->id; self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $record->userid = $user4->id; self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); // Check the posts were correctly created, remember, when creating a discussion a post // is generated as well, so we should have 4 posts, not 3. $this->assertEquals(4, $DB->count_records_select( 'forum_posts', 'discussion = :discussion', ['discussion' => $discussion->id] )); $record->tags = ['Cats', 'mice']; $record = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record); $this->assertEquals( ['Cats', 'mice'], array_values(\core_tag_tag::get_item_tags_array('mod_forum', 'forum_posts', $record->id)) ); } public function test_create_content(): void { global $DB; $this->resetAfterTest(true); // Create a bunch of users. $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); $user3 = self::getDataGenerator()->create_user(); $user4 = self::getDataGenerator()->create_user(); $this->setAdminUser(); // Create course and forum. $course = self::getDataGenerator()->create_course(); $forum = self::getDataGenerator()->create_module('forum', ['course' => $course]); $generator = self::getDataGenerator()->get_plugin_generator('mod_forum'); // This should create discussion. $post1 = $generator->create_content($forum); // This should create posts in the discussion. $post2 = $generator->create_content($forum, ['parent' => $post1->id]); $post3 = $generator->create_content($forum, ['discussion' => $post1->discussion]); // This should create posts answering another post. $post4 = $generator->create_content($forum, ['parent' => $post2->id]); // This should create post with tags. $post5 = $generator->create_content($forum, ['parent' => $post2->id, 'tags' => ['Cats', 'mice']]); $discussionrecords = $DB->get_records('forum_discussions', ['forum' => $forum->id]); $postrecords = $DB->get_records('forum_posts'); $postrecords2 = $DB->get_records('forum_posts', ['discussion' => $post1->discussion]); $this->assertEquals(1, count($discussionrecords)); $this->assertEquals(5, count($postrecords)); $this->assertEquals(5, count($postrecords2)); $this->assertEquals($post1->id, $discussionrecords[$post1->discussion]->firstpost); $this->assertEquals($post1->id, $postrecords[$post2->id]->parent); $this->assertEquals($post1->id, $postrecords[$post3->id]->parent); $this->assertEquals($post2->id, $postrecords[$post4->id]->parent); $this->assertEquals( ['Cats', 'mice'], array_values(\core_tag_tag::get_item_tags_array('mod_forum', 'forum_posts', $post5->id)) ); } public function test_create_post_time_system(): void { $this->resetAfterTest(true); $user = self::getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $forum = self::getDataGenerator()->create_module('forum', (object) [ 'course' => $course->id, ]); $starttime = time(); // Add a discussion. $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion((object) [ 'course' => $course->id, 'forum' => $forum->id, 'userid' => $user->id, ]); // Add a post. $post = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post((object) [ 'discussion' => $discussion->id, 'userid' => $user->id, ]); $this->assertGreaterThanOrEqual($starttime, $discussion->timemodified); $this->assertGreaterThanOrEqual($starttime, $post->created); // The fallback behavior is to add the number of created posts to the current time to avoid duplicates. $this->assertLessThanOrEqual(time() + 1, $discussion->timemodified); $this->assertLessThanOrEqual(time() + 2, $post->created); } public function test_create_post_time_frozen(): void { $this->resetAfterTest(true); $clock = $this->mock_clock_with_frozen(100); $user = self::getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $forum = self::getDataGenerator()->create_module('forum', (object) [ 'course' => $course->id, ]); $starttime = time(); // Add a discussion. $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion((object) [ 'course' => $course->id, 'forum' => $forum->id, 'userid' => $user->id, ]); $this->assertEquals(100, $discussion->timemodified); // Add a post. $clock->set_to(200); $post = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post((object) [ 'discussion' => $discussion->id, 'userid' => $user->id, ]); $this->assertEquals(200, $post->created); } } PK-B\`oxWxW&tests/builders_exported_posts_test.phpnu[. namespace mod_forum; use mod_forum_tests_generator_trait; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/generator_trait.php'); /** * The exported_posts builder tests. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class builders_exported_posts_test extends \advanced_testcase { // Make use of the test generator trait. use mod_forum_tests_generator_trait; /** @var \mod_forum\local\builders\exported_posts */ private $builder; /** * Set up function for tests. */ public function setUp(): void { parent::setUp(); // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); $builderfactory = \mod_forum\local\container::get_builder_factory(); $this->builder = $builderfactory->get_exported_posts_builder(); } /** * Tear down function for tests. */ public function tearDown(): void { // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); parent::tearDown(); } /** * Convert the stdClass values into their proper entity classes. * * @param stdClass[] $forums List of forums * @param stdClass[] $discussions List of discussions * @param stdClass[] $posts List of posts * @return array */ private function convert_to_entities(array $forums, array $discussions, array $posts) { global $DB; $entityfactory = \mod_forum\local\container::get_entity_factory(); return [ // Forums. array_map(function($forum) use ($entityfactory, $DB) { $course = $DB->get_record('course', ['id' => $forum->course]); $coursemodule = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($coursemodule->id); return $entityfactory->get_forum_from_stdClass($forum, $context, $coursemodule, $course); }, $forums), // Discussions. array_map(function($discussion) use ($entityfactory) { return $entityfactory->get_discussion_from_stdClass($discussion); }, $discussions), // Posts. array_map(function($post) use ($entityfactory) { return $entityfactory->get_post_from_stdClass($post); }, $posts) ]; } /** * Test the build function throws exception if not given all of the forums for * the list of posts. */ public function test_build_throws_exception_on_missing_forums(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum1 = $datagenerator->create_module('forum', ['course' => $course->id]); $forum2 = $datagenerator->create_module('forum', ['course' => $course->id]); [$discussion1, $post1] = $this->helper_post_to_forum($forum1, $user); [$discussion2, $post2] = $this->helper_post_to_forum($forum2, $user); [$forums, $discussions, $posts] = $this->convert_to_entities( [$forum1, $forum2], [$discussion1, $discussion2], [$post1, $post2] ); $this->expectException('moodle_exception'); $this->builder->build($user, [$forums[0]], $discussions, $posts); } /** * Test the build function throws exception if not given all of the discussions for * the list of posts. */ public function test_build_throws_exception_on_missing_discussions(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum1 = $datagenerator->create_module('forum', ['course' => $course->id]); $forum2 = $datagenerator->create_module('forum', ['course' => $course->id]); [$discussion1, $post1] = $this->helper_post_to_forum($forum1, $user); [$discussion2, $post2] = $this->helper_post_to_forum($forum2, $user); [$forums, $discussions, $posts] = $this->convert_to_entities( [$forum1, $forum2], [$discussion1, $discussion2], [$post1, $post2] ); $this->expectException('moodle_exception'); $this->builder->build($user, $forums, [$discussions[0]], $posts); } /** * Test the build function returns the exported posts in the order that the posts are * given. */ public function test_build_returns_posts_in_order(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $course = $datagenerator->create_course(); $user1 = $datagenerator->create_and_enrol($course); $user2 = $datagenerator->create_and_enrol($course); $forum1 = $datagenerator->create_module('forum', ['course' => $course->id]); $forum2 = $datagenerator->create_module('forum', ['course' => $course->id]); [$discussion1, $post1] = $this->helper_post_to_forum($forum1, $user1); [$discussion2, $post2] = $this->helper_post_to_forum($forum1, $user2); $post3 = $this->helper_reply_to_post($post1, $user1); $post4 = $this->helper_reply_to_post($post1, $user2); [$discussion3, $post5] = $this->helper_post_to_forum($forum2, $user1); [$discussion4, $post6] = $this->helper_post_to_forum($forum2, $user2); $post7 = $this->helper_reply_to_post($post2, $user1); $post8 = $this->helper_reply_to_post($post2, $user2); [$forums, $discussions, $posts] = $this->convert_to_entities( [$forum1, $forum2], [$discussion1, $discussion2, $discussion3, $discussion4], [$post1, $post2, $post3, $post4, $post5, $post6, $post7, $post8] ); // Randomly order the posts. shuffle($posts); $exportedposts = $this->builder->build($user1, $forums, $discussions, $posts); $expectedpostids = array_map(function($post) { return $post->get_id(); }, $posts); $actualpostids = array_map(function($exportedpost) { return (int) $exportedpost->id; }, $exportedposts); $this->assertEquals($expectedpostids, $actualpostids); } /** * Test the build function loads authors. */ public function test_build_loads_authors(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $user1 = $datagenerator->create_user(); $user2 = $datagenerator->create_user(); $user3 = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum1 = $datagenerator->create_module('forum', ['course' => $course->id]); $forum2 = $datagenerator->create_module('forum', ['course' => $course->id]); [$discussion1, $post1] = $this->helper_post_to_forum($forum1, $user1); [$discussion2, $post2] = $this->helper_post_to_forum($forum1, $user2); $post3 = $this->helper_reply_to_post($post1, $user1); $post4 = $this->helper_reply_to_post($post1, $user2); [$discussion3, $post5] = $this->helper_post_to_forum($forum2, $user1); [$discussion4, $post6] = $this->helper_post_to_forum($forum2, $user2); // These 2 replies from user 3 won't be inlcuded in the export. $post7 = $this->helper_reply_to_post($post2, $user3); $post8 = $this->helper_reply_to_post($post2, $user3); [$forums, $discussions, $posts] = $this->convert_to_entities( [$forum1, $forum2], [$discussion1, $discussion2, $discussion3, $discussion4], [$post1, $post2, $post3, $post4, $post5, $post6] ); $datagenerator->enrol_user($user1->id, $course->id); $exportedposts = $this->builder->build($user1, $forums, $discussions, $posts); // We didn't include any posts from user 3 so we shouldn't see the authors // that match that user. $expectedids = [$user1->id, $user2->id]; $actualids = array_unique(array_map(function($exportedpost) { return (int) $exportedpost->author->id; }, $exportedposts)); sort($expectedids); sort($actualids); $this->assertEquals($expectedids, $actualids); } /** * Test the build function loads attachments. */ public function test_build_loads_attachments(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $user1 = $datagenerator->create_user(); $user2 = $datagenerator->create_user(); $user3 = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum1 = $datagenerator->create_module('forum', ['course' => $course->id]); $forum2 = $datagenerator->create_module('forum', ['course' => $course->id]); [$discussion1, $post1] = $this->helper_post_to_forum($forum1, $user1); [$discussion2, $post2] = $this->helper_post_to_forum($forum1, $user2); $post3 = $this->helper_reply_to_post($post1, $user1); $post4 = $this->helper_reply_to_post($post1, $user2); [$discussion3, $post5] = $this->helper_post_to_forum($forum2, $user1); [$discussion4, $post6] = $this->helper_post_to_forum($forum2, $user2); $post7 = $this->helper_reply_to_post($post5, $user3); $post8 = $this->helper_reply_to_post($post5, $user3); $filestorage = get_file_storage(); [$forums, $discussions, $posts] = $this->convert_to_entities( [$forum1, $forum2], [$discussion1, $discussion2, $discussion3, $discussion4], [$post1, $post2, $post3, $post4, $post5, $post6, $post7, $post8] ); // Add an attachment to a post in forum 1. $attachment1 = $filestorage->create_file_from_string( [ 'contextid' => $forums[0]->get_context()->id, 'component' => 'mod_forum', 'filearea' => 'attachment', 'itemid' => $post1->id, 'filepath' => '/', 'filename' => 'example1.jpg', ], 'image contents' ); // Add an attachment to a post in forum 2. $attachment2 = $filestorage->create_file_from_string( [ 'contextid' => $forums[1]->get_context()->id, 'component' => 'mod_forum', 'filearea' => 'attachment', 'itemid' => $post7->id, 'filepath' => '/', 'filename' => 'example2.jpg', ], 'image contents' ); // Enrol the user so that they can see the posts. $datagenerator->enrol_user($user1->id, $course->id); $exportedposts = $this->builder->build($user1, $forums, $discussions, $posts); $expected = ['example1.jpg', 'example2.jpg']; $actual = array_reduce($exportedposts, function($carry, $exportedpost) { if (!empty($exportedpost->attachments)) { foreach ($exportedpost->attachments as $attachment) { $carry[] = $attachment->filename; } } return $carry; }, []); sort($expected); sort($actual); $this->assertEquals($expected, $actual); } /** * Test the build function loads author groups. */ public function test_build_loads_author_groups(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $user1 = $datagenerator->create_user(); $user2 = $datagenerator->create_user(); $user3 = $datagenerator->create_user(); $course1 = $datagenerator->create_course(); $course2 = $datagenerator->create_course(); $forum1 = $datagenerator->create_module('forum', ['course' => $course1->id]); $forum2 = $datagenerator->create_module('forum', ['course' => $course1->id]); [$discussion1, $post1] = $this->helper_post_to_forum($forum1, $user1); [$discussion2, $post2] = $this->helper_post_to_forum($forum1, $user2); $post3 = $this->helper_reply_to_post($post1, $user1); $post4 = $this->helper_reply_to_post($post1, $user2); [$discussion3, $post5] = $this->helper_post_to_forum($forum2, $user1); [$discussion4, $post6] = $this->helper_post_to_forum($forum2, $user2); $post7 = $this->helper_reply_to_post($post5, $user3); $post8 = $this->helper_reply_to_post($post5, $user3); [$forums, $discussions, $posts] = $this->convert_to_entities( [$forum1, $forum2], [$discussion1, $discussion2, $discussion3, $discussion4], [$post1, $post2, $post3, $post4, $post5, $post6, $post7, $post8] ); // Enrol the user so that they can see the posts. $datagenerator->enrol_user($user1->id, $course1->id); $datagenerator->enrol_user($user1->id, $course2->id); $datagenerator->enrol_user($user2->id, $course1->id); $datagenerator->enrol_user($user2->id, $course2->id); $datagenerator->enrol_user($user3->id, $course1->id); $datagenerator->enrol_user($user3->id, $course2->id); $group1 = $datagenerator->create_group(['courseid' => $course1->id]); $group2 = $datagenerator->create_group(['courseid' => $course1->id]); // This group shouldn't be included in the results since it's in a different course. $group3 = $datagenerator->create_group(['courseid' => $course2->id]); $datagenerator->create_group_member(['userid' => $user1->id, 'groupid' => $group1->id]); $datagenerator->create_group_member(['userid' => $user2->id, 'groupid' => $group1->id]); $datagenerator->create_group_member(['userid' => $user1->id, 'groupid' => $group2->id]); $datagenerator->create_group_member(['userid' => $user1->id, 'groupid' => $group3->id]); $exportedposts = $this->builder->build($user1, $forums, $discussions, $posts); $expected = [ $user1->id => [$group1->id, $group2->id], $user2->id => [$group1->id], $user3->id => [] ]; $actual = array_reduce($exportedposts, function($carry, $exportedpost) { $author = $exportedpost->author; $authorid = $author->id; if (!isset($carry[$authorid])) { $carry[$authorid] = array_map(function($group) { return $group['id']; }, $author->groups); } return $carry; }, []); $this->assertEquals($expected, $actual); } /** * Test the build function loads tags. */ public function test_build_loads_tags(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $user1 = $datagenerator->create_user(); $user2 = $datagenerator->create_user(); $user3 = $datagenerator->create_user(); $course1 = $datagenerator->create_course(); $course2 = $datagenerator->create_course(); $forum1 = $datagenerator->create_module('forum', ['course' => $course1->id]); $forum2 = $datagenerator->create_module('forum', ['course' => $course1->id]); [$discussion1, $post1] = $this->helper_post_to_forum($forum1, $user1); [$discussion2, $post2] = $this->helper_post_to_forum($forum1, $user2); $post3 = $this->helper_reply_to_post($post1, $user1); $post4 = $this->helper_reply_to_post($post1, $user2); [$discussion3, $post5] = $this->helper_post_to_forum($forum2, $user1); [$discussion4, $post6] = $this->helper_post_to_forum($forum2, $user2); $post7 = $this->helper_reply_to_post($post5, $user3); $post8 = $this->helper_reply_to_post($post5, $user3); [$forums, $discussions, $posts] = $this->convert_to_entities( [$forum1, $forum2], [$discussion1, $discussion2, $discussion3, $discussion4], [$post1, $post2, $post3, $post4, $post5, $post6, $post7, $post8] ); // Enrol the user so that they can see the posts. $datagenerator->enrol_user($user1->id, $course1->id); $datagenerator->enrol_user($user1->id, $course2->id); $datagenerator->enrol_user($user2->id, $course1->id); $datagenerator->enrol_user($user2->id, $course2->id); $datagenerator->enrol_user($user3->id, $course1->id); $datagenerator->enrol_user($user3->id, $course2->id); \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post1->id, $forums[0]->get_context(), ['foo', 'bar']); \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post4->id, $forums[0]->get_context(), ['foo', 'baz']); \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post7->id, $forums[1]->get_context(), ['bip']); $exportedposts = $this->builder->build($user1, $forums, $discussions, $posts); $expected = [ $post1->id => ['foo', 'bar'], $post4->id => ['foo', 'baz'], $post7->id => ['bip'] ]; $actual = array_reduce($exportedposts, function($carry, $exportedpost) { if (!empty($exportedpost->tags)) { $carry[$exportedpost->id] = array_map(function($tag) { return $tag['displayname']; }, $exportedpost->tags); } return $carry; }, []); $this->assertEquals($expected, $actual); } /** * Test the build function loads read_receipts. */ public function test_build_loads_read_receipts(): void { $this->resetAfterTest(); $datagenerator = $this->getDataGenerator(); $user1 = $datagenerator->create_user(['trackforums' => 1]); $user2 = $datagenerator->create_user(['trackforums' => 0]); $course1 = $datagenerator->create_course(); $course2 = $datagenerator->create_course(); $forum1 = $datagenerator->create_module('forum', ['course' => $course1->id, 'trackingtype' => FORUM_TRACKING_OPTIONAL]); $forum2 = $datagenerator->create_module('forum', ['course' => $course1->id, 'trackingtype' => FORUM_TRACKING_OFF]); [$discussion1, $post1] = $this->helper_post_to_forum($forum1, $user1); [$discussion2, $post2] = $this->helper_post_to_forum($forum1, $user2); $post3 = $this->helper_reply_to_post($post1, $user1); $post4 = $this->helper_reply_to_post($post1, $user2); [$discussion3, $post5] = $this->helper_post_to_forum($forum2, $user1); [$discussion4, $post6] = $this->helper_post_to_forum($forum2, $user2); $post7 = $this->helper_reply_to_post($post5, $user1); $post8 = $this->helper_reply_to_post($post5, $user1); [$forums, $discussions, $posts] = $this->convert_to_entities( [$forum1, $forum2], [$discussion1, $discussion2, $discussion3, $discussion4], [$post1, $post2, $post3, $post4, $post5, $post6, $post7, $post8] ); // Enrol the user so that they can see the posts. $datagenerator->enrol_user($user1->id, $course1->id); $datagenerator->enrol_user($user1->id, $course2->id); $datagenerator->enrol_user($user2->id, $course1->id); $datagenerator->enrol_user($user2->id, $course2->id); forum_tp_add_read_record($user1->id, $post1->id); forum_tp_add_read_record($user1->id, $post4->id); forum_tp_add_read_record($user1->id, $post7->id); forum_tp_add_read_record($user2->id, $post1->id); forum_tp_add_read_record($user2->id, $post4->id); forum_tp_add_read_record($user2->id, $post7->id); // User 1 has tracking enabled. $exportedposts = $this->builder->build($user1, $forums, $discussions, $posts); $expected = [ // Tracking set for forum 1 for user 1. $post1->id => false, $post2->id => true, $post3->id => true, $post4->id => false, // Tracking is off for forum 2 so everything should be null. $post5->id => null, $post6->id => null, $post7->id => null, $post8->id => null ]; $actual = array_reduce($exportedposts, function($carry, $exportedpost) { $carry[$exportedpost->id] = $exportedpost->unread; return $carry; }, []); $this->assertEquals($expected, $actual); // User 2 has tracking disabled. $exportedposts = $this->builder->build($user2, $forums, $discussions, $posts); // Tracking is off for user 2 so everything should be null. $expected = [ $post1->id => null, $post2->id => null, $post3->id => null, $post4->id => null, $post5->id => null, $post6->id => null, $post7->id => null, $post8->id => null ]; $actual = array_reduce($exportedposts, function($carry, $exportedpost) { $carry[$exportedpost->id] = $exportedpost->unread; return $carry; }, []); $this->assertEquals($expected, $actual); } } PK-B\Or> > tests/entities_post_test.phpnu[. namespace mod_forum; use mod_forum\local\entities\post as post_entity; /** * The post entity tests. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class entities_post_test extends \advanced_testcase { /** * Test the entity returns expected values. */ public function test_entity(): void { $this->resetAfterTest(); $owner = $this->getDataGenerator()->create_user(); $notowner = $this->getDataGenerator()->create_user(); $past = time() - 10; $post = new post_entity( 4, 1, 0, $owner->id, $past, $past, true, 'post subject', 'post message', FORMAT_MOODLE, true, false, 0, false, false, false, null, null ); $this->assertEquals(4, $post->get_id()); $this->assertEquals(1, $post->get_discussion_id()); $this->assertEquals(0, $post->get_parent_id()); $this->assertEquals(false, $post->has_parent()); $this->assertEquals($owner->id, $post->get_author_id()); $this->assertEquals($past, $post->get_time_created()); $this->assertEquals($past, $post->get_time_modified()); $this->assertEquals(true, $post->has_been_mailed()); $this->assertEquals('post subject', $post->get_subject()); $this->assertEquals('post message', $post->get_message()); $this->assertEquals(FORMAT_MOODLE, $post->get_message_format()); $this->assertEquals(true, $post->is_message_trusted()); $this->assertEquals(false, $post->has_attachments()); $this->assertEquals(0, $post->get_total_score()); $this->assertEquals(false, $post->should_mail_now()); $this->assertEquals(false, $post->is_deleted()); $this->assertTrue($post->get_age() >= 10); $this->assertEquals(true, $post->is_owned_by_user($owner)); $this->assertEquals(false, $post->is_owned_by_user($notowner)); } } PK-B\?tests/vaults_forum_test.phpnu[. namespace mod_forum; use mod_forum_tests_generator_trait; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/generator_trait.php'); /** * The forum vault tests. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \mod_forum\local\vaults\forum */ final class vaults_forum_test extends \advanced_testcase { // Make use of the test generator trait. use mod_forum_tests_generator_trait; /** * Test get_from_id. * * @covers ::get_from_id */ public function test_get_from_id(): void { $this->resetAfterTest(); $vaultfactory = \mod_forum\local\container::get_vault_factory(); $vault = $vaultfactory->get_forum_vault(); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); $entity = $vault->get_from_id($forum->id); $this->assertEquals($forum->id, $entity->get_id()); } /** * Test get_from_course_module_id. * * @covers ::get_from_course_module_id */ public function test_get_from_course_module_id(): void { $this->resetAfterTest(); $vaultfactory = \mod_forum\local\container::get_vault_factory(); $vault = $vaultfactory->get_forum_vault(); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum1 = $datagenerator->create_module('forum', ['course' => $course->id]); $forum2 = $datagenerator->create_module('forum', ['course' => $course->id]); $coursemodule1 = get_coursemodule_from_instance('forum', $forum1->id); $coursemodule2 = get_coursemodule_from_instance('forum', $forum2->id); // Don't exist. $entity = $vault->get_from_course_module_id($coursemodule1->id + 100); $this->assertEquals(null, $entity); $entity = $vault->get_from_course_module_id($coursemodule1->id); $this->assertEquals($forum1->id, $entity->get_id()); } /** * Test get_from_course_module_ids. * * @covers ::get_from_course_module_ids */ public function test_get_from_course_module_ids(): void { $this->resetAfterTest(); $vaultfactory = \mod_forum\local\container::get_vault_factory(); $vault = $vaultfactory->get_forum_vault(); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum1 = $datagenerator->create_module('forum', ['course' => $course->id]); $forum2 = $datagenerator->create_module('forum', ['course' => $course->id]); $coursemodule1 = get_coursemodule_from_instance('forum', $forum1->id); $coursemodule2 = get_coursemodule_from_instance('forum', $forum2->id); // Don't exist. $entities = array_values($vault->get_from_course_module_ids([$coursemodule1->id + 100, $coursemodule1->id + 200])); $this->assertEquals([], $entities); $entities = array_values($vault->get_from_course_module_ids([$coursemodule1->id, $coursemodule2->id])); usort($entities, function($a, $b) { return $a->get_id() <=> $b->get_id(); }); $this->assertCount(2, $entities); $this->assertEquals($forum1->id, $entities[0]->get_id()); $this->assertEquals($forum2->id, $entities[1]->get_id()); $entities = array_values($vault->get_from_course_module_ids([$coursemodule1->id])); usort($entities, function($a, $b) { return $a->get_id() <=> $b->get_id(); }); $this->assertCount(1, $entities); $this->assertEquals($forum1->id, $entities[0]->get_id()); } /** * Test get_from_post_id. * * @covers ::get_from_post_id */ public function test_get_from_post_id(): void { $this->resetAfterTest(); $vaultfactory = \mod_forum\local\container::get_vault_factory(); $vault = $vaultfactory->get_forum_vault(); $datagenerator = $this->getDataGenerator(); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); [$discussion, $post] = $this->helper_post_to_forum($forum, $user); $reply = $this->helper_reply_to_post($post, $user); $otherforum = $datagenerator->create_module('forum', ['course' => $course->id]); [$otherdiscussion, $otherpost] = $this->helper_post_to_forum($otherforum, $user); $otherreply = $this->helper_reply_to_post($otherpost, $user); $entity = $vault->get_from_post_id($post->id); $this->assertEquals($forum->id, $entity->get_id()); $entity = $vault->get_from_post_id($reply->id); $this->assertEquals($forum->id, $entity->get_id()); $this->assertEmpty($vault->get_from_post_id(-1)); } } PK-B\+Y 'tests/behat/no_groups_in_course.featurenu[@mod @mod_forum Feature: Posting to forums in a course with no groups behaves correctly Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | name | course | idnumber | groupmode | | forum | Standard forum | C1 | nogroups | 0 | | forum | Visible forum | C1 | visgroups | 2 | | forum | Separate forum | C1 | sepgroups | 1 | Scenario: Teachers can post in standard forum Given I am on the "Standard forum" "forum activity" page logged in as teacher1 When I click on "Add discussion topic" "link" Then I should not see "Post a copy to all groups" And I set the following fields to these values: | Subject | Teacher -> All participants | | Message | Teacher -> All participants | And I press "Post to forum" And I wait to be redirected And I should see "Teacher -> All participants" Scenario: Teachers can post in forum with separate groups Given I am on the "Separate forum" "forum activity" page logged in as teacher1 When I click on "Add discussion topic" "link" Then I should not see "Post a copy to all groups" And I set the following fields to these values: | Subject | Teacher -> All participants | | Message | Teacher -> All participants | And I press "Post to forum" And I wait to be redirected And I should see "Teacher -> All participants" Scenario: Teachers can post in forum with visible groups Given I am on the "Visible forum" "forum activity" page logged in as teacher1 When I click on "Add discussion topic" "link" Then I should not see "Post a copy to all groups" And I set the following fields to these values: | Subject | Teacher -> All participants | | Message | Teacher -> All participants | And I press "Post to forum" And I wait to be redirected And I should see "Teacher -> All participants" Scenario: Students can post in standard forum Given I am on the "Standard forum" "forum activity" page logged in as student1 When I click on "Add discussion topic" "link" Then I should not see "Post a copy to all groups" And I set the following fields to these values: | Subject | Student -> All participants | | Message | Student -> All participants | And I press "Post to forum" And I wait to be redirected And I should see "Student -> All participants" Scenario: Students cannot post in forum with separate groups When I am on the "Separate forum" "forum activity" page logged in as student1 Then I should see "You are not able to create a discussion because you are not a member of any group." And I should not see "Add discussion topic" Scenario: Students cannot post in forum with visible groups When I am on the "Visible forum" "forum activity" page logged in as student1 Then I should see "You are not able to create a discussion because you are not a member of any group." And I should not see "Add discussion topic" PK-B\3o.tests/behat/separate_group_grade_forum.featurenu[@mod @mod_forum @core_grades Feature: I can grade a students by group with separate groups In order to assess a student's contributions As a teacher I can assign grades to a student based on their separate groups Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | | student3 | Student | 3 | student3@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | | student3 | C1 | student | And the following "groups" exist: | name | course | idnumber | | Group A | C1 | G1 | | Group B | C1 | G2 | | Group C | C1 | G3 | And the following "group members" exist: | user | group | | student1 | G1 | | student2 | G2 | | student1 | G3 | | student2 | G3 | | student3 | G3 | And the following "activity" exists: | activity | forum | | course | C1 | | name | Test Forum 1 | And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And I am on the "Test Forum 1" "forum activity editing" page And I expand all fieldsets And I set the following fields to these values: | Forum name | Test Forum 1 | | Description | Test | And I set the field "Whole forum grading > Type" to "Point" And I set the field "Common module settings > Group mode" to "Separate groups" And I press "Save and display" @javascript Scenario: Grade users by group A When I select "Group A" from the "Separate groups" singleselect And I click on "Grade users" "button" Then I should see "1 out of 1" And I should not see "1 out of 2" And I should not see "1 out of 3" And I should see "Student 1" And I should not see "Student 2" @javascript Scenario: Grade users by group B And I select "Group B" from the "Separate groups" singleselect And I click on "Grade users" "button" Then I should see "1 out of 1" And I should not see "1 out of 2" And I should not see "1 out of 3" And I should not see "Student 1" And I should see "Student 2" @javascript Scenario: Grade users by group C And I select "Group C" from the "Separate groups" singleselect And I click on "Grade users" "button" Then I should not see "1 out of 1" And I should not see "1 out of 2" And I should see "1 out of 3" @javascript Scenario: Teacher can see only the discussions which are joined to Given the following "users" exist: | username | firstname | lastname | email | | teacher2 | Teacher | 2 | teacher2@example.com | And the following "course enrolments" exist: | user | course | role | | teacher2 | C1 | teacher | And the following "group members" exist: | user | group | | teacher2 | G2 | | teacher2 | G3 | When I am on the "Test Forum 1" "forum activity" page logged in as student1 And I select "Group A" from the "Separate groups" singleselect And I click on "Add discussion topic" "link" And I set the following fields to these values: | Subject | Discussion subject A | | Message | Discussion message A | And I press "Post to forum" And I select "Group C" from the "Separate groups" singleselect And I click on "Add discussion topic" "link" And I set the following fields to these values: | Subject | Discussion subject C | | Message | Discussion message C | And I press "Post to forum" Then I am on the "Test Forum 1" "forum activity" page logged in as teacher2 And I select "Group C" from the "Separate groups" singleselect And I click on "Grade users" "button" And I should see "Student 1" And I should see "Discussion subject C" And I should not see "Discussion subject A" PK-B\=G G tests/behat/add_forum.featurenu[@mod @mod_forum @_file_upload Feature: Add forum activities and discussions In order to discuss topics with other users As a teacher I need to add forum activities to moodle courses @javascript Scenario: Add a forum and a discussion attaching files Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And I log in as "teacher1" And I add a forum activity to course "Course 1" section "1" and I fill the form with: | Forum name | Test forum name | | Forum type | Standard forum for general use | | Description | Test forum description | And I add a new discussion to "Test forum name" forum with: | Subject | Forum post 1 | | Message | This is the body | And I am on the "Course 1" course page logged in as student1 When I add a new discussion to "Test forum name" forum with: | Subject | Post with attachment | | Message | This is the body | | Attachment | lib/tests/fixtures/empty.txt | And I reply "Forum post 1" post from "Test forum name" forum with: | Subject | Reply with attachment | | Message | This is the body | | Attachment | lib/tests/fixtures/upload_users.csv | Then I should see "Reply with attachment" And I should see "upload_users.csv" And I am on the "Test forum name" "forum activity" page And I follow "Post with attachment" And I should see "empty.txt" And I follow "Edit" And the field "Attachment" matches value "empty.txt" # Check the page that lists all the forums in a course. And I am on the "C1" "forum index" page And I should see "Test forum name" Scenario: Test forum settings validation Given the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And I log in as "admin" And I add a forum activity to course "Course 1" section "0" and I fill the form with: | Forum name | Test forum | | Forum type | single | | Group mode | 1 | When I press "Save and display" Then I should see "Separate groups can't be used with a single simple discussion." And I should see "A single simple discussion can't be used with separate groups." PK-B\iJJo o $tests/behat/track_read_posts.featurenu[@mod @mod_forum Feature: A teacher can set one of 3 possible options for tracking read forum posts In order to ease the forum posts follow up As a user I need to distinct the unread posts from the read ones Background: Given the following "users" exist: | username | firstname | lastname | email | trackforums | | student1 | Student | 1 | student1@example.com | 1 | | student2 | Student | 2 | student2@example.com | 0 | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student2 | C1 | student | Scenario: Tracking forum posts off Given the following "activity" exists: | activity | forum | | course | C1 | | idnumber | forum1 | | type | general | | name | Test forum name | | trackingtype | 0 | And the following "mod_forum > discussion" exists: | forum | forum1 | | course | C1 | | user | admin | | name | Test post subject | | message | Test post message | When I am on the "Course 1" course page logged in as student1 Then I should not see "1 unread post" And I follow "Test forum name" And I should not see "Track unread posts" Scenario: Tracking forum posts optional with user tracking on Given the following "activity" exists: | activity | forum | | course | C1 | | idnumber | forum1 | | name | Test forum name | | type | general | | trackingtype | 1 | And the following "mod_forum > discussion" exists: | forum | forum1 | | course | C1 | | user | admin | | name | Test post subject | | message | Test post message | When I am on the "Course 1" course page logged in as student1 Then I should see "1 unread post" And I follow "Test forum name" And I follow "Don't track unread posts" And I wait to be redirected And I am on "Course 1" course homepage And I should not see "1 unread post" And I follow "Test forum name" And I follow "Track unread posts" And I wait to be redirected And I click on "1" "link" in the "Admin User" "table_row" And I am on "Course 1" course homepage And I should not see "1 unread post" Scenario: Tracking forum posts optional with user tracking off Given the following "activity" exists: | activity | forum | | course | C1 | | idnumber | 00001 | | name | Test forum name | | type | general | | trackingtype | 1 | And the following "mod_forum > discussion" exists: | forum | 00001 | | course | C1 | | user | admin | | name | Test post subject | | message | Test post message | When I am on the "Course 1" course page logged in as student2 Then I should not see "1 unread post" And I follow "Test forum name" And I should not see "Track unread posts" Scenario: Tracking forum posts forced with user tracking on Given the following config values are set as admin: | forum_allowforcedreadtracking | 1 | And the following "activity" exists: | activity | forum | | course | C1 | | idnumber | 00001 | | name | Test forum name | | type | general | | trackingtype | 2 | And the following "mod_forum > discussion" exists: | forum | 00001 | | course | C1 | | user | admin | | name | Test post subject | | message | Test post message | When I am on the "Course 1" course page logged in as student1 Then I should see "1 unread post" And I am on the "Test forum name" "forum activity" page And I should not see "Don't track unread posts" And I follow "Test post subject" And I am on "Course 1" course homepage And I should not see "1 unread post" Scenario: Tracking forum posts forced with user tracking off Given the following config values are set as admin: | forum_allowforcedreadtracking | 1 | And the following "activity" exists: | activity | forum | | course | C1 | | idnumber | 00001 | | name | Test forum name | | type | general | | trackingtype | 2 | And the following "mod_forum > discussion" exists: | forum | 00001 | | course | C1 | | user | admin | | name | Test post subject | | message | Test post message | When I am on the "Course 1" course page logged in as student2 Then I should see "1 unread post" And I am on the "Test forum name" "forum activity" page And I should not see "Don't track unread posts" And I follow "Test post subject" And I am on "Course 1" course homepage And I should not see "1 unread post" Scenario: Tracking forum posts forced (with force disabled) with user tracking on Given the following config values are set as admin: | forum_allowforcedreadtracking | 1 | And the following "activity" exists: | activity | forum | | course | C1 | | idnumber | 00001 | | name | Test forum name | | type | general | | trackingtype | 2 | And the following "mod_forum > discussion" exists: | forum | 00001 | | course | C1 | | user | admin | | name | Test post subject | | message | Test post message | And the following config values are set as admin: | forum_allowforcedreadtracking | 0 | When I am on the "Course 1" course page logged in as student1 Then I should see "1 unread post" And I follow "Test forum name" And I follow "Don't track unread posts" And I wait to be redirected And I am on "Course 1" course homepage And I should not see "1 unread post" And I follow "Test forum name" And I follow "Track unread posts" And I wait to be redirected And I click on "1" "link" in the "Admin User" "table_row" And I am on "Course 1" course homepage And I should not see "1 unread post" Scenario: Tracking forum posts forced (with force disabled) with user tracking off Given the following config values are set as admin: | forum_allowforcedreadtracking | 1 | And the following "activity" exists: | activity | forum | | course | C1 | | idnumber | 00001 | | name | Test forum name | | type | general | | trackingtype | 2 | And the following "mod_forum > discussion" exists: | forum | 00001 | | course | C1 | | user | admin | | name | Test post subject | | message | Test post message | And the following config values are set as admin: | forum_allowforcedreadtracking | 0 | When I am on the "Course 1" course page logged in as student2 Then I should not see "1 unread post" And I follow "Test forum name" And I should not see "Track unread posts" PK-B\<3[h 2tests/behat/forum_subscriptions_management.featurenu[@mod @mod_forum Feature: A teacher can control the subscription to a forum In order to change individual user's subscriptions As a course administrator I can change subscription setting for my users Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher | Teacher | Tom | teacher@example.com | | student1 | Student | 1 | student.1@example.com | | student2 | Student | 2 | student.2@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | Scenario: A teacher can change toggle subscription editing on and off Given the following "activity" exists: | activity | forum | | course | C1 | | idnumber | f01 | | name | Test forum name | When I am on the "Test forum name" "forum activity" page logged in as teacher And I navigate to "Subscriptions" in current page administration Then I select "Manage subscribers" from the "Subscribers" singleselect And "Add" "button" should exist And "Remove" "button" should exist And I select "View subscribers" from the "Subscribers" singleselect And the "Subscribers" select box should contain "View subscribers" @javascript Scenario Outline: Toggle forum subscription mode via settings navigation Given the following "activity" exists: | course | C1 | | activity | forum | | name | Test forum | | idnumber | F1 | | type | general | | forcesubscribe | | When I am on the "Test forum" "forum activity" page logged in as "teacher" And I navigate to "Subscriptions" in current page administration And I select "" from the "Subscription mode" singleselect Then I should see "" Examples: | initialmode | updatedmode | updatedmodeconfirmed | | 1 | Optional subscription | Everyone can now choose to be subscribed | | 0 | Forced subscription | Everyone is now subscribed to this forum | | 0 | Auto subscription | Everyone is now subscribed to this forum | | 0 | Subscription disabled | Subscriptions are now disallowed | PK-B\y > > %tests/behat/edit_post_teacher.featurenu[@mod @mod_forum Feature: Teachers can edit or delete any forum post In order to refine the forum contents As a teacher I need to edit or delete any user's forum posts Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | name | course | idnumber | | forum | Test forum name | C1 | forum1 | And the following "mod_forum > discussions" exist: | forum | course | user | name | message | | forum1 | C1 | teacher1 | Teacher post subject | Teacher post message | And the following "mod_forum > posts" exist: | user | parentsubject | subject | message | | student1 | Teacher post subject | Student post subject | Student post message | Scenario: A teacher can delete another user's posts When I am on the "Test forum name" "forum activity" page logged in as teacher1 And I follow "Teacher post subject" And I click on "Delete" "link" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' forumpost ')][contains(., 'Student post subject')]" "xpath_element" And I press "Continue" Then I should not see "Student post subject" And I should not see "Student post message" Scenario: A teacher can edit another user's posts When I am on the "Test forum name" "forum activity" page logged in as teacher1 And I follow "Teacher post subject" And I click on "Edit" "link" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' forumpost ')][contains(., 'Student post subject')]" "xpath_element" And I set the following fields to these values: | Subject | Edited student subject | And I press "Save changes" And I wait to be redirected Then I should see "Edited student subject" And I should see "Edited by Teacher 1 - original submission" Scenario: A student can't edit or delete another user's posts When I am on the "Test forum name" "forum activity" page logged in as student1 And I follow "Teacher post subject" Then I should not see "Edit" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' forumpost ')][contains(., 'Teacher post subject')]" "xpath_element" And I should not see "Delete" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' forumpost ')][contains(., 'Teacher post subject')]" "xpath_element" PK-B\{ N88.tests/behat/separate_group_discussions.featurenu[@mod @mod_forum Feature: Posting to all groups in a separate group discussion is restricted to users with access to all groups In order to post to all groups in a forum with separate groups As a teacher I need to have the accessallgroups capability Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | noneditor1 | Non-editing teacher | 1 | noneditor1@example.com | | noneditor2 | Non-editing teacher | 2 | noneditor2@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | | student3 | Student | 3 | student3@example.com | | student4 | Student | 4 | student4@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | noneditor1 | C1 | teacher | | noneditor2 | C1 | teacher | | student1 | C1 | student | | student2 | C1 | student | | student3 | C1 | student | | student4 | C1 | student | And the following "groups" exist: | name | course | idnumber | participation | | Group A | C1 | G1 | 1 | | Group B | C1 | G2 | 1 | | Group C | C1 | G3 | 1 | | Group D | C1 | G4 | 0 | And the following "group members" exist: | user | group | | teacher1 | G1 | | teacher1 | G2 | | noneditor1 | G1 | | noneditor1 | G2 | | noneditor1 | G3 | | noneditor2 | G1 | | noneditor2 | G2 | | student1 | G1 | | student2 | G1 | | student2 | G2 | | student3 | G4 | And the following "activities" exist: | activity | name | course | idnumber | groupmode | | forum | Standard forum name | C1 | sepgroups | 1 | And the following "mod_forum > discussions" exist: | forum | name | subject | message | group | | sepgroups | Initial Disc ALL | Initial Disc ALL | Disc ALL content | All participants | | sepgroups | Initial Disc G1 | Initial Disc G1 | Disc G1 content | G1 | | sepgroups | Initial Disc G2 | Initial Disc G2 | Disc G2 content | G2 | | sepgroups | Initial Disc G3 | Initial Disc G3 | Disc G3 content | G3 | Scenario: Teacher with accessallgroups can view all groups When I am on the "Standard forum name" "forum activity" page logged in as teacher1 Then the "Separate groups" select box should contain "All participants" And the "Separate groups" select box should contain "Group A" And the "Separate groups" select box should contain "Group B" And the "Separate groups" select box should contain "Group C" And the "Separate groups" select box should not contain "Group D" And I select "All participants" from the "Separate groups" singleselect And I should see "Initial Disc ALL" And I should see "Initial Disc G1" And I should see "Initial Disc G2" And I should see "Initial Disc G2" And I select "Group A" from the "Separate groups" singleselect And I should see "Initial Disc ALL" And I should see "Initial Disc G1" But I should not see "Initial Disc G2" And I should not see "Initial Disc G3" And I select "Group B" from the "Separate groups" singleselect And I should see "Initial Disc ALL" And I should see "Initial Disc G2" But I should not see "Initial Disc G1" And I should not see "Initial Disc G3" Scenario: Teacher with accessallgroups can select any group when posting Given I am on the "Standard forum name" "forum activity" page logged in as teacher1 When I click on "Add discussion topic" "link" And I click on "Advanced" "button" Then the "Group" select box should contain "All participants" And the "Group" select box should contain "Group A" And the "Group" select box should contain "Group B" And the "Group" select box should contain "Group C" And the "Group" select box should not contain "Group D" And I should see "Post a copy to all groups" Scenario: Teacher with accessallgroups can post in groups they are a member of Given I am on the "Standard forum name" "forum activity" page logged in as teacher1 And I select "Group A" from the "Separate groups" singleselect When I click on "Add discussion topic" "link" And I click on "Advanced" "button" Then I should see "Post a copy to all groups" And I set the following fields to these values: | Subject | Teacher 1 -> Group B | | Message | Teacher 1 -> Group B | # Change the group in the post form. | Group | Group B | And I press "Post to forum" And I wait to be redirected # We should be redirected to the group that we selected when posting. And the field "Separate groups" matches value "Group B" And I should see "Group B" in the "Teacher 1 -> Group B" "table_row" And I should not see "Group A" in the "Teacher 1 -> Group B" "table_row" And I should not see "Group C" in the "Teacher 1 -> Group B" "table_row" # It should also be displayed under All participants And I select "All participants" from the "Separate groups" singleselect And I should see "Group B" in the "Teacher 1 -> Group B" "table_row" And I should not see "Group A" in the "Teacher 1 -> Group B" "table_row" And I should not see "Group C" in the "Teacher 1 -> Group B" "table_row" # It should not be displayed in Groups A, or C. And I select "Group A" from the "Separate groups" singleselect And I should not see "Teacher 1 -> Group B" And I select "Group C" from the "Separate groups" singleselect And I should not see "Teacher 1 -> Group B" Scenario: Teacher with accessallgroups can post in groups they are not a member of Given I am on the "Standard forum name" "forum activity" page logged in as teacher1 And I select "Group A" from the "Separate groups" singleselect When I click on "Add discussion topic" "link" And I click on "Advanced" "button" Then I should see "Post a copy to all groups" And I set the following fields to these values: | Subject | Teacher 1 -> Group C | | Message | Teacher 1 -> Group C | | Group | Group C | And I press "Post to forum" And I wait to be redirected # We should be redirected to the group that we selected when posting. And the field "Separate groups" matches value "Group C" # We redirect to the group posted in automatically. And I should see "Group C" in the "Teacher 1 -> Group C" "table_row" And I should not see "Group A" in the "Teacher 1 -> Group C" "table_row" And I should not see "Group B" in the "Teacher 1 -> Group C" "table_row" # It should also be displayed under All participants And I select "All participants" from the "Separate groups" singleselect And I should see "Group C" in the "Teacher 1 -> Group C" "table_row" And I should not see "Group A" in the "Teacher 1 -> Group C" "table_row" And I should not see "Group B" in the "Teacher 1 -> Group C" "table_row" # It should not be displayed in Groups A, or B. And I select "Group A" from the "Separate groups" singleselect And I should not see "Teacher 1 -> Group C" And I select "Group B" from the "Separate groups" singleselect And I should not see "Teacher 1 -> Group C" Scenario: Teacher with accessallgroups can post to all groups Given I am on the "Standard forum name" "forum activity" page logged in as teacher1 When I click on "Add discussion topic" "link" And I click on "Advanced" "button" And I set the following fields to these values: | Subject | Teacher 1 -> Post to all | | Message | Teacher 1 -> Post to all | | Post a copy to all groups | 1 | And I press "Post to forum" And I wait to be redirected # Posting to all groups means that we should be redirected to the page we started from. And the field "Separate groups" matches value "All participants" And I select "Group A" from the "Separate groups" singleselect Then I should see "Group A" in the "Teacher 1 -> Post to all" "table_row" And I should not see "Group B" in the "Teacher 1 -> Post to all" "table_row" And I should not see "Group C" in the "Teacher 1 -> Post to all" "table_row" And I select "Group B" from the "Separate groups" singleselect And I should see "Group B" in the "Teacher 1 -> Post to all" "table_row" And I should not see "Group A" in the "Teacher 1 -> Post to all" "table_row" And I should not see "Group C" in the "Teacher 1 -> Post to all" "table_row" And I select "Group C" from the "Separate groups" singleselect And I should see "Group C" in the "Teacher 1 -> Post to all" "table_row" And I should not see "Group A" in the "Teacher 1 -> Post to all" "table_row" And I should not see "Group B" in the "Teacher 1 -> Post to all" "table_row" # No point testing the "All participants". Scenario: Students in one group can only post in their group When I am on the "Standard forum name" "forum activity" page logged in as student1 Then I should see "Group A" And I click on "Add discussion topic" "link" And I should see "Group A" And I should not see "Group B" And I should not see "Group C" And I should not see "Post a copy to all groups" And I set the following fields to these values: | Subject | Student -> B | | Message | Student -> B | And I press "Post to forum" And I wait to be redirected And I should see "Group A" in the "Student -> B" "table_row" And I should not see "Group B" in the "Student -> B" "table_row" Scenario: Students in no group can see all group discussions, but not post. When I am on the "Standard forum name" "forum activity" page logged in as student4 Then I should see "All participants" And I should see "Initial Disc ALL" And I should see "You are not able to create a discussion because you are not a member of any group." And I should not see "Add discussion topic" Scenario: Students in non-participation groups can see all group discussions, but not post. When I am on the "Standard forum name" "forum activity" page logged in as student3 Then I should see "All participants" And I should see "Initial Disc ALL" And I should see "You are not able to create a discussion because you are not a member of any group." And I should not see "Add discussion topic" Scenario: Students in multiple group can post in all of their group individually When I am on the "Standard forum name" "forum activity" page logged in as student2 And I select "Group A" from the "Separate groups" singleselect And I click on "Add discussion topic" "link" And I click on "Advanced" "button" And the "Group" select box should not contain "All participants" And the "Group" select box should contain "Group A" And the "Group" select box should contain "Group B" And the "Group" select box should not contain "Group C" And I should not see "Post a copy to all groups" And I set the following fields to these values: | Subject | Student -> B | | Message | Student -> B | | Group | Group B | And I press "Post to forum" And I wait to be redirected # We should be redirected to the group that we selected when posting. And the field "Separate groups" matches value "Group B" And I should see "Group B" in the "Student -> B" "table_row" And I should not see "Group A" in the "Student -> B" "table_row" And I select "Group A" from the "Separate groups" singleselect And I should not see "Student -> B" # Now try posting in Group A (starting at Group B) And I select "Group B" from the "Separate groups" singleselect And I click on "Add discussion topic" "link" And I click on "Advanced" "button" And the "Group" select box should not contain "All participants" And the "Group" select box should contain "Group A" And the "Group" select box should contain "Group B" And the "Group" select box should not contain "Group C" And I should not see "Post a copy to all groups" And I set the following fields to these values: | Subject | Student -> A | | Message | Student -> A | | Group | Group A | And I press "Post to forum" And I wait to be redirected # We should be redirected to the group that we selected when posting. And the field "Separate groups" matches value "Group A" And I should see "Group A" in the "Student -> A" "table_row" And I should not see "Group B" in the "Student -> A" "table_row" And I select "Group B" from the "Separate groups" singleselect And I should not see "Student -> A" Scenario: Teacher in all groups but without accessallgroups can only post in their groups Given I am on the "Standard forum name" "forum activity" page logged in as noneditor1 When I click on "Add discussion topic" "link" And I click on "Advanced" "button" Then the "Group" select box should not contain "All participants" And the "Group" select box should contain "Group A" And the "Group" select box should contain "Group B" And I should see "Post a copy to all groups" Scenario: Teacher in some groups and without accessallgroups can only post in their groups Given I am on the "Standard forum name" "forum activity" page logged in as noneditor1 When I click on "Add discussion topic" "link" And I click on "Advanced" "button" Then the "Group" select box should not contain "All participants" And the "Group" select box should contain "Group A" And the "Group" select box should contain "Group B" And I should see "Post a copy to all groups" Scenario: Students can view all participants discussions in separate groups mode When I am on the "Standard forum name" "forum activity" page logged in as student1 Then I should see "Initial Disc ALL" PK-B\}-%tests/behat/timed_discussions.featurenu[@mod @mod_forum Feature: Users can choose to set start and end time for display of their discussions In order to temporarly hide discussions to students As a teacher I need to set a discussion time start and time end Scenario: Student should not see the tooltip or the discussion Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | And the following "activities" exist: | activity | name | course | idnumber | type | | forum | Test forum name | C1 | forump1 | general | And the following "mod_forum > discussions" exist: | user | forum | name | message | timeend | timestart | | admin | forump1 | Discussion 1 | Discussion contents 1, first message | | | | admin | forump1 | Discussion 2 timed not visible | Discussion contents 2, first message | ##1 Jan 2014 08:00## | | | admin | forump1 | Discussion 3 timed visible now | Discussion contents 3, first message | | 1 | And the following config values are set as admin: | forum_enabletimedposts | 1 | And I am on the "Test forum name" "forum activity" page logged in as admin And I should see "Discussion 2 timed" And I should see "Discussion 3 timed" And "[data-region=timed-label]" "css_element" should exist When I am on the "Test forum name" "forum activity" page logged in as student1 Then I should see "Discussion 1" And I should not see "Discussion 2 timed" And "[data-region=timed-label]" "css_element" should not exist And I should see "Discussion 3 timed" PK-B\--&tests/behat/discussion_display.featurenu[@mod @mod_forum @javascript Feature: Students can choose from 4 discussion display options and their choice is remembered In order to read forum posts in a suitable view As a user I need to select which display method I want to use Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | And the following "activity" exists: | course | C1 | | activity | forum | | name | Test forum name | | idnumber | forum | And the following "mod_forum > discussions" exist: | forum | name | subject | message | | forum | Discussion 1 | Discussion 1 | Discussion contents 1, first message | | forum | Discussion 2 | Discussion 2 | Discussion contents 2, first message | And the following "mod_forum > posts" exist: | parentsubject | subject | message | | Discussion 1 | Reply 1 to discussion 1 | Discussion contents 1, second message | | Discussion 2 | Reply 1 to discussion 2 | Discussion contents 2, second message | Scenario: Display replies flat, with oldest first Given I am on the "Course 1" course page logged in as student1 And I reply "Discussion 1" post from "Test forum name" forum with: | Subject | Reply 2 to discussion 1 | | Message | Discussion contents 1, third message | When I select "Display replies flat, with oldest first" from the "mode" singleselect Then I should see "Discussion contents 1, first message" in the "div.firstpost.starter" "css_element" And I should see "Discussion contents 1, second message" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' forumpost ') and not(contains(@class, 'starter'))]" "xpath_element" And I reply "Discussion 2" post from "Test forum name" forum with: | Subject | Reply 2 to discussion 2 | | Message | Discussion contents 2, third message | And the field "Display mode" matches value "Display replies flat, with oldest first" And I should see "Discussion contents 2, first message" in the "div.firstpost.starter" "css_element" And I should see "Discussion contents 2, second message" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' forumpost ') and not(contains(@class, 'starter'))]" "xpath_element" Scenario: Display replies flat, with newest first Given I am on the "Course 1" course page logged in as student1 And I reply "Discussion 1" post from "Test forum name" forum with: | Subject | Reply 2 to discussion 1 | | Message | Discussion contents 1, third message | When I select "Display replies flat, with newest first" from the "mode" singleselect Then I should see "Discussion contents 1, first message" in the "div.firstpost.starter" "css_element" And I should see "Discussion contents 1, third message" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' forumpost ') and not(contains(@class, 'starter'))]" "xpath_element" And I reply "Discussion 2" post from "Test forum name" forum with: | Subject | Reply 2 to discussion 2 | | Message | Discussion contents 2, third message | And the field "Display mode" matches value "Display replies flat, with newest first" And I should see "Discussion contents 2, first message" in the "div.firstpost.starter" "css_element" And I should see "Discussion contents 2, third message" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' forumpost ') and not(contains(@class, 'starter'))]" "xpath_element" Scenario: Display replies in threaded form Given I am on the "Test forum name" "forum activity" page logged in as student1 And I follow "Discussion 1" When I select "Display replies in threaded form" from the "mode" singleselect Then I should see "Discussion contents 1, first message" And I should see "Reply 1 to discussion 1" in the "div.forumthread" "css_element" And I click on "Test forum name" "link" in the "page-header" "region" And I follow "Discussion 2" And the field "Display mode" matches value "Display replies in threaded form" And I should see "Discussion contents 2, first message" And I should see "Reply 1 to discussion 2" in the "div.forumthread" "css_element" Scenario: Display replies in nested form Given I am on the "Test forum name" "forum activity" page logged in as student1 And I follow "Discussion 1" When I select "Display replies in nested form" from the "mode" singleselect Then I should see "Discussion contents 1, first message" in the "div.firstpost.starter" "css_element" And I should see "Discussion contents 1, second message" in the "div.indent div.forumpost" "css_element" And I click on "Test forum name" "link" in the "page-header" "region" And I follow "Discussion 2" And the field "Display mode" matches value "Display replies in nested form" And I should see "Discussion contents 2, first message" in the "div.firstpost.starter" "css_element" And I should see "Discussion contents 2, second message" in the "div.indent div.forumpost" "css_element" PK-B\ǺQQtests/behat/behat_mod_forum.phpnu[. /** * Steps definitions related with the forum activity. * * @package mod_forum * @category test * @copyright 2013 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); use Behat\Gherkin\Node\TableNode; /** * Forum-related steps definitions. * * @package mod_forum * @category test * @copyright 2013 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_mod_forum extends behat_base { /** * Reset forum caches between tests. * * @BeforeScenario @mod_forum */ public function reset_forum_caches(): void { \mod_forum\subscriptions::reset_discussion_cache(); \mod_forum\subscriptions::reset_forum_cache(); } /** * Adds a topic to the forum specified by it's name. Useful for the Announcements and blog-style forums. * * @Given /^I add a new topic to "(?P(?:[^"]|\\")*)" forum with:$/ * @param string $forumname * @param TableNode $table */ public function i_add_a_new_topic_to_forum_with($forumname, TableNode $table) { $this->add_new_discussion($forumname, $table, get_string('addanewdiscussion', 'forum')); } /** * Adds a Q&A discussion to the Q&A-type forum specified by it's name with the provided table data. * * @Given /^I add a new question to "(?P(?:[^"]|\\")*)" forum with:$/ * @param string $forumname * @param TableNode $table */ public function i_add_a_new_question_to_forum_with($forumname, TableNode $table) { $this->add_new_discussion($forumname, $table, get_string('addanewdiscussion', 'forum')); } /** * Adds a discussion to the forum specified by it's name with the provided table data (usually Subject and Message). The step begins from the forum's course page. * * @Given /^I add a new discussion to "(?P(?:[^"]|\\")*)" forum with:$/ * @param string $forumname * @param TableNode $table */ public function i_add_a_forum_discussion_to_forum_with($forumname, TableNode $table) { $this->add_new_discussion($forumname, $table, get_string('addanewdiscussion', 'forum')); } /** * Adds a discussion to the forum specified by it's name with the provided table data (usually Subject and Message). * The step begins from the forum's course page. * * @Given /^I add a new discussion to "(?P(?:[^"]|\\")*)" forum inline with:$/ * @param string $forumname * @param TableNode $table */ public function i_add_a_forum_discussion_to_forum_inline_with($forumname, TableNode $table) { $this->add_new_discussion_inline($forumname, $table, get_string('addanewdiscussion', 'forum')); } /** * Adds a reply to the specified post of the specified forum. The step begins from the forum's page or from the forum's course page. * * @Given /^I reply "(?P(?:[^"]|\\")*)" post from "(?P(?:[^"]|\\")*)" forum with:$/ * @param string $postname The subject of the post * @param string $forumname The forum name * @param TableNode $table */ public function i_reply_post_from_forum_with($postsubject, $forumname, TableNode $table) { // Navigate to forum. $this->goto_main_post_reply($postsubject); // Fill form and post. $this->execute('behat_forms::i_set_the_following_fields_to_these_values', $table); $this->execute('behat_forms::press_button', get_string('posttoforum', 'forum')); $this->execute('behat_general::i_wait_to_be_redirected'); } /** * Inpage Reply - adds a reply to the specified post of the specified forum. The step begins from the forum's page or from the forum's course page. * * @Given /^I reply "(?P(?:[^"]|\\")*)" post from "(?P(?:[^"]|\\")*)" forum using an inpage reply with:$/ * @param string $postsubject The subject of the post * @param string $forumname The forum name * @param TableNode $table */ public function i_reply_post_from_forum_using_an_inpage_reply_with($postsubject, $forumname, TableNode $table) { // Navigate to forum. $this->execute('behat_navigation::i_am_on_page_instance', [$this->escape($forumname), 'forum activity']); $this->execute('behat_general::click_link', $this->escape($postsubject)); $this->execute('behat_general::click_link', get_string('reply', 'forum')); // Fill form and post. $this->execute('behat_forms::i_set_the_following_fields_to_these_values', $table); $this->execute('behat_forms::press_button', get_string('posttoforum', 'mod_forum')); } /** * Navigates to a particular discussion page * * @Given /^I navigate to post "(?P(?:[^"]|\\")*)" in "(?P(?:[^"]|\\")*)" forum$/ * @param string $postsubject The subject of the post * @param string $forumname The forum name */ public function i_navigate_to_post_in_forum($postsubject, $forumname) { // Navigate to forum discussion. $this->execute('behat_navigation::i_am_on_page_instance', [$this->escape($forumname), 'forum activity']); $this->execute('behat_general::click_link', $this->escape($postsubject)); } /** * Opens up the action menu for the discussion * * @Given /^I click on "(?P(?:[^"]|\\")*)" action menu$/ * @param string $discussion The subject of the discussion */ public function i_click_on_action_menu($discussion) { $this->execute('behat_general::i_click_on_in_the', [ "[data-container='discussion-tools'] [data-bs-toggle='dropdown']", "css_element", "//tr[contains(concat(' ', normalize-space(@class), ' '), ' discussion ') and contains(.,'$discussion')]", "xpath_element" ]); } /** * Creates new discussions within forums of a given course. * * @Given the following forum discussions exist in course :coursename: * @param string $coursename The full name of the course where the forums exist. * @param TableNode $discussionsdata The discussion posts to be created. */ public function the_following_forum_discussions_exist(string $coursename, TableNode $discussionsdata) { global $DB; $courseid = $this->get_course_id($coursename); $forumgenerator = behat_util::get_data_generator()->get_plugin_generator('mod_forum'); // Add the discussions to the relevant forum. foreach ($discussionsdata->getHash() as $discussioninfo) { $discussioninfo['course'] = $courseid; $discussioninfo['forum'] = $this->get_forum_id($courseid, $discussioninfo['forum']); $discussioninfo['userid'] = $this->get_user_id($discussioninfo['user']); // Prepare data for any attachments. if (!empty($discussioninfo['attachments']) || !empty($discussioninfo['inlineattachments'])) { $discussioninfo['attachment'] = 1; $cm = get_coursemodule_from_instance('forum', $discussioninfo['forum']); } // Prepare data for groups if needed. if (!empty($discussioninfo['group'])) { $discussioninfo['groupid'] = $this->get_group_id($courseid, $discussioninfo['group']); unset($discussioninfo['group']); } // Create the discussion post. $discussion = $forumgenerator->create_discussion($discussioninfo); $postid = $DB->get_field('forum_posts', 'id', ['discussion' => $discussion->id]); // Override the creation and modified timestamps as required. if (!empty($discussioninfo['created']) || !empty($discussioninfo['modified'])) { $discussiondata = [ 'id' => $discussion->id, 'timemodified' => empty($discussioninfo['modified']) ? $discussioninfo['created'] : $discussioninfo['modified'], ]; $DB->update_record('forum_discussions', $discussiondata); $postdata = [ 'id' => $postid, 'modified' => empty($discussioninfo['modified']) ? $discussioninfo['created'] : $discussioninfo['modified'], ]; if (!empty($discussioninfo['created'])) { $postdata['created'] = $discussioninfo['created']; } $DB->update_record('forum_posts', $postdata); } // Create attachments to the discussion post if required. if (!empty($discussioninfo['attachments'])) { $attachments = array_map('trim', explode(',', $discussioninfo['attachments'])); $this->create_post_attachments($postid, $discussioninfo['userid'], $attachments, $cm, 'attachment'); } // Create inline attachments to the discussion post if required. if (!empty($discussioninfo['inlineattachments'])) { $inlineattachments = array_map('trim', explode(',', $discussioninfo['inlineattachments'])); $this->create_post_attachments($postid, $discussioninfo['userid'], $inlineattachments, $cm, 'post'); } } } /** * Creates replies to discussions within forums of a given course. * * @Given the following forum replies exist in course :coursename: * @param string $coursename The full name of the course where the forums exist. * @param TableNode $repliesdata The reply posts to be created. */ public function the_following_forum_replies_exist(string $coursename, TableNode $repliesdata) { global $DB; $courseid = $this->get_course_id($coursename); $forumgenerator = behat_util::get_data_generator()->get_plugin_generator('mod_forum'); // Add the replies to the relevant discussions. foreach ($repliesdata->getHash() as $replyinfo) { $replyinfo['course'] = $courseid; $replyinfo['forum'] = $this->get_forum_id($courseid, $replyinfo['forum']); $replyinfo['userid'] = $this->get_user_id($replyinfo['user']); [ 'discussionid' => $replyinfo['discussion'], 'parentid' => $replyinfo['parent'], ] = $this->get_base_discussion($replyinfo['forum'], $replyinfo['discussion']); // Prepare data for any attachments. if (!empty($replyinfo['attachments']) || !empty($replyinfo['inlineattachments'])) { $replyinfo['attachment'] = 1; $cm = get_coursemodule_from_instance('forum', $replyinfo['forum']); } // Get the user id of the user to whom the reply is private. if (!empty($replyinfo['privatereplyto'])) { $replyinfo['privatereplyto'] = $this->get_user_id($replyinfo['privatereplyto']); } // Create the reply post. $reply = $forumgenerator->create_post($replyinfo); // Create attachments to the post if required. if (!empty($replyinfo['attachments'])) { $attachments = array_map('trim', explode(',', $replyinfo['attachments'])); $this->create_post_attachments($reply->id, $replyinfo['userid'], $attachments, $cm, 'attachment'); } // Create inline attachments to the post if required. if (!empty($replyinfo['inlineattachments'])) { $inlineattachments = array_map('trim', explode(',', $replyinfo['inlineattachments'])); $this->create_post_attachments($reply->id, $replyinfo['userid'], $inlineattachments, $cm, 'post'); } } } /** * Checks if the user can subscribe to the forum. * * @Given /^I can subscribe to this forum$/ */ public function i_can_subscribe_to_this_forum() { $this->execute('behat_general::assert_page_contains_text', [get_string('subscribe', 'mod_forum')]); } /** * Checks if the user can unsubscribe from the forum. * * @Given /^I can unsubscribe from this forum$/ */ public function i_can_unsubscribe_from_this_forum() { $this->execute('behat_general::assert_page_contains_text', [get_string('unsubscribe', 'mod_forum')]); } /** * Subscribes to the forum. * * @Given /^I subscribe to this forum$/ */ public function i_subscribe_to_this_forum() { $this->execute('behat_general::click_link', [get_string('subscribe', 'mod_forum')]); } /** * Unsubscribes from the forum. * * @Given /^I unsubscribe from this forum$/ */ public function i_unsubscribe_from_this_forum() { $this->execute('behat_general::click_link', [get_string('unsubscribe', 'mod_forum')]); } /** * Fetch user ID from its username. * * @param string $username The username. * @return int The user ID. * @throws Exception */ protected function get_user_id($username) { global $DB; if (!$userid = $DB->get_field('user', 'id', ['username' => $username])) { throw new Exception("A user with username '{$username}' does not exist"); } return $userid; } /** * Fetch course ID using course name. * * @param string $coursename The name of the course. * @return int The course ID. * @throws Exception */ protected function get_course_id(string $coursename): int { global $DB; if (!$courseid = $DB->get_field('course', 'id', ['fullname' => $coursename])) { throw new Exception("A course with name '{$coursename}' does not exist"); } return $courseid; } /** * Fetch forum ID using forum name. * * @param int $courseid The course ID the forum exists within. * @param string $forumname The name of the forum. * @return int The forum ID. * @throws Exception */ protected function get_forum_id(int $courseid, string $forumname): int { global $DB; $conditions = [ 'course' => $courseid, 'name' => $forumname, ]; if (!$forumid = $DB->get_field('forum', 'id', $conditions)) { throw new Exception("A forum with name '{$forumname}' does not exist in the provided course"); } return $forumid; } /** * Fetch Group ID using group name. * * @param int $courseid The course ID the forum exists within. * @param string $groupname The short name of the group. * @return int The group ID. * @throws Exception */ protected function get_group_id(int $courseid, string $groupname): int { global $DB; if ($groupname === 'All participants') { return -1; } $conditions = [ 'courseid' => $courseid, 'idnumber' => $groupname, ]; if (!$groupid = $DB->get_field('groups', 'id', $conditions)) { throw new Exception("A group with name '{$groupname}' does not exist in the provided course"); } return $groupid; } /** * Fetch discussion ID and first post ID by discussion name. * * @param int $forumid The forum ID where the discussion resides. * @param string $name The name of the discussion. * @return array The discussion ID and first post ID. * @throws dml_exception If the discussion name is not unique within the forum (or doesn't exist). */ protected function get_base_discussion(int $forumid, string $name): array { global $DB; $conditions = [ 'name' => $name, 'forum' => $forumid, ]; $result = $DB->get_record("forum_discussions", $conditions, 'id, firstpost', MUST_EXIST); return [ 'discussionid' => $result->id, 'parentid' => $result->firstpost, ]; } /** * Create one or more attached or inline attachments to a forum post. * * @param int $postid The ID of the forum post. * @param int $userid The user ID creating the attachment. * @param array $attachmentnames Names of all attachments to be created. * @param stdClass $cm The context module of the forum. * @param string $filearea The file area being written to, eg 'attachment' or 'post' (inline). */ protected function create_post_attachments(int $postid, int $userid, array $attachmentnames, stdClass $cm, string $filearea): void { $filestorage = get_file_storage(); foreach ($attachmentnames as $attachmentname) { $filestorage->create_file_from_string( [ 'contextid' => context_module::instance($cm->id)->id, 'component' => 'mod_forum', 'filearea' => $filearea, 'itemid' => $postid, 'filepath' => '/', 'filename' => $attachmentname, 'userid' => $userid, ], "File content {$attachmentname}" ); } } /** * Returns the steps list to add a new discussion to a forum. * * Abstracts add a new topic and add a new discussion, as depending * on the forum type the button string changes. * * @param string $forumname * @param TableNode $table * @param string $buttonstr */ protected function add_new_discussion($forumname, TableNode $table, $buttonstr) { // Navigate to forum. $this->execute('behat_navigation::i_am_on_page_instance', [$this->escape($forumname), 'forum activity']); $this->execute('behat_general::click_link', $buttonstr); $this->execute('behat_forms::press_button', get_string('showadvancededitor')); $this->fill_new_discussion_form($table); } /** * Returns the steps list to add a new discussion to a forum inline. * * Abstracts add a new topic and add a new discussion, as depending * on the forum type the button string changes. * * @param string $forumname * @param TableNode $table * @param string $buttonstr */ protected function add_new_discussion_inline($forumname, TableNode $table, $buttonstr) { // Navigate to forum. $this->execute('behat_navigation::i_am_on_page_instance', [$this->escape($forumname), 'forum activity']); $this->execute('behat_general::click_link', $buttonstr); $this->fill_new_discussion_form($table); } /** * Fill in the forum's post form and submit. It assumes you've already navigated and enabled the form for view. * * @param TableNode $table * @throws coding_exception */ protected function fill_new_discussion_form(TableNode $table) { // Fill form and post. $this->execute('behat_forms::i_set_the_following_fields_to_these_values', $table); $this->execute('behat_forms::press_button', get_string('posttoforum', 'forum')); $this->execute('behat_general::i_wait_to_be_redirected'); } /** * Go to the default reply to post page. * This is used instead of navigating through 4-5 different steps and to solve issues where JS would be required to click * on the advanced button * * @param $postsubject * @throws coding_exception * @throws dml_exception * @throws moodle_exception */ protected function goto_main_post_reply($postsubject) { global $DB; $post = $DB->get_record("forum_posts", array("subject" => $postsubject), 'id', MUST_EXIST); $url = new moodle_url('/mod/forum/post.php', ['reply' => $post->id]); $this->execute('behat_general::i_visit', [$url]); } } PK-B\k .tests/behat/forum_activity_breadcrumbs.featurenu[@mod @mod_forum @javascript Feature: A teacher or admin can view breadcrumbs in the reply, delete, split, edit and add discussion advanced pages Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher | Teacher | Tom | teacher@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher | C1 | editingteacher | And the following "activity" exists: | course | C1 | | activity | forum | | name | Test forum name | | idnumber | forum1 | Scenario: A teacher views add discussion topics advanced page Given I am on the "Test forum name" "forum activity" page logged in as teacher And I click on "Add discussion topic" "link" When I click on "Advanced" "button" And I should see "Add discussion topic" Then I should see "Add discussion topic" in the ".breadcrumb" "css_element" And I should see "Test forum name" in the ".breadcrumb" "css_element" Scenario: A teacher adds posts and then verifies the breadcrumbs in the links Given the following "mod_forum > discussions" exist: | user | forum | name | message | | teacher | forum1 | Test post subject one | Test post message one | And the following "mod_forum > posts" exist: | user | parentsubject | subject | message | | teacher | Test post subject one | Reply 1 to discussion 1 | Discussion contents 1, second message | And I am on the "Test forum name" "forum activity" page logged in as teacher And I follow "Test post subject one" When I follow "Edit" Then I should see "Edit discussion topic" And I should see "Edit discussion topic" in the ".breadcrumb" "css_element" And I should see "Test post subject one" in the ".breadcrumb" "css_element" And I click on "Cancel" "button" And I follow "Delete" And I should not see "Test forum description" And I should see "Delete" in the ".breadcrumb" "css_element" And I click on "Cancel" "button" And I follow "Reply" And I click on "Advanced" "button" And I should not see "Test forum description" And I should see "Reply to discussion" And I should see "Add reply" in the ".breadcrumb" "css_element" And I click on "Cancel" "button" And I follow "Split" And I should not see "Test forum description" And I should see "Split discussion" in the ".breadcrumb" "css_element" And I click on "Cancel" "button" PK-B\)QY4 4 %tests/behat/edit_post_student.featurenu[@mod @mod_forum Feature: Students can edit or delete their forum posts within a set time limit In order to refine forum posts As a user I need to edit or delete my forum posts within a certain period of time after posting Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | And the following "activities" exist: | activity | name | course | idnumber | | forum | Test forum name | C1 | forum | Scenario: Edit forum post Given the following "mod_forum > discussions" exist: | user | forum | name | message | | student1 | forum | Forum post subject | This is the body | And I am on the "Test forum name" "forum activity" page logged in as "student1" And I follow "Forum post subject" And I follow "Edit" When I set the following fields to these values: | Subject | Edited post subject | | Message | Edited post body | And I press "Save changes" And I wait to be redirected Then I should see "Edited post subject" And I should see "Edited post body" Scenario: Delete forum post Given the following "mod_forum > discussions" exist: | user | forum | name | message | | student1 | forum | Forum post subject | This is the body | And I am on the "Test forum name" "forum activity" page logged in as "student1" And I follow "Forum post subject" When I follow "Delete" And I press "Continue" Then I should not see "Forum post subject" @block_recent_activity Scenario: Time limit expires Given the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | recent_activity | Course | C1 | course-view-* | side-pre | And the following config values are set as admin: | maxeditingtime | 1 | And the following "mod_forum > discussions" exist: | user | forum | name | message | timemodified | | student1 | forum | Forum post subject | This is the body | ##now +1 second## | And I am on the "Course 1" course page logged in as student1 And I should see "New forum posts:" in the "Recent activity" "block" And I should see "Forum post subject" in the "Recent activity" "block" When I wait "2" seconds And I follow "Forum post subject" Then I should not see "Edit" in the "region-main" "region" And I should not see "Delete" in the "region-main" "region" PK-B\ˈ#tests/behat/discussion_lock.featurenu[@mod @mod_forum @javascript Feature: As a teacher, you can manually lock individual discussions when viewing the discussion Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | And the following "activity" exists: | course | C1 | | activity | forum | | name | Test forum name | | idnumber | forum1 | And the following "mod_forum > discussions" exist: | user | forum | name | message | | admin | forum1 | Discussion 1 | Discussion contents 1, first message | | admin | forum1 | Discussion 2 | Discussion contents 2, first message | And the following "mod_forum > posts" exist: | user | parentsubject | subject | message | | admin | Discussion 1 | Reply 1 to discussion 1 | Discussion contents 1, second message | | admin | Discussion 2 | Reply 1 to discussion 2 | Discussion contents 2, second message | Scenario: Lock a discussion and view Given I am on the "Course 1" course page logged in as admin And I navigate to post "Discussion 1" in "Test forum name" forum And I press "Settings" Then "Lock this discussion" "link" should be visible And I follow "Lock this discussion" Then I should see "This discussion has been locked so you can no longer reply to it." And I press "Settings" Then "Lock this discussion" "link" should not be visible Then "Unlock this discussion" "link" should be visible And I press "Settings" And I follow "Discussion 2" Then I should not see "This discussion has been locked so you can no longer reply to it." And I am on the "Course 1" course page logged in as student1 And I navigate to post "Discussion 1" in "Test forum name" forum Then I should see "This discussion has been locked so you can no longer reply to it." And "Reply" "link" should not be visible @accessibility Scenario: A locked discussion must be accessible Given I am on the "Course 1" course page logged in as admin And I navigate to post "Discussion 1" in "Test forum name" forum When I reply "Discussion 1" post from "Test forum name" forum with: | Subject | Discussion 1: Hello world! | | Message | Discussion contents 1, hello world! | # Check discussion view accessibility with success notification shown on post. Then the page should meet accessibility standards with "wcag143" extra tests And I press "Settings" When I follow "Lock this discussion" # Check discussion view accessibility with info notification shown when discussion is locked. And the page should meet accessibility standards with "wcag143" extra tests And I am on the "Test forum name" "forum activity" page logged in as student1 # Check discussion list accessibility with danger pill shown when discussion is locked. And the page should meet accessibility standards with "wcag143" extra tests And I follow "Preferences" in the user menu And I click on "Forum preferences" "link" And I set the following fields to these values: | Use experimental nested discussion view | Yes | And I press "Save changes" And I am on "Course 1" course homepage And I navigate to post "Discussion 1" in "Test forum name" forum # Check experimental discussion view accessibility with danger pill shown when discussion is locked. And the page should meet accessibility standards with "wcag143" extra tests PK-B\:\w5tests/behat/discussion_post_threshold_warning.featurenu[@mod @mod_forum Feature: An admin or teacher sets the post threshold for blocking and warning A student should not be able to post more than blocking value Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | | student2 | Student | 1 | student2@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student2 | C1 | student | And the following "activities" exist: | activity | name | course | blockperiod | blockafter | warnafter | | forum | Test forum name | C1 | 172800 | 3 | 2 | @javascript Scenario: A student should not be able to post new discussion or reply to the existing discussion once the threshold block count is reached Given I am on the "Test forum name" "forum activity" page logged in as student1 When I add a new discussion to "Test forum name" forum with: | Subject | Test post subject one | | Message | Test post message one | And I add a new discussion to "Test forum name" forum with: | Subject | Test post subject two | | Message | Test post message two | And I should see "Add discussion topic" # Verify that when navigated to one of the topics and then click reply the warning notification is shown. And I click on "Test post subject two" "link" And I click on "Reply" "link" And I should see "You are approaching the posting threshold. You have posted 2 times in the last 2 days and the limit is 3 posts." And I click on "Test forum name" "link" And I add a new discussion to "Test forum name" forum with: | Subject | Test post subject three | | Message | Test post message three | Then I should not see "Add discussion topic" # Verify that no reply link available in the posts. And I click on "Test post subject three" "link" And I should not see "Reply" And I am on the "Test forum name" "forum activity" page And I click on "Test post subject two" "link" And I should not see "Reply" # Verify that student2 is not affected by the posts made by student1 And I am on the "Test forum name" "forum activity" page logged in as student2 And I should see "Add discussion topic" @javascript Scenario: A student should see warning when the post is about to reach threshold when experimental nested discussion view is set Given I am on the "Test forum name" "forum activity" page logged in as student1 When I add a new discussion to "Test forum name" forum with: | Subject | Test post subject one | | Message | Test post message one | And I add a new discussion to "Test forum name" forum with: | Subject | Test post subject two | | Message | Test post message two | And I should see "Add discussion topic" #Now verify that when "Use experimental nested discussion view" is set, the user should see same warning in the Reply. And I follow "Preferences" in the user menu And I click on "Forum preferences" "link" And I set the field "Use experimental nested discussion view" to "Yes" And I press "Save changes" And I am on the "Test forum name" "forum activity" page And I click on "Test post subject two" "link" When I press "Reply" Then I should see "You are approaching the posting threshold. You have posted 2 times in the last 2 days and the limit is 3 posts." @javascript Scenario: A student with 'mod/forum:postwithoutthrottling' capability should be able to post unlimited number of times Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | mod/forum:postwithoutthrottling | Allow | student | Course | C1 | And I am on the "Test forum name" "forum activity" page logged in as student1 And I should see "This forum has a limit to the number of forum postings you can make in a given time period - this is currently set at 3 posting(s) in 2 days" When I add a new discussion to "Test forum name" forum with: | Subject | Test post subject one | | Message | Test post message one | And I add a new discussion to "Test forum name" forum with: | Subject | Test post subject two | | Message | Test post message two | And I should see "Add discussion topic" # Verify that when navigated to one of the topics and then click reply the warning notification is shown. And I click on "Test post subject two" "link" And I click on "Reply" "link" # With 'mod/forum:postwithoutthrottling' assigned capability the message below should not be displayed. And I should not see "You are approaching the posting threshold. You have posted 2 times in the last 2 days and the limit is 3 posts." And I click on "Test forum name" "link" And I add a new discussion to "Test forum name" forum with: | Subject | Test post subject three | | Message | Test post message three | Then I should see "Add discussion topic" # Verify that reply link is available in the posts. And I click on "Test post subject three" "link" And I should see "Reply" And I am on the "Test forum name" "forum activity" page And I click on "Test post subject two" "link" And I should see "Reply" PK-B\/5; ; ;tests/behat/separate_group_single_group_discussions.featurenu[@mod @mod_forum Feature: Posting to groups in a separate group discussion when restricted to groupings In order to post to groups in a forum with separate groups and groupings As a teacher I need to have groups configured to post to a group Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | teacher1 | teacher1 | teacher1@example.com | | teacher2 | teacher2 | teacher2 | teacher2@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher2 | C1 | teacher | And the following "groups" exist: | name | course | idnumber | | G1G1 | C1 | G1G1 | | G1G2 | C1 | G1G2 | | G2G1 | C1 | G2G1 | And the following "groupings" exist: | name | course | idnumber | | G1 | C1 | G1 | | G2 | C1 | G2 | And the following "group members" exist: | user | group | | teacher1 | G1G1 | | teacher1 | G1G2 | | teacher1 | G2G1 | | teacher2 | G1G1 | | teacher2 | G1G2 | | teacher2 | G2G1 | And the following "grouping groups" exist: | grouping | group | | G1 | G1G1 | | G1 | G1G2 | | G2 | G2G1 | And the following "activities" exist: | activity | course | idnumber | name | intro | type | section | groupmode | grouping | | forum | C1 | 00001 | Multiple groups forum | Standard forum description | general | 1 | 1 | G1 | | forum | C1 | 00001 | Single groups forum | Standard forum description | general | 1 | 1 | G2 | Scenario: Teacher with accessallgroups can post in all groups Given I am on the "Multiple groups forum" "forum activity" page logged in as teacher1 When I click on "Add discussion topic" "link" And I click on "Advanced" "button" Then the "Group" select box should contain "All participants" And the "Group" select box should contain "G1G1" And the "Group" select box should contain "G1G2" And I am on "Course 1" course homepage And I follow "Single groups forum" And I click on "Add discussion topic" "link" And I click on "Advanced" "button" And the "Group" select box should contain "All participants" And the "Group" select box should contain "G2G1" And I should not see "Post a copy to all groups" Scenario: Teacher in all groups but without accessallgroups can post in either group but not to All Participants Given I am on the "Multiple groups forum" "forum activity" page logged in as teacher2 When I click on "Add discussion topic" "link" And I click on "Advanced" "button" Then the "Group" select box should not contain "All participants" And the "Group" select box should contain "G1G1" And the "Group" select box should contain "G1G2" And I am on "Course 1" course homepage And I follow "Single groups forum" And I click on "Add discussion topic" "link" And I click on "Advanced" "button" And I should see "G2G1" And "Group" "select" should not exist PK-B\88-tests/behat/visible_group_discussions.featurenu[@mod @mod_forum Feature: Posting to all groups in a visible group discussion is restricted to users with access to all groups In order to post to all groups in a forum with visible groups As a teacher I need to have the accessallgroups capability Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | | student3 | Student | 3 | student3@example.com | | student4 | Student | 4 | student4@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | | student3 | C1 | student | | student4 | C1 | student | And the following "groups" exist: | name | course | idnumber | participation | | Group A | C1 | G1 | 1 | | Group B | C1 | G2 | 1 | | Group C | C1 | G3 | 1 | | Group D | C1 | G4 | 0 | And the following "group members" exist: | user | group | | teacher1 | G1 | | teacher1 | G2 | | student1 | G1 | | student2 | G1 | | student2 | G2 | | student3 | G4 | And the following "activities" exist: | activity | name | course | idnumber | groupmode | | forum | Standard forum name | C1 | groups | 2 | And the following "mod_forum > discussions" exist: | forum | name | subject | message | group | | groups | Initial Disc ALL | Initial Disc ALL | Disc ALL content | All participants | | groups | Initial Disc G1 | Initial Disc G1 | Disc G1 content | G1 | | groups | Initial Disc G2 | Initial Disc G2 | Disc G2 content | G2 | | groups | Initial Disc G3 | Initial Disc G3 | Disc G3 content | G3 | Scenario: Teacher with accessallgroups can view all groups When I am on the "Standard forum name" "forum activity" page logged in as teacher1 Then the "Visible groups" select box should contain "All participants" And the "Visible groups" select box should contain "Group A" And the "Visible groups" select box should contain "Group B" And the "Visible groups" select box should contain "Group C" And the "Visible groups" select box should not contain "Group D" And I select "All participants" from the "Visible groups" singleselect And I should see "Initial Disc ALL" And I should see "Initial Disc G1" And I should see "Initial Disc G2" And I should see "Initial Disc G2" And I select "Group A" from the "Visible groups" singleselect And I should see "Initial Disc ALL" And I should see "Initial Disc G1" But I should not see "Initial Disc G2" And I should not see "Initial Disc G3" And I select "Group B" from the "Visible groups" singleselect And I should see "Initial Disc ALL" And I should see "Initial Disc G2" But I should not see "Initial Disc G1" And I should not see "Initial Disc G3" Scenario: Teacher with accessallgroups can select any group when posting Given I am on the "Standard forum name" "forum activity" page logged in as teacher1 When I click on "Add discussion topic" "link" And I click on "Advanced" "button" Then the "Group" select box should contain "All participants" And the "Group" select box should contain "Group A" And the "Group" select box should contain "Group B" And the "Group" select box should contain "Group C" And the "Group" select box should not contain "Group D" And I should see "Post a copy to all groups" Scenario: Teacher with accessallgroups can post in groups they are a member of Given I am on the "Standard forum name" "forum activity" page logged in as teacher1 And I select "Group A" from the "Visible groups" singleselect When I click on "Add discussion topic" "link" And I click on "Advanced" "button" Then I should see "Post a copy to all groups" And I set the following fields to these values: | Subject | Teacher 1 -> Group B | | Message | Teacher 1 -> Group B | # Change the group in the post form. | Group | Group B | And I press "Post to forum" And I wait to be redirected # We should be redirected to the group that we selected when posting. And the field "Visible groups" matches value "Group B" And I should see "Group B" in the "Teacher 1 -> Group B" "table_row" And I should not see "Group A" in the "Teacher 1 -> Group B" "table_row" And I should not see "Group C" in the "Teacher 1 -> Group B" "table_row" # It should also be displayed under All participants And I select "All participants" from the "Visible groups" singleselect And I should see "Group B" in the "Teacher 1 -> Group B" "table_row" And I should not see "Group A" in the "Teacher 1 -> Group B" "table_row" And I should not see "Group C" in the "Teacher 1 -> Group B" "table_row" # It should not be displayed in Groups A, or C. And I select "Group A" from the "Visible groups" singleselect And I should not see "Teacher 1 -> Group B" And I select "Group C" from the "Visible groups" singleselect And I should not see "Teacher 1 -> Group B" Scenario: Teacher with accessallgroups can post in groups they are not a member of Given I am on the "Standard forum name" "forum activity" page logged in as teacher1 And I select "Group A" from the "Visible groups" singleselect When I click on "Add discussion topic" "link" And I click on "Advanced" "button" Then I should see "Post a copy to all groups" And I set the following fields to these values: | Subject | Teacher 1 -> Group C | | Message | Teacher 1 -> Group C | | Group | Group C | And I press "Post to forum" And I wait to be redirected # We should be redirected to the group that we selected when posting. And the field "Visible groups" matches value "Group C" # We redirect to the group posted in automatically. And I should see "Group C" in the "Teacher 1 -> Group C" "table_row" And I should not see "Group A" in the "Teacher 1 -> Group C" "table_row" And I should not see "Group B" in the "Teacher 1 -> Group C" "table_row" # It should also be displayed under All participants And I select "All participants" from the "Visible groups" singleselect And I should see "Group C" in the "Teacher 1 -> Group C" "table_row" And I should not see "Group A" in the "Teacher 1 -> Group C" "table_row" And I should not see "Group B" in the "Teacher 1 -> Group C" "table_row" # It should not be displayed in Groups A, or B. And I select "Group A" from the "Visible groups" singleselect And I should not see "Teacher 1 -> Group C" And I select "Group B" from the "Visible groups" singleselect And I should not see "Teacher 1 -> Group C" Scenario: Teacher with accessallgroups can post to all groups Given I am on the "Standard forum name" "forum activity" page logged in as teacher1 When I click on "Add discussion topic" "link" And I click on "Advanced" "button" And I set the following fields to these values: | Subject | Teacher 1 -> Post to all | | Message | Teacher 1 -> Post to all | | Post a copy to all groups | 1 | And I press "Post to forum" And I wait to be redirected # Posting to all groups means that we should be redirected to the page we started from. And the field "Visible groups" matches value "All participants" And I select "Group A" from the "Visible groups" singleselect Then I should see "Group A" in the "Teacher 1 -> Post to all" "table_row" And I should not see "Group B" in the "Teacher 1 -> Post to all" "table_row" And I should not see "Group C" in the "Teacher 1 -> Post to all" "table_row" And I select "Group B" from the "Visible groups" singleselect And I should see "Group B" in the "Teacher 1 -> Post to all" "table_row" And I should not see "Group A" in the "Teacher 1 -> Post to all" "table_row" And I should not see "Group C" in the "Teacher 1 -> Post to all" "table_row" And I select "Group C" from the "Visible groups" singleselect And I should see "Group C" in the "Teacher 1 -> Post to all" "table_row" And I should not see "Group A" in the "Teacher 1 -> Post to all" "table_row" And I should not see "Group B" in the "Teacher 1 -> Post to all" "table_row" # No point testing the "All participants". Scenario: Students can view all groups When I am on the "Standard forum name" "forum activity" page logged in as student1 Then the "Visible groups" select box should contain "All participants" And the "Visible groups" select box should contain "Group A" And the "Visible groups" select box should contain "Group B" And the "Visible groups" select box should contain "Group C" And the "Visible groups" select box should not contain "Group D" And I select "All participants" from the "Visible groups" singleselect And I should see "Initial Disc ALL" And I should see "Initial Disc G1" And I should see "Initial Disc G2" And I should see "Initial Disc G2" And I select "Group A" from the "Visible groups" singleselect And I should see "Initial Disc ALL" And I should see "Initial Disc G1" But I should not see "Initial Disc G2" And I should not see "Initial Disc G3" And I select "Group B" from the "Visible groups" singleselect And I should see "Initial Disc ALL" And I should see "Initial Disc G2" But I should not see "Initial Disc G1" And I should not see "Initial Disc G3" Scenario: Students in one group can only post in their group When I am on the "Standard forum name" "forum activity" page logged in as student1 Then I should see "Group A" And I click on "Add discussion topic" "link" And I click on "Advanced" "button" And I should see "Group A" And I should not see "Group B" And I should not see "Group C" And I should not see "Group D" And I should not see "Post a copy to all groups" And I set the following fields to these values: | Subject | Student -> B | | Message | Student -> B | And I press "Post to forum" And I wait to be redirected And I should see "Group A" in the "Student -> B" "table_row" And I should not see "Group B" in the "Student -> B" "table_row" Scenario: Students in multiple group can post in all of their group individually When I am on the "Standard forum name" "forum activity" page logged in as student2 And I select "Group A" from the "Visible groups" singleselect And I click on "Add discussion topic" "link" And I click on "Advanced" "button" And the "Group" select box should not contain "All participants" And the "Group" select box should contain "Group A" And the "Group" select box should contain "Group B" And the "Group" select box should not contain "Group C" And the "Group" select box should not contain "Group D" And I should not see "Post a copy to all groups" And I set the following fields to these values: | Subject | Student -> B | | Message | Student -> B | | Group | Group B | And I press "Post to forum" And I wait to be redirected # We should be redirected to the group that we selected when posting. And the field "Visible groups" matches value "Group B" And I should see "Group B" in the "Student -> B" "table_row" And I should not see "Group A" in the "Student -> B" "table_row" And I select "Group A" from the "Visible groups" singleselect And I should not see "Student -> B" # Now try posting in Group A (starting at Group B) And I select "Group B" from the "Visible groups" singleselect And I click on "Add discussion topic" "link" And I click on "Advanced" "button" And the "Group" select box should not contain "All participants" And the "Group" select box should contain "Group A" And the "Group" select box should contain "Group B" And the "Group" select box should not contain "Group C" And I should not see "Post a copy to all groups" And I set the following fields to these values: | Subject | Student -> A | | Message | Student -> A | | Group | Group A | And I press "Post to forum" And I wait to be redirected # We should be redirected to the group that we selected when posting. And the field "Visible groups" matches value "Group A" And I should see "Group A" in the "Student -> A" "table_row" And I should not see "Group B" in the "Student -> A" "table_row" And I select "Group B" from the "Visible groups" singleselect And I should not see "Student -> A" Scenario: Students in no group can see all discussions, but not post. Given I log in as "student4" And I am on "Course 1" course homepage When I follow "Standard forum name" And I select "All participants" from the "Visible groups" singleselect Then I should see "Initial Disc ALL" And I should see "Initial Disc G1" And I should see "Initial Disc G2" And I should see "Initial Disc G2" And I should see "You are not able to create a discussion because you are not a member of any group." And I should not see "Add discussion topic" Scenario: Students in non-participation groups can see all discussions, but not post. Given I log in as "student3" And I am on "Course 1" course homepage When I follow "Standard forum name" And I select "All participants" from the "Visible groups" singleselect Then I should see "Initial Disc ALL" And I should see "Initial Disc G1" And I should see "Initial Disc G2" And I should see "Initial Disc G2" And I should see "You do not have permission to add a new discussion topic for all participants." And I should not see "Add discussion topic" PK-B\֬ܒ@@/tests/behat/forum_subscriptions_default.featurenu[@mod @mod_forum Feature: A user can control their default discussion subscription settings In order to automatically subscribe to discussions As a user I can choose my default subscription preference Background: Given the following "users" exist: | username | firstname | lastname | email | autosubscribe | | student1 | Student | One | student.one@example.com | 1 | | student2 | Student | Two | student.one@example.com | 0 | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student2 | C1 | student | And the following "activities" exist: | activity | name | course | idnumber | type | | forum | Test forum name | C1 | forump1 | general | And I am on the "Test forum name" "forum activity editing" page logged in as admin And I set the following fields to these values: | Subscription mode | Optional subscription | And I press "Save and return to course" Scenario: Creating a new discussion in an optional forum follows user preferences Given I am on the "Test forum name" "forum activity" page logged in as student1 When I click on "Add discussion topic" "link" And I click on "Advanced" "button" Then "input[name=discussionsubscribe]:checked" "css_element" should exist And I am on the "Test forum name" "forum activity" page logged in as student2 And I click on "Add discussion topic" "link" And I click on "Advanced" "button" And "input[name=discussionsubscribe]:not(:checked)" "css_element" should exist Scenario: Replying to an existing discussion in an optional forum follows user preferences Given the following forum discussions exist in course "Course 1": | user | forum | name | message | | admin | Test forum name | Test post subject | Test post message | And I am on the "Test forum name" "forum activity" page logged in as student1 And I follow "Test post subject" When I follow "Reply" Then "input[name=discussionsubscribe]:checked" "css_element" should exist And I am on the "Test forum name" "forum activity" page logged in as student2 And I follow "Test post subject" And I follow "Reply" And "input[name=discussionsubscribe]:not(:checked)" "css_element" should exist Scenario: Creating a new discussion in an automatic forum follows forum subscription Given I am on the "Test forum name" "forum activity editing" page logged in as admin And I set the following fields to these values: | Subscription mode | Auto subscription | And I press "Save and return to course" And I am on the "Test forum name" "forum activity" page logged in as student1 When I click on "Add discussion topic" "link" And I click on "Advanced" "button" Then "input[name=discussionsubscribe]:checked" "css_element" should exist And I am on the "Test forum name" "forum activity" page logged in as student2 And I click on "Add discussion topic" "link" And I click on "Advanced" "button" And "input[name=discussionsubscribe]:checked" "css_element" should exist Scenario: Replying to an existing discussion in an automatic forum follows forum subscription Given the following forum discussions exist in course "Course 1": | user | forum | name | message | | admin | Test forum name | Test post subject | Test post message | And I am on the "Test forum name" "forum activity editing" page logged in as admin And I set the following fields to these values: | Subscription mode | Optional subscription | And I press "Save and return to course" And I am on the "Test forum name" "forum activity" page logged in as student1 And I follow "Test post subject" When I follow "Reply" Then "input[name=discussionsubscribe]:checked" "css_element" should exist And I am on the "Test forum name" "forum activity" page logged in as student2 And I follow "Test post subject" And I follow "Reply" And "input[name=discussionsubscribe]:not(:checked)" "css_element" should exist @javascript Scenario: Replying to an existing discussion in an automatic forum which has been unsubscribed from follows user preferences Given the following forum discussions exist in course "Course 1": | user | forum | name | message | | admin | Test forum name | Test post subject | Test post message | And I am on the "Test forum name" "forum activity editing" page logged in as admin And I set the following fields to these values: | Subscription mode | Auto subscription | And I press "Save and return to course" And I am on the "Test forum name" "forum activity" page logged in as student1 And I click on "input[id^=subscription-toggle]" "css_element" in the "Test post subject" "table_row" And I follow "Test post subject" When I follow "Reply" And I click on "Advanced" "button" And "input[name=discussionsubscribe][checked]" "css_element" should exist And I am on the "Test forum name" "forum activity" page logged in as student2 And I click on "input[id^=subscription-toggle]" "css_element" in the "Test post subject" "table_row" And I follow "Test post subject" And I follow "Reply" And I click on "Advanced" "button" And "input[name=discussionsubscribe]:not([checked])" "css_element" should exist PK-B\ C3 3 7tests/behat/groups_in_course_no_groups_in_forum.featurenu[@mod @mod_forum Feature: Forums in 'No groups' mode allow posting to All participants for all users In order to post to a forum in 'No groups' mode, which is in course which has groups As any user I need to post Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "groups" exist: | name | course | idnumber | | Group A | C1 | G1 | | Group B | C1 | G2 | And the following "group members" exist: | user | group | | teacher1 | G1 | | teacher1 | G2 | | student1 | G1 | And the following "activities" exist: | activity | name | course | idnumber | groupmode | | forum | Standard forum name | C1 | nogroups | 0 | Scenario: Teacher can post Given I am on the "Standard forum name" "forum activity" page logged in as teacher1 And I should not see "Group A" And I should not see "Group B" When I click on "Add discussion topic" "link" Then I should not see "Post a copy to all groups" And I should not see "Group" in the "form" "css_element" And I set the following fields to these values: | Subject | Teacher 1 -> Forum | | Message | Teacher 1 -> Forum | And I press "Post to forum" And I wait to be redirected And I should see "Teacher 1 -> Forum" Scenario: Student can post Given I am on the "Standard forum name" "forum activity" page logged in as student1 And I should not see "Group A" And I should not see "Group B" When I click on "Add discussion topic" "link" Then I should not see "Post a copy to all groups" And I should not see "Group" in the "form" "css_element" And I set the following fields to these values: | Subject | Student 1 -> Forum | | Message | Student 1 -> Forum | And I press "Post to forum" And I wait to be redirected And I should see "Student 1 -> Forum" PK-B\1h&& tests/behat/forum_export.featurenu[@mod @mod_forum @javascript Feature: Export forum In order to parse forum data for linguistic analysis As a teacher I need to export the forum data for select users Background: Add a forum and a discussion Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | name | type | course | idnumber | | forum | Test forum 1 | general | C1 | 123 | Scenario: Teacher can export forum Given I am on the "Test forum 1" "forum activity" page logged in as teacher1 And I navigate to "Export" in current page administration And I open the autocomplete suggestions list And I should see "Student 1" in the ".form-autocomplete-suggestions" "css_element" And I should see "Teacher 1" in the ".form-autocomplete-suggestions" "css_element" And I should not see "Student 2" in the ".form-autocomplete-suggestions" "css_element" # This will fail if an exception is thrown. This is the best we can do without the ability to use the download. Hence, there is no "Then" step. And I click on "Export" "button" Scenario: Students cannot export forum by default Given I am on the "Test forum 1" "forum activity" page logged in as student1 Then "Export" "link" should not exist in current page administration Scenario: User with the capability can export Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | mod/forum:exportforum | Allow | student | Course | C1 | When I am on the "Test forum 1" "forum activity" page logged in as student1 And I navigate to "Export" in current page administration And I open the autocomplete suggestions list And I should see "Student 1" in the ".form-autocomplete-suggestions" "css_element" And I should see "Teacher 1" in the ".form-autocomplete-suggestions" "css_element" And I should not see "Student 2" in the ".form-autocomplete-suggestions" "css_element" # This will fail if an exception is thrown. This is the best we can do without the ability to use the download. Hence, there is no "Then" step. And I click on "Export" "button" And I log out Scenario: Group mode is respected when exporting discussions Given the following "groups" exist: | name | course | idnumber | | G1 | C1 | G1 | | G2 | C1 | G2 | And the following "users" exist: | username | firstname | lastname | email | | teachera | Teacher | A | teacherA@example.com | | teacherb | Teacher | B | teacherB@example.com | | teacherc | Teacher | C | teacherC@example.com | | teacherd | Teacher | D | teacherD@example.com | And the following "course enrolments" exist: | user | course | role | | teachera | C1 | teacher | | teacherb | C1 | teacher | | teacherc | C1 | teacher | | teacherd | C1 | teacher | And the following "group members" exist: | user | group | | teachera | G1 | | teachera | G2 | | teacherb | G1 | | teacherc | G2 | And the following "activities" exist: | activity | course | idnumber | name | intro | type | section | groupmode | | forum | C1 | 00001 | Separate groups forum | Standard forum description | general | 1 | 1 | And the following "mod_forum > discussions" exist: | user | forum | name | message | group | | teachera | Separate groups forum | Discussion 1 Group 1 | Test post message | G1 | | teacherb | Separate groups forum | Discussion 2 Group 1 | Test post message | G1 | | teachera | Separate groups forum | Discussion 1 Group 2 | Test post message | G2 | | teacherc | Separate groups forum | Discussion 2 Group 2 | Test post message | G2 | And I am on the "Separate groups forum" "forum activity" page logged in as teacher1 And I navigate to "Export" in current page administration When I expand the "Users" autocomplete # Editing teacher can see all users and discussions. Then I should see "Teacher A" in the "Users" "autocomplete" And I should see "Teacher B" in the "Users" "autocomplete" And I should see "Teacher C" in the "Users" "autocomplete" And I should see "Teacher D" in the "Users" "autocomplete" And I should see "Teacher 1" in the "Users" "autocomplete" And I should see "Student 1" in the "Users" "autocomplete" And I press the escape key And I expand the "Discussions" autocomplete And I should see "Discussion 1 Group 1" in the "Discussions" "autocomplete" And I should see "Discussion 2 Group 1" in the "Discussions" "autocomplete" And I should see "Discussion 1 Group 2" in the "Discussions" "autocomplete" And I should see "Discussion 2 Group 2" in the "Discussions" "autocomplete" And I click on "Export" "button" And I am on the "Separate groups forum" "forum activity" page logged in as teachera And I navigate to "Export" in current page administration When I expand the "Users" autocomplete # Teacher A is is in both groups. Then I should see "Teacher A" in the "Users" "autocomplete" And I should see "Teacher B" in the "Users" "autocomplete" And I should see "Teacher C" in the "Users" "autocomplete" And I should not see "Teacher D" in the "Users" "autocomplete" And I should not see "Teacher 1" in the "Users" "autocomplete" And I should not see "Student 1" in the "Users" "autocomplete" And I press the escape key And I expand the "Discussions" autocomplete And I should see "Discussion 1 Group 1" in the "Discussions" "autocomplete" And I should see "Discussion 2 Group 1" in the "Discussions" "autocomplete" And I should see "Discussion 1 Group 2" in the "Discussions" "autocomplete" And I should see "Discussion 2 Group 2" in the "Discussions" "autocomplete" And I click on "Export" "button" And I am on the "Separate groups forum" "forum activity" page logged in as teacherb And I navigate to "Export" in current page administration When I expand the "Users" autocomplete # Teacher B is in group 1. Then I should see "Teacher A" in the "Users" "autocomplete" And I should see "Teacher B" in the "Users" "autocomplete" And I should not see "Teacher C" in the "Users" "autocomplete" And I should not see "Teacher D" in the "Users" "autocomplete" And I should not see "Teacher 1" in the "Users" "autocomplete" And I should not see "Student 1" in the "Users" "autocomplete" And I press the escape key And I expand the "Discussions" autocomplete And I should see "Discussion 1 Group 1" in the "Discussions" "autocomplete" And I should see "Discussion 2 Group 1" in the "Discussions" "autocomplete" And I should not see "Discussion 1 Group 2" in the "Discussions" "autocomplete" And I should not see "Discussion 2 Group 2" in the "Discussions" "autocomplete" And I click on "Export" "button" And I am on the "Separate groups forum" "forum activity" page logged in as teacherc And I navigate to "Export" in current page administration When I expand the "Users" autocomplete # Teacher C is in group 2. Then I should see "Teacher A" in the "Users" "autocomplete" And I should not see "Teacher B" in the "Users" "autocomplete" And I should see "Teacher C" in the "Users" "autocomplete" And I should not see "Teacher D" in the "Users" "autocomplete" And I should not see "Teacher 1" in the "Users" "autocomplete" And I should not see "Student 1" in the "Users" "autocomplete" And I press the escape key And I expand the "Discussions" autocomplete And I should not see "Discussion 1 Group 1" in the "Discussions" "autocomplete" And I should not see "Discussion 2 Group 1" in the "Discussions" "autocomplete" And I should see "Discussion 1 Group 2" in the "Discussions" "autocomplete" And I should see "Discussion 2 Group 2" in the "Discussions" "autocomplete" And I click on "Export" "button" And I am on the "Separate groups forum" "forum activity" page logged in as teacherd And I navigate to "Export" in current page administration When I expand the "Users" autocomplete # Teacher D is in no group. Then I should not see "Teacher A" in the "Users" "autocomplete" And I should not see "Teacher B" in the "Users" "autocomplete" And I should not see "Teacher C" in the "Users" "autocomplete" And I should not see "Teacher D" in the "Users" "autocomplete" And I should not see "Teacher 1" in the "Users" "autocomplete" And I should not see "Student 1" in the "Users" "autocomplete" And I press the escape key And I expand the "Discussions" autocomplete And I should not see "Discussion 1 Group 1" in the "Discussions" "autocomplete" And I should not see "Discussion 2 Group 1" in the "Discussions" "autocomplete" And I should not see "Discussion 1 Group 2" in the "Discussions" "autocomplete" And I should not see "Discussion 2 Group 2" in the "Discussions" "autocomplete" And I click on "Export" "button" PK-B\Ϥh-tests/behat/forum_activity_completion.featurenu[@mod @mod_forum @core_completion Feature: View activity completion in the forum activity In order to have visibility of forum completion requirements As a student I need to be able to view my forum completion progress Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Vinnie | Student1 | student1@example.com | | teacher1 | Darrell | Teacher1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | enablecompletion | showcompletionconditions | | Course 1 | C1 | 1 | 1 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | teacher1 | C1 | editingteacher | And the following "activity" exists: | activity | forum | | course | C1 | | idnumber | mh1 | | name | Music history | And I am on the "Music history" "forum activity editing" page logged in as teacher1 And I expand all fieldsets And I set the following fields to these values: | Whole forum grading > Type | Point | | Add requirements | 1 | | View the activity | 1 | | Receive a grade | 1 | | completiongradeitemnumber | Whole forum | | Any grade | 1 | | completionpostsenabled | 1 | | completionposts | 2 | | completiondiscussionsenabled | 1 | | completiondiscussions | 1 | | completionrepliesenabled | 1 | | completionreplies | 1 | And I press "Save and display" @javascript Scenario: Forum module displays automatic completion conditions to teachers When I am on the "Music history" "forum activity" page logged in as teacher1 Then "Music history" should have the "View" completion condition And "Music history" should have the "Start discussions: 1" completion condition And "Music history" should have the "Make forum posts: 2" completion condition And "Music history" should have the "Post replies: 1" completion condition And "Music history" should have the "Receive a grade" completion condition @javascript Scenario: A student can complete a forum activity by meeting the completion conditions Given I am on the "Music history" "forum activity" page logged in as student1 And the "View" completion condition of "Music history" is displayed as "done" And the "Start discussions: 1" completion condition of "Music history" is displayed as "todo" And the "Make forum posts: 2" completion condition of "Music history" is displayed as "todo" And the "Post replies: 1" completion condition of "Music history" is displayed as "todo" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And I add a new discussion to "Music history" forum with: | Subject | Fun instruments | | Message | I like drums | And I am on the "Music history" "forum activity" page And the "View" completion condition of "Music history" is displayed as "done" And the "Start discussions: 1" completion condition of "Music history" is displayed as "done" And the "Make forum posts: 2" completion condition of "Music history" is displayed as "todo" And the "Post replies: 1" completion condition of "Music history" is displayed as "todo" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And I reply "Fun instruments" post from "Music history" forum with: | Subject | Reply 1 to Fun instruments | | Message | Guitar is also Fun | And I am on the "Music history" "forum activity" page And the "View" completion condition of "Music history" is displayed as "done" And the "Start discussions: 1" completion condition of "Music history" is displayed as "done" And the "Make forum posts: 2" completion condition of "Music history" is displayed as "done" And the "Post replies: 1" completion condition of "Music history" is displayed as "done" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" # Grade the student And I am on the "Music history" "forum activity" page logged in as teacher1 And I press "Grade users" And I set the field "grade" to "33" And I press "Save" And I press "Close grader" # All conditions should now be completed. When I am on the "Music history" "forum activity" page logged in as student1 Then the "View" completion condition of "Music history" is displayed as "done" And the "Start discussions: 1" completion condition of "Music history" is displayed as "done" And the "Make forum posts: 2" completion condition of "Music history" is displayed as "done" And the "Post replies: 1" completion condition of "Music history" is displayed as "done" And the "Receive a grade" completion condition of "Music history" is displayed as "done" @javascript Scenario: A student can manually mark the forum activity as done but a teacher cannot Given I am on the "Music history" "forum activity editing" page logged in as teacher1 And I expand all fieldsets And I set the field "Students must manually mark the activity as done" to "1" And I press "Save and display" # Teacher view. And the manual completion button for "Music history" should be disabled # Student view. When I am on the "Music history" "forum activity" page logged in as student1 Then the manual completion button of "Music history" is displayed as "Mark as done" And I toggle the manual completion state of "Music history" And the manual completion button of "Music history" is displayed as "Done" PK-B\ *tests/behat/posts_ordering_general.featurenu[@mod @mod_forum Feature: New discussions and discussions with recently added replies are displayed first In order to use forum as a discussion tool As a user I need to see currently active discussions first Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activity" exists: | course | C1 | | activity | forum | | name | Course general forum | | idnumber | forum1 | # # Add three posts into the blog. # And the following "mod_forum > discussions" exist: | user | forum | name | message | timemodified | | student1 | forum1 | Forum post 1 | This is the first post | ##now +1 second## | | student1 | forum1 | Forum post 2 | This is the second post | ##now +2 second## | | student1 | forum1 | Forum post 3 | This is the third post | ##now +3 second## | # # We need javascript/wait to prevent creation of the posts in the same second. The threads # would then ignore each other in the prev/next navigation as the Forum is unable to compute # the correct order. # @javascript Scenario: Replying to a forum post or editing it puts the discussion to the front Given I am on the "Course general forum" "forum activity" page logged in as student1 # # Edit one of the forum posts. # And I follow "Forum post 2" And I click on "Edit" "link" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' forumpost ')][contains(., 'Forum post 2')]" "xpath_element" And I set the following fields to these values: | Subject | Edited forum post 2 | And I press "Save changes" And I wait to be redirected # # Reply to another forum post. # And I am on the "Course general forum" "forum activity" page logged in as teacher1 And I follow "Forum post 1" When I reply "Forum post 1" post from "Course general forum" forum with: | Message | Reply to the first post | And I am on the "Course general forum" "forum activity" page # # Make sure the order of the forum posts is as expected, with most recent new participation first (ie excluding edits). # Then I should see "Forum post 1" in the "//tr[contains(concat(' ', normalize-space(@class), ' '), ' discussion ')][position()=1]" "xpath_element" And I should see "Forum post 3" in the "//tr[contains(concat(' ', normalize-space(@class), ' '), ' discussion ')][position()=2]" "xpath_element" And I should see "Edited forum post 2" in the "//tr[contains(concat(' ', normalize-space(@class), ' '), ' discussion ')][position()=3]" "xpath_element" # # Make sure the next/prev navigation uses the same order of the posts. # And I follow "Forum post 3" And "//a[@aria-label='Next discussion: Forum post 1']" "xpath_element" should exist And "//a[@aria-label='Previous discussion: Edited forum post 2']" "xpath_element" should exist PK-B\v4tests/behat/forum_subscriptions_availability.featurenu[@mod @mod_forum Feature: As a teacher I need to see an accurate list of subscribed users In order to see who is subscribed to a forum As a teacher I need to view the list of subscribed users Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher | Teacher | Teacher | teacher@example.com | | student1 | Student | 1 | student.1@example.com | | student2 | Student | 2 | student.2@example.com | | student3 | Student | 3 | student.3@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | | student3 | C1 | student | And the following "groups" exist: | name | course | idnumber | | Group 1 | C1 | G1 | | Group 2 | C1 | G2 | And the following "group members" exist: | user | group | | student1 | G1 | | student2 | G2 | And the following "groupings" exist: | name | course | idnumber | | Grouping 1 | C1 | GG1 | And the following "grouping groups" exist: | grouping | group | | GG1 | G1 | And the following "activities" exist: | activity | course | idnumber | type | name | forcesubscribe | | forum | C1 | 1 | general | Forced Forum 1 | 1 | | forum | C1 | 0001 | general | Forced Forum 2 | | | forum | C1 | 0002 | general | Forced Forum 3 | 2 | And I log in as "teacher" And I am on "Course 1" course homepage with editing mode on @javascript Scenario: A forced forum lists all subscribers When I am on the "Forced Forum 1" "forum activity" page And I navigate to "Subscriptions" in current page administration Then I should see "Student 1" And I should see "Teacher Teacher" And I should see "Student 2" And I should see "Student 3" And I am on the "Forced Forum 1" "forum activity editing" page And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Grouping" "button" in the "Add restriction..." "dialogue" And I set the field with xpath "//select[@name='id']" to "Grouping 1" And I press "Save and display" And I navigate to "Subscriptions" in current page administration And I should see "Student 1" And I should see "Teacher Teacher" And I should not see "Student 2" And I should not see "Student 3" Scenario: A forced forum does not allow to edit the subscribers Given I am on the "Forced Forum 2" "forum activity editing" page And I set the following fields to these values: | Subscription mode | Forced subscription | | Availability | Show on course page | And I press "Save and return to course" And I am on the "Forced Forum 2" "forum activity" page And I navigate to "Subscriptions" in current page administration Then I should see "Teacher Teacher" And I should see "Student 1" And I should see "Student 2" And I should see "Student 3" Scenario: A forced and hidden forum lists only teachers Given I am on the "Forced Forum 2" "forum activity editing" page And I set the following fields to these values: | Subscription mode | Forced subscription | | Availability | Hide on course page | And I press "Save and return to course" And I am on the "Forced Forum 2" "forum activity" page And I navigate to "Subscriptions" in current page administration Then I should see "Teacher Teacher" And I should not see "Student 1" And I should not see "Student 2" And I should not see "Student 3" @javascript Scenario: An automatic forum lists all subscribers When I am on the "Forced Forum 3" "forum activity" page And I navigate to "Subscriptions" in current page administration Then I should see "Student 1" And I should see "Teacher Teacher" And I should see "Student 2" And I should see "Student 3" And I am on the "Forced Forum 3" "forum activity editing" page And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Grouping" "button" in the "Add restriction..." "dialogue" And I set the field with xpath "//select[@name='id']" to "Grouping 1" And I press "Save and display" And I navigate to "Subscriptions" in current page administration And I should see "Student 1" And I should see "Teacher Teacher" And I should not see "Student 2" And I should not see "Student 3" PK-B\MVt& & tests/behat/edit_tags.featurenu[@mod @mod_forum @core_tag Feature: Edited forum posts handle tags correctly In order to get forum posts properly labelled As a user I need to introduce the tags while editing Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activity" exists: | course | C1 | | activity | forum | | name | Test forum name | | idnumber | forum1 | And the following "mod_forum > discussions" exist: | user | forum | name | message | | teacher1 | forum1 | Teacher post subject | Teacher post message | @javascript Scenario: Forum post edition of custom tags works as expected Given I am on the "Course 1" Course page logged in as student1 And I reply "Teacher post subject" post from "Test forum name" forum with: | Subject | Student post subject | | Message | Student post message | | Tags | Tag1 | Then I should see "Tag1" in the ".tag_list" "css_element" And I click on "Edit" "link" in the "//div[@aria-label='Student post subject by Student 1']" "xpath_element" Then I should see "Tag1" in the ".form-autocomplete-selection" "css_element" @javascript Scenario: Forum post edition of standard tags works as expected Given the following "tags" exist: | name | isstandard | | OT1 | 1 | | OT2 | 1 | | OT3 | 1 | And I am on the "Test forum name" "forum activity" page logged in as teacher1 And I click on "Add discussion topic" "link" And I click on "Advanced" "button" And I expand all fieldsets And I open the autocomplete suggestions list And I should see "OT1" in the ".form-autocomplete-suggestions" "css_element" And I should see "OT2" in the ".form-autocomplete-suggestions" "css_element" And I should see "OT3" in the ".form-autocomplete-suggestions" "css_element" And I reply "Teacher post subject" post from "Test forum name" forum with: | Subject | Student post subject | | Message | Student post message | | Tags | OT1, OT3 | Then I should see "OT1" in the ".tag_list" "css_element" And I should see "OT3" in the ".tag_list" "css_element" And I should not see "OT2" in the ".tag_list" "css_element" And I click on "Edit" "link" in the "//div[@aria-label='Student post subject by Teacher 1']" "xpath_element" And I should see "OT1" in the ".form-autocomplete-selection" "css_element" And I should see "OT3" in the ".form-autocomplete-selection" "css_element" And I should not see "OT2" in the ".form-autocomplete-selection" "css_element" PK-B\p\GG,tests/behat/discussion_subscriptions.featurenu[@mod @mod_forum Feature: A user can control their own subscription preferences for a discussion In order to receive notifications for things I am interested in As a user I need to choose my discussion subscriptions Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | One | student.one@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | @javascript Scenario: An optional forum can have discussions subscribed to Given the following "activity" exists: | activity | forum | | course | C1 | | idnumber | forum1 | | name | Test forum name | | type | general | | forcesubscribe | 0 | And the following "mod_forum > discussions" exist: | user | forum | name | message | | admin | forum1 | Test post subject one | Test post message one | | admin | forum1 | Test post subject two | Test post message two | When I am on the "Test forum name" "forum activity" page logged in as student1 Then I can subscribe to this forum And "Subscribe to this discussion" "checkbox" should exist in the "Test post subject one" "table_row" And "Subscribe to this discussion" "checkbox" should exist in the "Test post subject two" "table_row" And I click on "input[id^=subscription-toggle]" "css_element" in the "Test post subject one" "table_row" And I can subscribe to this forum And "Unsubscribe from this discussion" "checkbox" should exist in the "Test post subject one" "table_row" And "Subscribe to this discussion" "checkbox" should exist in the "Test post subject two" "table_row" And I click on "input[id^=subscription-toggle]" "css_element" in the "Test post subject one" "table_row" And I can subscribe to this forum And "Subscribe to this discussion" "checkbox" should exist in the "Test post subject one" "table_row" And "Subscribe to this discussion" "checkbox" should exist in the "Test post subject two" "table_row" And I click on "input[id^=subscription-toggle]" "css_element" in the "Test post subject one" "table_row" And I can subscribe to this forum And "Unsubscribe from this discussion" "checkbox" should exist in the "Test post subject one" "table_row" And "Subscribe to this discussion" "checkbox" should exist in the "Test post subject two" "table_row" And I follow "Subscribe to forum" And I should see "You will be notified of new posts in the forum 'Test forum name'" And I can unsubscribe from this forum And "Unsubscribe from this discussion" "checkbox" should exist in the "Test post subject one" "table_row" And "Unsubscribe from this discussion" "checkbox" should exist in the "Test post subject two" "table_row" And I unsubscribe from this forum And I should see "Student One will NOT be notified of new posts in 'Test forum name'" And I can subscribe to this forum And "Subscribe to this discussion" "checkbox" should exist in the "Test post subject one" "table_row" And "Subscribe to this discussion" "checkbox" should exist in the "Test post subject two" "table_row" @javascript Scenario: An automatic subscription forum can have discussions unsubscribed from Given the following "activity" exists: | activity | forum | | course | C1 | | idnumber | forum1 | | name | Test forum name | | type | general | | forcesubscribe | 2 | And the following "mod_forum > discussions" exist: | user | forum | name | message | | admin | forum1 | Test post subject one | Test post message one | | admin | forum1 | Test post subject two | Test post message two | When I am on the "Test forum name" "forum activity" page logged in as student1 Then I can unsubscribe from this forum And "Unsubscribe from this discussion" "checkbox" should exist in the "Test post subject one" "table_row" And "Unsubscribe from this discussion" "checkbox" should exist in the "Test post subject two" "table_row" And I click on "input[id^=subscription-toggle]" "css_element" in the "Test post subject one" "table_row" And I can unsubscribe from this forum And "Subscribe to this discussion" "checkbox" should exist in the "Test post subject one" "table_row" And "Unsubscribe from this discussion" "checkbox" should exist in the "Test post subject two" "table_row" And I click on "input[id^=subscription-toggle]" "css_element" in the "Test post subject one" "table_row" And I can unsubscribe from this forum And "Unsubscribe from this discussion" "checkbox" should exist in the "Test post subject one" "table_row" And "Unsubscribe from this discussion" "checkbox" should exist in the "Test post subject two" "table_row" And I click on "input[id^=subscription-toggle]" "css_element" in the "Test post subject one" "table_row" And I can unsubscribe from this forum And "Subscribe to this discussion" "checkbox" should exist in the "Test post subject one" "table_row" And "Unsubscribe from this discussion" "checkbox" should exist in the "Test post subject two" "table_row" And I unsubscribe from this forum And I should see "Student One will NOT be notified of new posts in 'Test forum name'" And I can subscribe to this forum And "Subscribe to this discussion" "checkbox" should exist in the "Test post subject one" "table_row" And "Subscribe to this discussion" "checkbox" should exist in the "Test post subject two" "table_row" And I subscribe to this forum And I should see "You will be notified of new posts in the forum 'Test forum name'" And I can unsubscribe from this forum And "Unsubscribe from this discussion" "checkbox" should exist in the "Test post subject one" "table_row" And "Unsubscribe from this discussion" "checkbox" should exist in the "Test post subject two" "table_row" @javascript Scenario: A user does not lose their preferences when a forum is switch from optional to automatic Given the following "activity" exists: | activity | forum | | course | C1 | | idnumber | forum1 | | name | Test forum name | | type | general | | forcesubscribe | 0 | And the following "mod_forum > discussions" exist: | user | forum | name | message | | admin | forum1 | Test post subject one | Test post message one | | admin | forum1 | Test post subject two | Test post message two | And I am on the "Test forum name" "forum activity" page logged in as student1 And I can subscribe to this forum And "Subscribe to this discussion" "checkbox" should exist in the "Test post subject one" "table_row" And "Subscribe to this discussion" "checkbox" should exist in the "Test post subject two" "table_row" And I click on "input[id^=subscription-toggle]" "css_element" in the "Test post subject one" "table_row" And I can subscribe to this forum And "Unsubscribe from this discussion" "checkbox" should exist in the "Test post subject one" "table_row" And "Subscribe to this discussion" "checkbox" should exist in the "Test post subject two" "table_row" And I am on the "Test forum name" "forum activity editing" page logged in as admin And I set the following fields to these values: | Subscription mode | Auto subscription | And I press "Save and return to course" And I am on the "Test forum name" "forum activity" page logged in as student1 And I can unsubscribe from this forum And "Unsubscribe from this discussion" "checkbox" should exist in the "Test post subject one" "table_row" And "Unsubscribe from this discussion" "checkbox" should exist in the "Test post subject two" "table_row" When I unsubscribe from this forum Then I should see "Student One will NOT be notified of new posts in 'Test forum name'" And I can subscribe to this forum And "Unsubscribe from this discussion" "checkbox" should exist in the "Test post subject one" "table_row" And "Subscribe to this discussion" "checkbox" should exist in the "Test post subject two" "table_row" Scenario: An optional forum prompts a user to subscribe to a discussion when posting unless they have already chosen not to subscribe Given the following "activity" exists: | activity | forum | | course | C1 | | idnumber | forum1 | | name | Test forum name | | type | general | | forcesubscribe | 0 | And the following "mod_forum > discussions" exist: | user | forum | name | message | | admin | forum1 | Test post subject one | Test post message one | | admin | forum1 | Test post subject two | Test post message two | When I am on the "Test forum name" "forum activity" page logged in as student1 And I should see "Subscribe to forum" And I reply "Test post subject one" post from "Test forum name" forum with: | Subject | Reply 1 to discussion 1 | | Message | Discussion contents 1, second message | | Discussion subscription | 1 | And I reply "Test post subject two" post from "Test forum name" forum with: | Subject | Reply 1 to discussion 1 | | Message | Discussion contents 1, second message | | Discussion subscription | 0 | And I am on the "Test forum name" "forum activity" page Then "Unsubscribe from this discussion" "checkbox" should exist in the "Test post subject one" "table_row" And "Subscribe to this discussion" "checkbox" should exist in the "Test post subject two" "table_row" And I follow "Test post subject one" And I follow "Reply" And the field "Discussion subscription" matches value "Send me notifications of new posts in this discussion" And I follow "Test forum name" And I follow "Test post subject two" And I follow "Reply" And the field "Discussion subscription" matches value "I don't want to be notified of new posts in this discussion" Scenario: An automatic forum prompts a user to subscribe to a discussion when posting unless they have already chosen not to subscribe Given the following "activity" exists: | activity | forum | | course | C1 | | idnumber | forum1 | | name | Test forum name | | type | general | | forcesubscribe | 2 | And the following "mod_forum > discussions" exist: | user | forum | name | message | | admin | forum1 | Test post subject one | Test post message one | | admin | forum1 | Test post subject two | Test post message two | # added for this scenario And the following "users" exist: | username | firstname | lastname | email | | student2 | Student | Two | student.two@example.com | And the following "course enrolments" exist: | user | course | role | | student2 | C1 | student | When I am on the "Test forum name" "forum activity" page logged in as student2 And I should see "Unsubscribe from forum" And I reply "Test post subject one" post from "Test forum name" forum with: | Subject | Reply 1 to discussion 1 | | Message | Discussion contents 1, second message | | Discussion subscription | 1 | And I reply "Test post subject two" post from "Test forum name" forum with: | Subject | Reply 1 to discussion 1 | | Message | Discussion contents 1, second message | | Discussion subscription | 0 | And I am on the "Test forum name" "forum activity" page Then "Unsubscribe from this discussion" "checkbox" should exist in the "Test post subject one" "table_row" And "Subscribe to this discussion" "checkbox" should exist in the "Test post subject two" "table_row" And I follow "Test post subject one" And I follow "Reply" And the field "Discussion subscription" matches value "Send me notifications of new posts in this discussion" And I am on the "Test forum name" "forum activity" page And I follow "Test post subject two" And I follow "Reply" And the field "Discussion subscription" matches value "I don't want to be notified of new posts in this discussion" Scenario: A guest should not be able to subscribe to a discussion Given the following "activities" exist: | activity | name | course | section | idnumber | type | | forum | Test forum name | Acceptance test site | 1 | forum1 | general | And the following "mod_forum > discussions" exist: | user | forum | name | message | | admin | forum1 | Test post subject one | Test post message one | When I am on the "Test forum name" "forum activity" page logged in as guest Then "Subscribe to this discussion" "checkbox" should not exist in the "Test post subject one" "table_row" And "Unsubscribe from this discussion" "checkbox" should not exist in the "Test post subject one" "table_row" And I follow "Test post subject one" And "Subscribe to this discussion" "checkbox" should not exist And "Unsubscribe from this discussion" "checkbox" should not exist Scenario: A user who is not logged in should not be able to subscribe to a discussion Given the following "activities" exist: | activity | name | course | section | idnumber | type | | forum | Test forum name | Acceptance test site | 1 | forum1 | general | And the following "mod_forum > discussions" exist: | user | forum | name | message | | admin | forum1 | Test post subject one | Test post message one | When I am on site homepage And I follow "Test forum name" Then "Subscribe to this discussion" "checkbox" should not exist in the "Test post subject one" "table_row" And "Unsubscribe from this discussion" "checkbox" should not exist in the "Test post subject one" "table_row" And I follow "Test post subject one" And "Subscribe to this discussion" "checkbox" should not exist And "Unsubscribe from this discussion" "checkbox" should not exist Scenario: A user can toggle their subscription preferences when viewing a discussion Given the following "activity" exists: | activity | forum | | course | C1 | | idnumber | forum1 | | name | Test forum name | | type | general | | forcesubscribe | 0 | And the following "mod_forum > discussions" exist: | user | forum | name | message | | admin | forum1 | Test post subject one | Test post message one | When I am on the "Test forum name" "forum activity" page logged in as student1 Then "Subscribe to forum" "link" should exist And I follow "Test post subject one" And "You are not subscribed to this discussion. Click to subscribe" "link" should exist And I follow "Test forum name" And I follow "Subscribe to forum" And I should see "You will be notified of new posts in the forum 'Test forum name'" And "Unsubscribe from forum" "link" should exist And I follow "Test post subject one" And "You are subscribed to this discussion. Click to unsubscribe" "link" should exist And I follow "You are subscribed to this discussion. Click to unsubscribe" And I should see "Student One will NOT be notified of new posts in 'Test post subject one' of 'Test forum name'" And I follow "Test post subject one" And "You are not subscribed to this discussion. Click to subscribe" "link" should exist And I follow "Test forum name" And I follow "Unsubscribe from forum" And I should see "Student One will NOT be notified of new posts in 'Test forum name'" And "Subscribe to forum" "link" should exist And I follow "Test post subject one" And "You are not subscribed to this discussion. Click to subscribe" "link" should exist And I follow "You are not subscribed to this discussion. Click to subscribe" And I should see "You will be notified of new posts in 'Test post subject one' in the forum 'Test forum name'." And "Unsubscribe from this discussion" "checkbox" should exist in the "Test post subject one" "table_row" And I follow "Subscribe to forum" And I should see "You will be notified of new posts in the forum 'Test forum name'" And "Unsubscribe from forum" "link" should exist And I follow "Test post subject one" And "You are subscribed to this discussion. Click to unsubscribe" "link" should exist And I follow "Test forum name" And I follow "Unsubscribe from forum" And I should see "Student One will NOT be notified of new posts in 'Test forum name'" And "Subscribe to forum" "link" should exist And I follow "Test post subject one" And "You are not subscribed to this discussion. Click to subscribe" "link" should exist PK-B\F""tests/behat/grade_forum.featurenu[@mod @mod_forum @core_grades @javascript Feature: I can grade a students interaction across a forum In order to assess a student's contributions As a teacher I can assign grades to a student based on their contributions Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | format | numsections | | Course 1 | C1 | weeks | 5 | And the following "grade categories" exist: | fullname | course | | Tutor | C1 | | Peers | C1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "scales" exist: | name | scale | | Test Scale 1 | Disappointing, Good, Very good, Excellent | And the following "activity" exists: | activity | forum | | course | C1 | | idnumber | 0001 | | name | Test Forum 1 | And I log in as "teacher1" And I change window size to "large" And I am on "Course 1" course homepage with editing mode on Scenario: Ensure that forum grade settings do not leak to Ratings Given I am on the "Test Forum 1" "forum activity editing" page And I expand all fieldsets # Fields should be hidden when grading is not set. When I set the field "Whole forum grading > Type" to "None" Then "Whole forum grading > Grade to pass" "field" should not be visible And "Whole forum grading > Grade category" "field" should not be visible And "Whole forum grading > Maximum grade" "field" should not be visible And "Ratings > Grade to pass" "field" should not be visible And "Ratings > Grade category" "field" should not be visible And "Ratings > Maximum grade" "field" should not be visible # Only Whole forum grading fields should be visible. When I set the field "Whole forum grading > Type" to "Point" Then "Whole forum grading > Grade to pass" "field" should be visible And "Whole forum grading > Grade category" "field" should be visible And "Whole forum grading > Maximum grade" "field" should be visible But "Ratings > Grade to pass" "field" should not be visible And "Ratings > Grade category" "field" should not be visible And "Ratings > Maximum grade" "field" should not be visible # Save some values. Given I set the field "Whole forum grading > Maximum grade" to "10" And I set the field "Whole forum grading > Grade category" to "Tutor" And I set the field "Whole forum grading > Grade to pass" to "4" When I press "Save and return to course" And I navigate to "View > Grader report" in the course gradebook # There shouldn't be any Ratings grade item. Then I should see "Test Forum 1 whole forum" But I should not see "Test Forum 1 rating" # The values saved should be reflected here. And I click on grade item menu "Test Forum 1 whole forum" of type "gradeitem" on "grader" page And I choose "Edit grade item" in the open action menu When I click on "Show more..." "link" in the ".modal-dialog" "css_element" Then the field "Maximum grade" matches value "10" Then the field "Grade to pass" matches value "4" And I should see "Tutor" in the "Parent category" "fieldset" Scenario: Ensure that Ratings settings do not leak to Forum grading Given I am on the "Test Forum 1" "forum activity editing" page And I expand all fieldsets # Fields should be hidden when grading is not set. When I set the field "Ratings > Aggregate type" to "No ratings" Then "Ratings > Type" "field" should not be visible And "Ratings > Grade to pass" "field" should not be visible And "Ratings > Grade category" "field" should not be visible And "Ratings > Maximum grade" "field" should not be visible And "Whole forum grading > Grade to pass" "field" should not be visible And "Whole forum grading > Grade category" "field" should not be visible And "Whole forum grading > Maximum grade" "field" should not be visible # Set to "Count of ratings" When I set the field "Ratings > Aggregate type" to "Count of ratings" Then "Ratings > Type" "field" should be visible When I set the field "Ratings > Type" to "None" Then "Ratings > Grade to pass" "field" should not be visible And "Ratings > Grade category" "field" should not be visible And "Ratings > Maximum grade" "field" should not be visible And "Whole forum grading > Grade to pass" "field" should not be visible And "Whole forum grading > Grade category" "field" should not be visible And "Whole forum grading > Maximum grade" "field" should not be visible # Use point grading When I set the field "Ratings > Type" to "Point" Then "Ratings > Grade to pass" "field" should be visible And "Ratings > Grade category" "field" should be visible And "Ratings > Maximum grade" "field" should be visible And "Whole forum grading > Grade to pass" "field" should not be visible And "Whole forum grading > Grade category" "field" should not be visible And "Whole forum grading > Maximum grade" "field" should not be visible # Save some values. Given I set the field "Ratings > Maximum grade" to "10" And I set the field "Ratings > Grade category" to "Tutor" And I set the field "Ratings > Grade to pass" to "4" When I press "Save and return to course" And I navigate to "View > Grader report" in the course gradebook # There shouldn't be any Whole forum grade gradeitem. Then I should see "Test Forum 1 rating" But I should not see "Test Forum 1 whole forum" # The values saved should be reflected here. And I click on grade item menu "Test Forum 1 rating" of type "gradeitem" on "grader" page And I choose "Edit grade item" in the open action menu When I click on "Show more..." "link" in the ".modal-dialog" "css_element" Then the field "Maximum grade" matches value "10" Then the field "Grade to pass" matches value "4" And I should see "Tutor" in the "Parent category" "fieldset" Scenario: Setting both a rating and a whole forum grade does not bleed Given I am on the "Test Forum 1" "forum activity editing" page And I expand all fieldsets And I set the field "Ratings > Aggregate type" to "Count of ratings" And I set the field "Ratings > Type" to "Point" And I set the field "Ratings > Maximum grade" to "100" And I set the field "Ratings > Grade category" to "Peers" And I set the field "Ratings > Grade to pass" to "40" And I set the field "Whole forum grading > Type" to "Point" And I set the field "Whole forum grading > Maximum grade" to "10" And I set the field "Whole forum grading > Grade category" to "Tutor" And I set the field "Whole forum grading > Grade to pass" to "4" And I press "Save and return to course" And I navigate to "View > Grader report" in the course gradebook # There shouldn't be any Whole forum grade gradeitem. Then I should see "Test Forum 1 rating" And I should see "Test Forum 1 whole forum" # The values saved should be reflected here. And I click on grade item menu "Test Forum 1 rating" of type "gradeitem" on "grader" page And I choose "Edit grade item" in the open action menu When I click on "Show more..." "link" in the ".modal-dialog" "css_element" Then the field "Maximum grade" matches value "100" Then the field "Grade to pass" matches value "40" And I should see "Peers" in the "Parent category" "fieldset" And I press "Cancel" And I click on grade item menu "Test Forum 1 whole forum" of type "gradeitem" on "grader" page And I choose "Edit grade item" in the open action menu When I click on "Show more..." "link" in the ".modal-dialog" "css_element" Then the field "Maximum grade" matches value "10" Then the field "Grade to pass" matches value "4" And I should see "Tutor" in the "Parent category" "fieldset" Scenario: Ensure that only gradable users are available in forum grading interface Given I am on the "Test Forum 1" "forum activity editing" page logged in as "admin" And I expand all fieldsets When I set the field "Whole forum grading > Type" to "Point" And I set the field "Whole forum grading > Maximum grade" to "10" And I set the field "Whole forum grading > Grade to pass" to "4" And I press "Save and display" And I press "Grade users" Then I should see "1 out of 1" And I should see "Student 1" And I should not see "Teacher 1" And I press "Save changes and proceed to the next user" And I should see "Student 1" And I should not see "Teacher 1" PK-B\  #tests/behat/advanced_search.featurenu[@mod @mod_forum Feature: The forum search allows users to perform advanced searches for forum posts In order to perform an advanced search for a forum post As a teacher I can use the search feature Background: Given the following "users" exist: | username | firstname | lastname | email | idnumber | | teacher1 | Teacher | ONE | teacher1@example.com | T1 | | teacher2 | Teacher | TWO | teacher2@example.com | T1 | | student1 | Student | 1 | student1@example.com | S1 | And the following "courses" exist: | fullname | shortname | category | newsitems | | Course 1 | C1 | 0 | 1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher2 | C1 | editingteacher | | student1 | C1 | student | And the following "tags" exist: | name | isstandard | | SearchedTag | 1 | And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | news_items | Course | C1 | course-view-* | side-pre | And the following "mod_forum > discussions" exist: | user | forum | name | subject | message | | teacher1 | Announcements | My subject | My subject | My message | | teacher1 | Announcements | Your subjective | Your subjective | Your long message | Scenario: Perform an advanced search using any term Given I am on the "Announcements" "forum activity" page logged in as student1 And I press "Search" And I should see "Please enter search terms into one or more of the following fields" And I set the field "words" to "subject" When I press "Search" Then I should see "My subject" And I should see "Your subjective" Scenario: Perform an advanced search avoiding words Given I am on the "Announcements" "forum activity" page logged in as student1 And I press "Search" And I should see "Please enter search terms into one or more of the following fields" And I set the field "words" to "My" And I set the field "notwords" to "subjective" When I press "Search" Then I should see "My subject" And I should not see "Your subjective" Scenario: Perform an advanced search using whole words Given database family used is one of the following: | mysql | | postgres | And I am on the "Announcements" "forum activity" page logged in as student1 And I press "Search" And I should see "Please enter search terms into one or more of the following fields" And I set the field "fullwords" to "subject" When I press "Search" Then I should see "My subject" And I should not see "Your subjective" Scenario: Perform an advanced search matching the subject Given I am on the "Announcements" "forum activity" page logged in as student1 And I press "Search" And I should see "Please enter search terms into one or more of the following fields" And I set the field "subject" to "subjective" When I press "Search" Then I should not see "My message" And I should see "Your subjective" Scenario: Perform an advanced search matching the author Given the following "mod_forum > discussions" exist: | user | forum | name | subject | message | | teacher2 | Announcements | My Subjects | My Subjects | My message | And I am on the "Announcements" "forum activity" page logged in as student1 When I press "Search" And I should see "Please enter search terms into one or more of the following fields" And I set the field "user" to "TWO" And I press "Search" Then I should see "Teacher TWO" And I should not see "Teacher ONE" Scenario: Perform an advanced search with multiple words Given I am on the "Announcements" "forum activity" page logged in as student1 And I press "Search" And I should see "Please enter search terms into one or more of the following fields" And I set the field "subject" to "your subjective" When I press "Search" Then I should not see "My message" And I should see "Your subjective" @javascript @accessibility Scenario: Perform an advanced search using tags Given I am on the "Announcements" "forum activity" page logged in as teacher1 And I follow "My subject" And I follow "Edit" And I set the following fields to these values: | Tags | SearchedTag | And I press "Save changes" And I am on the "Announcements" "forum activity" page logged in as student1 And I press "Search" And I should see "Please enter search terms into one or more of the following fields" And I set the field "Is tagged with" to "SearchedTag" When I press "Search" Then I should see "My subject" And I should not see "Your subjective" And the "region-main" "region" should meet accessibility standards with "best-practice" extra tests @javascript Scenario: Perform an advanced search on starred discussions without text Given I am on the "Announcements" "forum activity" page logged in as student1 And I click on "Star this discussion" "link" in the "Your subjective" "table_row" And I press "Search" And I should see "Please enter search terms into one or more of the following fields" And I set the field "starredonly" to "1" When I press "Search" Then I should not see "My message" And I should see "Your subjective" @javascript Scenario: Perform an advanced search on starred discussions with text Given I am on the "Announcements" "forum activity" page logged in as student1 And I click on "Star this discussion" "link" in the "Your subjective" "table_row" And I press "Search" And I should see "Please enter search terms into one or more of the following fields" And I set the field "words" to "message" And I set the field "starredonly" to "1" When I press "Search" Then I should not see "My message" And I should see "Your subjective" PK-B\9 #tests/behat/private_replies.featurenu[@mod @mod_forum Feature: Forum posts can be replied to in private In order to post feedback to my students As a Teacher I need to be able to reply privately to students Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | teacher2 | Teacher | 2 | teacher2@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | And the following "courses" exist: | fullname | shortname | category | | Science 101 | C1 | 0 | And the following "activities" exist: | activity | name | course | idnumber | | forum | Study discussions | C1 | forum | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher2 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And the following "mod_forum > discussions" exist: | user | forum | name | message | | student1 | forum | Answers to the homework | Here are the answers to last night's homework. | And the following forum replies exist in course "Science 101": | user | forum | discussion | message | privatereplyto | | teacher1 | Study discussions | Answers to the homework |How about you and I have a meeting after class about plagiarism?| student1 | Scenario: As a teacher I can see my own response Given I am on the "Study discussions" "forum activity" page logged in as teacher1 When I follow "Answers to the homework" Then I should see "How about you and I have a meeting after class about plagiarism?" Scenario: As a fellow teacher I can see the other teacher's response Given I am on the "Study discussions" "forum activity" page logged in as teacher2 When I follow "Answers to the homework" Then I should see "How about you and I have a meeting after class about plagiarism?" Scenario: As the intended recipient I can see my own response Given I am on the "Study discussions" "forum activity" page logged in as student1 When I follow "Answers to the homework" Then I should see "How about you and I have a meeting after class about plagiarism?" Scenario: As a non-privileged user I cannot see my own response Given I am on the "Study discussions" "forum activity" page logged in as student2 When I follow "Answers to the homework" Then I should not see "How about you and I have a meeting after class about plagiarism?" PK-B\mR.tests/behat/h5p_inline_editing_content.featurenu[@mod @mod_forum @filter @filter_displayh5p @core_h5p @_file_upload @_switch_iframe Feature: Inline editing H5P content in mod_forum In order to edit an existing H5P content As a user I need to see the button and access to the H5P editor Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | teacher2 | Teacher | 2 | teacher2@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher2 | C1 | teacher | | student1 | C1 | student | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | C1 | contenttype_h5p | admin | Greeting card | /h5p/tests/fixtures/greeting-card.h5p | And the following "activities" exist: | activity | name | introformat | course | content | contentformat | idnumber | | forum | ForumName1 | 1 | C1 | H5Ptest | 1 | 1 | And the "displayh5p" filter is "on" # Override this capability to let teachers and students to Turn editing on. And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/course:update | Allow | teacher | System | | | moodle/course:update | Allow | student | System | | @javascript @repository_contentbank Scenario: Edit H5P content from a forum intro using copy to content bank file Given I am on the "ForumName1" "forum activity editing" page logged in as admin # Add H5P content to the forum description. And I click on "Insert H5P content" "button" in the "#fitem_id_introeditor" "css_element" And I click on "Browse repositories..." "button" in the "Insert H5P content" "dialogue" And I select "Content bank" repository in file picker And I click on "Greeting card" "file" in repository content area And I click on "Make a copy of the file" "radio" And I click on "Select this file" "button" And I click on "Insert H5P" "button" in the "Insert H5P content" "dialogue" And I click on "Save and display" "button" And I switch to "h5p-iframe" class iframe And I switch to "h5p-iframe" class iframe And I should see "Hello world!" And I switch to the main frame # The Edit button is only displayed when editing mode is on. And I should not see "Edit H5P content" When I am on "Course 1" course homepage with editing mode on And I am on the "ForumName1" "forum activity" page Then I should see "Edit H5P content" And I log out # Check teacher1 can see the Edit button too. And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And I am on the "ForumName1" "forum activity" page And I should see "Edit H5P content" And I log out # Check teacher2 (non-editing teacher) can't see the Edit button, because she can't edit the forum activity. And I log in as "teacher2" And I am on "Course 1" course homepage with editing mode on And I am on the "ForumName1" "forum activity" page And I should not see "Edit H5P content" And I log out # Check student1 can't see the Edit button. And I log in as "student1" And I am on "Course 1" course homepage with editing mode on And I am on the "ForumName1" "forum activity" page And I should not see "Edit H5P content" @javascript @repository_contentbank Scenario: Edit H5P content from a forum post Given the following "mod_forum > discussions" exist: | user | forum | name | message | | admin | 1 | Forum post by admin | Forum post by admin | Given I am on the "ForumName1" "forum activity" page logged in as admin And I follow "Forum post by admin" And I click on "Edit" "link" # Add H5P content to a forum post as admin. And I set the following fields to these values: | Subject | Forum post by admin | And I click on "Insert H5P content" "button" in the "#fitem_id_message" "css_element" And I click on "Browse repositories..." "button" in the "Insert H5P content" "dialogue" And I select "Content bank" repository in file picker And I click on "Greeting card" "file" in repository content area And I click on "Select this file" "button" And I click on "Insert H5P" "button" in the "Insert H5P content" "dialogue" And I press "Save changes" And I switch to "h5p-iframe" class iframe And I switch to "h5p-iframe" class iframe And I should see "Hello world!" And I switch to the main frame # The Edit button is only displayed when editing mode is on. And I should not see "Edit H5P content" When I am on "Course 1" course homepage with editing mode on And I am on the "ForumName1" "forum activity" page And I follow "Forum post by admin" Then I should see "Edit H5P content" And I log out # Check teacher1 can see the Edit button because she can edit the post too. And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And I am on the "ForumName1" "forum activity" page And I follow "Forum post by admin" And I should see "Edit H5P content" And I log out # Check teacher2 (non-editing teacher) can see the Edit button because she can edit the post too. And I log in as "teacher2" And I am on "Course 1" course homepage with editing mode on And I am on the "ForumName1" "forum activity" page And I follow "Forum post by admin" And I should see "Edit H5P content" And I log out # Check student1 can't see the Edit button. And I log in as "student1" And I am on "Course 1" course homepage with editing mode on And I am on the "ForumName1" "forum activity" page And I follow "Forum post by admin" And I should not see "Edit H5P content" PK-B\hP+$tests/behat/add_forum_inline.featurenu[@mod @mod_forum @javascript Feature: Add forum activities and discussions utilizing the inline add discussion form Background: Add a forum and a discussion attaching files Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activity" exists: | activity | forum | | course | C1 | | idnumber | 0001 | | name | Test forum name | | type | general | And the following "mod_forum > discussion" exists: | forum | 0001 | | course | C1 | | user | teacher1 | | name | Forum post 1 | | message | This is the body | Scenario: Student can add a discussion via the inline form Given I am on the "Course 1" course page logged in as student1 Then I add a new discussion to "Test forum name" forum inline with: | Subject | Post with attachment | | Message | This is the body | PK-B\}+ *tests/behat/split_forum_discussion.featurenu[@mod @mod_forum Feature: Forum discussions can be split In order to manage forum discussions in my course As a Teacher I need to be able to split threads to keep them on topic. Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | And the following "courses" exist: | fullname | shortname | category | | Science 101 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And the following "activities" exist: | activity | name | course | idnumber | type | | forum | Study discussions | C1 | forump1 | general | And the following "mod_forum > discussions" exist: | forum | course | user | name | message | | forump1 | C1 | teacher1 | Photosynthesis discussion | Lets discuss our learning about Photosynthesis this week in this thread. | And the following "mod_forum > posts" exist: | user | parentsubject | subject | message | | student1 | Photosynthesis discussion | Mass number | Can anyone tell me which number is the mass number in the periodic table? | | student2 | Mass number | | I would also like to know this | Scenario: Split a forum discussion Given I am on the "Study discussions" "forum activity" page logged in as teacher1 And I follow "Photosynthesis discussion" When I follow "Split" And I set the following fields to these values: | Discussion name | Mass number in periodic table | And I press "Split" Then I should see "Mass number in periodic table" And I follow "Study discussions" And I should see "Teacher 1" in the "Photosynthesis" "table_row" # Confirm that the last post author has been updated. And I should not see "Student 2" in the "Photosynthesis" "table_row" # Confirm that the current author has been shown for the new split discussion. And I should see "Student 1" in the "Mass number in periodic table" "table_row" PK-B\a )tests/behat/grade_view_discussion.featurenu[@mod @mod_forum @javascript Feature: View discussion while grading in a forum In order to grade efficiently As a teacher I want to be able to see the full discussion the student was taking part in. Background: # Student 1 needs to be created first or they will not be the first user on the grading screen. Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student.1@example.com | | student2 | Student | 2 | student.2@example.com | | teacher | Teacher | Tom | teacher@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student2 | C1 | student | | teacher | C1 | editingteacher | And the following "activity" exists: | activity | forum | | name | Gradable forum | | course | C1 | | idnumber | forum1 | | grade_forum | 100 | | scale | 100 | # If there is more than one pots for Student 1 the test will not be able to select the # correct View discussion link, as there is no selector for thier container. And the following forum discussions exist in course "Course 1": | forum | user | name | message | | Gradable forum | student1 | My topic | This is the thing I posted about | And the following forum replies exist in course "Course 1": | forum | user | discussion | message | | Gradable forum | student2 | My topic | I disagree | Scenario: Viewing a discussion Given I am on the "Gradable forum" "forum activity" page logged in as teacher And I press "Grade users" When I press "View discussion" Then I should see "I disagree" in the "My topic" "dialogue" And I click on "Cancel" "button" in the "My topic" "dialogue" And I should not see "I disagree" Scenario: Viewing a discussion while grading is fullscreen Given I am on the "Gradable forum" "forum activity" page logged in as teacher And I press "Grade users" # Uses the aria-label for the menu in in the grading interface. And I press "Actions for the grader interface" And I press "Toggle full screen" When I press "View discussion" Then I should see "I disagree" in the "My topic" "dialogue" And I click on "Cancel" "button" in the "My topic" "dialogue" And I should not see "I disagree" PK-B\%H 0tests/behat/forum_subscribers_navigation.featurenu[@mod @mod_forum @javascript Feature: A teacher or admin can view subscriptions tab Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher | Teacher | Tom | teacher@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher | C1 | editingteacher | And the following "activity" exists: | course | C1 | | activity | forum | | name | Test forum name | Scenario: A teacher views view subscribers by default and views the Subscribers heading Given I am on the "Test forum name" "forum activity" page logged in as teacher When I navigate to "Subscriptions" in current page administration Then I should see "View subscribers" in the "//div[@class='urlselect']//option[@selected]" "xpath_element" And I should see "Subscribers" And I should see "There are no subscribers yet for this forum." Scenario: A teacher selects forced subscription and subscribers selector is not visible Given I am on the "Test forum name" "forum activity" page logged in as teacher And I navigate to "Subscriptions" in current page administration When I select "Forced subscription" from the "Subscription mode" singleselect And I should see "Everyone is now subscribed to this forum" Then I should not see "View subscribers" And I should not see "Manage subscribers" And I should not see "Manage subscribers" # Now select Optional subscription And I select "Optional subscription" from the "Subscription mode" singleselect And I should see "Everyone can now choose to be subscribed" And I should see "View subscribers" Scenario: A teacher selects forced subscription and subscribers selector is not visible Given I am on the "Test forum name" "forum activity" page logged in as teacher And I navigate to "Subscriptions" in current page administration When I select "Manage subscribers" from the "jump" singleselect And I should see "Manage subscribers" Then "Subscription mode" "select" should not exist And I should not see "Optional subscription" And I should not see "Forced subscription" And I should not see "Auto subscription" And I should not see "Subscription disabled" Scenario: A teacher selects reports tab and verify the heading Given I am on the "Test forum name" "forum activity" page logged in as teacher And I should see "There are no discussion topics yet in this forum." in the "//div[contains(@class, 'alert-info')]" "xpath_element" And I navigate to "Reports" in current page administration And I should see "Forum summary report" And I should see "Nothing to display" in the "//div[contains(@class, 'alert-info')]" "xpath_element" PK-B\stests/behat/guest_users.featurenu[@mod @mod_forum @javascript Feature: Guest and not logged users could see the option to add new post or reply In order to guide users to create an account As a guest or not logged user I want to see the option to add new post or reply Background: Given the following config values are set as admin: | enrol_guest | Yes | And the following "users" exist: | username | firstname | lastname | email | | teacher | Teacher | 1 | teacher@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher | C1 | editingteacher | And I am on the "Course 1" "enrolment methods" page logged in as teacher And I click on "Enable" "link" in the "Guest access" "table_row" Scenario Outline: As a not enrolled guest I don't see the option to add a new discussion Given the following "activities" exist: | activity | name | course | idnumber | type | | forum | Forum | C1 | forum | | And the following "mod_forum > discussions" exist: | user | forum | name | message | | teacher | forum | Forum discussion 1 | How awesome is this forum discussion? | And I log out And I am on "Course 1" course homepage When I press "Access as a guest" And I am on the "Forum" "forum activity" page Then I should not see "Add discussion topic" And I should see "Forum discussion 1" And I click on "Forum discussion 1" "link" And I should not see "Reply" Examples: | type | | general | | eachuser | | qanda | Scenario: As a not enrolled guest I don't see the option to add a new discussion in a single forum Given the following "activities" exist: | activity | name | course | idnumber | type | | forum | Forum (single discussion) | C1 | forum | single | And I log out And I am on "Course 1" course homepage When I press "Access as a guest" And I am on the "Forum (single discussion)" "forum activity" page Then I should not see "Add discussion topic" And I should see "Forum (single discussion)" And I should not see "Reply" Scenario: As a not enrolled guest I don't see the option to add a new discussion in a blog type forum Given the following "activities" exist: | activity | name | course | idnumber | type | | forum | Forum | C1 | forum | blog | And the following "mod_forum > discussions" exist: | user | forum | name | message | | teacher | forum | Forum discussion 1 | How awesome is this forum discussion? | And I log out And I am on "Course 1" course homepage When I press "Access as a guest" And I am on the "Forum" "forum activity" page Then I should not see "Add discussion topic" And I should see "Forum discussion 1" And I should not see "Reply" Scenario Outline: As an enrolled guest I see the option to add a new discussion Given I am on the "Course 1" "enrolment methods" page logged in as teacher And I click on "Enable" "link" in the "Self enrolment" "table_row" And the following "activities" exist: | activity | name | course | idnumber | type | | forum | Forum | C1 | forum | | And the following "mod_forum > discussions" exist: | user | forum | name | message | | teacher | forum | Forum discussion 1 | How awesome is this forum discussion? | And I log out And I am on "Course 1" course homepage When I press "Access as a guest" And I am on the "Forum" "forum activity" page Then I should see "Add discussion topic" And I click on "Add discussion topic" "link" And I should see "Only logged in users can post to this forum" And I click on "Cancel" "button" And I should see "Forum discussion 1" And I click on "Forum discussion 1" "link" And I should see "Reply" And I click on "Reply" "link" And I should see "Only logged in users can post to this forum" And I click on "Log in" "button" And I should see "Log in" Examples: | type | | general | | eachuser | | qanda | Scenario: As an enrolled guest I see the option to reply in a single forum Given I am on the "Course 1" "enrolment methods" page logged in as teacher And I click on "Enable" "link" in the "Self enrolment" "table_row" And the following "activities" exist: | activity | name | course | idnumber | type | | forum | Forum (single discussion) | C1 | forum | single | And I log out And I am on "Course 1" course homepage When I press "Access as a guest" And I am on the "Forum (single discussion)" "forum activity" page And I should see "Forum (single discussion)" Then I should see "Reply" And I click on "Reply" "link" And I should see "Only logged in users can post to this forum" And I click on "Cancel" "button" And I should see "Reply" And I click on "Reply" "link" And I click on "Log in" "button" And I should see "Log in" Scenario: As an enrolled guest I see the option to reply in a blog type forum Given I am on the "Course 1" "enrolment methods" page logged in as teacher And I click on "Enable" "link" in the "Self enrolment" "table_row" And the following "activities" exist: | activity | name | course | idnumber | type | | forum | Forum | C1 | forum | blog | And the following "mod_forum > discussions" exist: | user | forum | name | message | | teacher | forum | Forum discussion 1 | How awesome is this forum discussion? | And I log out And I am on "Course 1" course homepage When I press "Access as a guest" And I am on the "Forum" "forum activity" page Then I should see "Add discussion topic" And I click on "Add discussion topic" "link" And I should see "Only logged in users can post to this forum" And I click on "Cancel" "button" And I should see "Forum discussion 1" And I click on "Add discussion topic" "link" And I should see "Only logged in users can post to this forum" And I click on "Log in" "button" And I should see "Log in" PK-B\+c.tests/behat/forum_default_rss_settings.featurenu[@mod @mod_forum @core_rss Feature: Verify that default RSS settings for forums are correctly applied. In order to ensure consistent RSS configuration across new forums As an admin I need to verify that the default RSS values set at site level appear when creating a new forum. Background: Given the following "courses" exist: | fullname | shortname | | RSS Test Course | RSS101 | And the following config values are set as admin: | enablerssfeeds | 1 | | forum_enablerssfeeds | 1 | | forum_rsstype | 1 | | forum_rssarticles | 5 | Scenario: Admin sets default forum RSS values and verifies them on new forum creation Given I log in as "admin" When I am on "RSS101" course homepage with editing mode on And I add a forum activity to course "RSS101" section "New section" And I expand all fieldsets Then the field "RSS feed for this activity" matches value "Discussions" And the field "Number of RSS recent articles" matches value "5" PK-B\L+tests/behat/single_forum_discussion.featurenu[@mod @mod_forum Feature: Single simple forum discussion type In order to restrict the discussion topic to one As a teacher I need to create a forum with a single simple discussion Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | name | intro | type | course | idnumber | | forum | Single discussion forum name | Single discussion forum description | single | C1 | forum | Scenario: Teacher can start the single simple discussion When I am on the "Single discussion forum name" "forum activity" page logged in as teacher1 Then I should see "Single discussion forum description" in the "div.firstpost.starter" "css_element" And I should not see "Add a new discussion topic" Scenario: Student can not add more discussions Given I am on the "Course 1" course page logged in as student1 When I reply "Single discussion forum name" post from "Single discussion forum name" forum with: | Subject | Reply to single discussion subject | | Message | Reply to single discussion message | Then I should not see "Add a new discussion topic" And I should see "Reply" in the "div.firstpost.starter" "css_element" And I should see "Reply to single discussion message" PK-B\NOO tests/behat/inpage_reply.featurenu[@mod @mod_forum @javascript Feature: Students can reply to a discussion in page. Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | teacher1 | C1 | editingteacher | And the following "activity" exists: | activity | forum | | course | C1 | | idnumber | 0001 | | name | Test forum name | And the following "mod_forum > discussions" exist: | forum | course | user | name | message | | 0001 | C1 | teacher1 | Discussion 1 | Discussion contents 1, first message | | 0001 | C1 | teacher1 | Discussion 2 | Discussion contents 2, first message | Scenario: Confirm inpage replies work Given I am on the "Course 1" course page logged in as student1 When I reply "Discussion 2" post from "Test forum name" forum using an inpage reply with: | post | Discussion contents 1, third message | Then I should see "Discussion contents 1, third message" And I reload the page And I should see "Discussion contents 1, third message" Scenario: Confirm inpage replies work - private reply Given I am on the "Course 1" course page logged in as teacher1 When I reply "Discussion 2" post from "Test forum name" forum using an inpage reply with: | post | Discussion contents 1, third message | | privatereply | 1 | Then I should see "Discussion contents 1, third message" And I should see "This is a private reply. (Teachers and other users with the capability to view private replies can also see it.)" And I reload the page And I should see "Discussion contents 1, third message" PK-B\=MH'''tests/behat/forum_subscriptions.featurenu[@mod @mod_forum Feature: A user can control their own subscription preferences for a forum In order to receive notifications for things I am interested in As a user I need to choose my forum subscriptions Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | One | student.one@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | And the following "activities" exist: | activity | course | idnumber | name | type | | forum | C1 | forum1 | Test forum name | general | And the following "mod_forum > discussions" exist: | forum | course | user | name | message | | forum1 | C1 | admin | Test post subject | Test post message | And I am on the "Test forum name" "forum activity editing" page logged in as admin Scenario: A disallowed subscription forum cannot be subscribed to Given I set the following fields to these values: | Subscription mode | Subscription disabled | And I press "Save and return to course" When I am on the "Test forum name" "forum activity" page logged in as student1 Then I should not see "Subscribe to this forum" And I should not see "Unsubscribe from this forum" And "You are subscribed to this discussion. Click to unsubscribe." "link" should not exist in the "Test post subject" "table_row" And "You are not subscribed to this discussion. Click to subscribe." "link" should not exist in the "Test post subject" "table_row" Scenario: A forced subscription forum cannot be subscribed to Given I set the following fields to these values: | Subscription mode | Forced subscription | And I press "Save and return to course" When I am on the "Test forum name" "forum activity" page logged in as student1 Then I should not see "Subscribe to this forum" And I should not see "Unsubscribe from this forum" And "You are subscribed to this discussion. Click to unsubscribe." "link" should not exist in the "Test post subject" "table_row" And "You are not subscribed to this discussion. Click to subscribe." "link" should not exist in the "Test post subject" "table_row" Scenario: An optional forum can be subscribed to Given I set the following fields to these values: | Subscription mode | Optional subscription | And I press "Save and return to course" When I am on the "Test forum name" "forum activity" page logged in as student1 Then I should see "Subscribe to forum" And I should not see "Unsubscribe from forum" And I follow "Subscribe to forum" And I should see "You will be notified of new posts in the forum 'Test forum name'" And I should see "Unsubscribe from forum" And I should not see "Subscribe to forum" Scenario: An Automatic forum can be unsubscribed from Given I set the following fields to these values: | Subscription mode | Auto subscription | And I press "Save and return to course" When I am on the "Test forum name" "forum activity" page logged in as student1 Then I should see "Unsubscribe from forum" And I should not see "Subscribe to forum" And I follow "Unsubscribe from forum" And I should see "Student One will NOT be notified of new posts in 'Test forum name'" And I should see "Subscribe to forum" And I should not see "Unsubscribe from forum" PK-B\QW"3tests/behat/forum_subscription_landing_page.featurenu[@mod @mod_forum @javascript Feature: A teacher or admin when changes the subscription mode should land in the subscriptions tab Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher | Teacher | Tom | teacher@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher | C1 | editingteacher | And the following "activity" exists: | course | C1 | | activity | forum | | name | Test forum name | Scenario: A teacher views subscriptions tab and changes the subscriptions mode to forced mode and lands in subscription tab Given I am on the "Test forum name" "forum activity" page logged in as teacher And I navigate to "Subscriptions" in current page administration When I select "Forced subscription" from the "Subscription mode" singleselect And I should see "Everyone is now subscribed to this forum" Then I should see "Forced subscription" And I should not see "View subscribers" And I should not see "Manage subscribers" PK-B\DNv.qq#tests/behat/recent_activity.featurenu[@mod @mod_forum @block_recent_activity Feature: Users can see the relevant recent forum posts from the recent activity block In order to quickly see the updates from forums in my course As a user I need to be able to see the recent forum posts in the recent activity block Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student2 | C1 | student | | teacher1 | C1 | editingteacher | And the following "groups" exist: | name | course | idnumber | | Group 1 | C1 | G1 | | Group 2 | C1 | G2 | And the following "group members" exist: | user | group | | student1 | G1 | | student2 | G2 | | teacher1 | G1 | | teacher1 | G2 | And the following "activities" exist: | activity | name | course | idnumber | type | groupmode | visible | | forum | Separate groups forum | C1 | forum1 | general | 1 | 1 | | forum | Visible groups forum | C1 | forum2 | general | 2 | 1 | | forum | Standard forum | C1 | forum3 | general | 0 | 1 | | forum | Hidden forum | C1 | forum4 | general | 0 | 0 | | forum | Q&A forum | C1 | forum5 | qanda | 0 | 1 | And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | recent_activity | Course | C1 | course-view-* | side-pre | And I log in as "teacher1" Scenario: Recent forum activity with separate group discussion Given the following "mod_forum > discussions" exist: | user | forum | name | message | group | | teacher1 | forum1 | Group 1 separate discussion | Group 1 members only | G1 | When I am on the "Course 1" course page logged in as student1 Then I should see "Group 1 separate discussion" in the "Recent activity" "block" And I am on the "Course 1" course page logged in as student2 And I should not see "Group 1 separate discussion" in the "Recent activity" "block" Scenario: Recent forum activity with visible groups discussion Given the following "mod_forum > discussions" exist: | user | forum | name | message | group | | teacher1 | forum2 | Group 1 visible discussion | Not just for group 1 members | G1 | When I am on the "Course 1" course page logged in as student1 Then I should see "Group 1 visible discussion" in the "Recent activity" "block" And I am on the "Course 1" course page logged in as student2 And I should see "Group 1 visible discussion" in the "Recent activity" "block" Scenario: Recent forum activity with recent post as a private reply Given the following "mod_forum > discussions" exist: | user | forum | name | message | | teacher1 | forum3 | Standard forum discussion | Discuss anything under the sun here! | And the following "mod_forum > posts" exist: | user | parentsubject | subject | message | privatereplyto | | teacher1 | Standard forum discussion | Teacher's private reply | This is a private reply | 1 | And I am on "Course 1" course homepage And I should see "Standard forum discussion" in the "Recent activity" "block" And I should see "Teacher's private reply" in the "Recent activity" "block" When I am on the "Course 1" course page logged in as student1 Then I should see "Standard forum discussion" in the "Recent activity" "block" But I should not see "Teacher's private reply" in the "Recent activity" "block" Scenario: Recent forum activity with recent post in a hidden forum Given the following "mod_forum > discussions" exist: | user | forum | name | message | | teacher1 | forum4 | Hidden discussion | Should be hidden! | And I am on "Course 1" course homepage And I should see "Hidden discussion" in the "Recent activity" "block" When I am on the "Course 1" course page logged in as student1 Then I should not see "Hidden discussion" in the "Recent activity" "block" Scenario: Recent forum activity with question and answer forum Given the following "mod_forum > discussions" exist: | user | forum | name | message | | teacher1 | forum5 | The egg vs the chicken | Which came first? The egg or the chicken? | And the following "mod_forum > posts" exist: | user | parentsubject | subject | message | | student1 | The egg vs the chicken | Student 1's answer | The egg! | And I am on the "Course 1" course page logged in as student1 And I should see "The egg vs the chicken" in the "Recent activity" "block" And I should see "Student 1's answer" in the "Recent activity" "block" And the following config values are set as admin: | maxeditingtime | 1 | When I am on the "Course 1" course page logged in as student2 Then I should see "The egg vs the chicken" in the "Recent activity" "block" But I should not see "Student 1's answer" in the "Recent activity" "block" And I reply "The egg vs the chicken" post from "Q&A forum" forum with: | Subject | Student 2's answer | | Message | The chicken, duh! | And I wait "2" seconds And I am on "Course 1" course homepage And I should see "Student 1's answer" in the "Recent activity" "block" And I should see "Student 2's answer" in the "Recent activity" "block" Scenario: Recent forum activity with timed discussion Given the following "mod_forum > discussions" exist: | user | forum | name | message | timeend | | teacher1 | forum3 | Timed discussion | Discuss anything under the sun here... no more!!! | ##1 Jan 2020 08:00## | And I am on "Course 1" course homepage And I should see "Timed discussion" in the "Recent activity" "block" When I am on the "Course 1" course page logged in as student1 Then I should not see "Timed discussion" in the "Recent activity" "block" PK-B\IA+tests/behat/post_to_multiple_groups.featurenu[@mod @mod_forum Feature: A user with access to multiple groups should be able to post a copy of a message to all the groups they have access to In order to post to all groups a user has access to As a user I need to have the option to post a copy of a message to all groups Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | | student3 | Student | 3 | student3@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | | Course 2 | C2 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher1 | C2 | editingteacher | | student1 | C1 | student | | student1 | C2 | student | | student2 | C1 | student | | student2 | C2 | student | | student3 | C1 | student | | student3 | C2 | student | And the following "groups" exist: | name | course | idnumber | | Group A | C1 | C1G1 | | Group B | C1 | C1G2 | | Group C | C1 | C1G3 | | Group A | C2 | C2G1 | | Group B | C2 | C2G2 | | Group C | C2 | C2G3 | And the following "groupings" exist: | name | course | idnumber | | G1 | C2 | G1 | And the following "group members" exist: | user | group | | teacher1 | C1G1 | | teacher1 | C1G2 | | teacher1 | C1G3 | | teacher1 | C2G1 | | teacher1 | C2G1 | | student1 | C1G1 | | student1 | C2G1 | | student1 | C2G2 | | student2 | C1G1 | | student2 | C1G2 | | student3 | C1G1 | | student3 | C1G2 | | student3 | C1G3 | And the following "grouping groups" exist: | grouping | group | | G1 | C2G1 | | G1 | C2G2 | And the following "activities" exist: | activity | name | course | idnumber | groupmode | grouping | | forum | No group forum | C1 | forum | 0 | | | forum | Separate group forum | C1 | forum | 1 | | | forum | Visible group forum | C1 | forum | 2 | | | forum | Groupings forum | C2 | forum | 1 | G1 | And the following forum discussions exist in course "Course 1": | user | forum | name | message | posttomygroups | | teacher1 | Separate group forum | Discussion 1 | test | 1 | | teacher1 | Visible group forum | Discussion 1 | test | 1 | Scenario: Teacher is able to post a copy of a message to all groups in a separate group forum When I am on the "Separate group forum" "forum activity" page logged in as student1 Then I should see "Discussion 1" And I am on the "Separate group forum" "forum activity" page logged in as student2 And I should see "Discussion 1" And I am on the "Separate group forum" "forum activity" page logged in as student3 And I should see "Discussion 1" Scenario: Teacher is able to post a copy of a message to all groups in a visible group forum When I am on the "Visible group forum" "forum activity" page logged in as student1 Then I should see "Discussion 1" And I am on the "Visible group forum" "forum activity" page logged in as student2 And I should see "Discussion 1" And I am on the "Visible group forum" "forum activity" page logged in as student3 And I should see "Discussion 1" Scenario: Teacher is unable to post a copy of a message to all groups in a no group forum Given I am on the "No group forum" "forum activity" page logged in as teacher1 When I click on "Add discussion topic" "link" Then I should not see "Post a copy to all groups" Scenario: Posts to all groups that have groupings should only display within the grouping and not to other groups Given I log in as "teacher1" And I am on "Course 2" course homepage And I add a new discussion to "Groupings forum" forum with: | Subject | Discussion 1 | | Message | test | | Post a copy to all groups | 1 | When I am on the "Groupings forum" "forum activity" page logged in as student1 Then I should see "Discussion 1" And I am on the "Groupings forum" "forum activity" page logged in as student2 And I should not see "Discussion 1" @javascript @mod_forum_quick_discussion_post_to_all_groups Scenario: The 'Post a copy to all groups' option can be available in the quick discussion creation form Given I log in as "teacher1" And I am on "Course 1" course homepage And I follow "Separate group forum" When I click on "Add discussion topic" "link" Then "Post a copy to all groups" "field" should exist PK-B\]](tests/behat/favourite_discussion.featurenu[@mod @mod_forum @javascript Feature: A student can favourite a discussion via the forum settings menu Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | And the following "activity" exists: | course | C1 | | activity | forum | | name | Test forum name | | idnumber | forum1 | And the following "mod_forum > discussions" exist: | user | forum | name | message | | admin | forum1 | Discussion 1 | Discussion contents 1, first message | And the following "mod_forum > posts" exist: | user | parentsubject | subject | message | | admin | Discussion 1 | Reply 1 to discussion 1 | Discussion contents 1, second message | | student1 | Discussion 1 | Reply 2 to discussion 1 | Discussion contents 1, third message | Scenario: Student can favourite a discussion from within an individual discussion Given I am on the "Test forum name" "forum activity" page logged in as student1 When I open the action menu in "[data-container='discussion-tools']" "css_element" And I click on "[title='Star this discussion']" "css_element" And I wait "3" seconds And I open the action menu in "[data-container='discussion-tools']" "css_element" And I click on "[title='Unstar this discussion']" "css_element" Scenario: Student can favourite a discussion from the discussion list When I am on the "Test forum name" "forum activity" page logged in as student1 And I click on "Discussion 1" action menu And I click on "[title='Star this discussion']" "css_element" And I click on "Discussion 1" action menu And I click on "[title='Unstar this discussion']" "css_element" PK-B\z"tests/behat/my_forum_posts.featurenu[@mod @mod_forum Feature: A user can view their posts and discussions In order to ensure a user can view their posts and discussions As a student I need to view my post and discussions Scenario: View the student's posts and discussions Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | And the following "activities" exist: | activity | name | course | idnumber | groupmode | | forum | Test forum name | C1 | forum | 0 | And the following "mod_forum > discussions" exist: | user | forum | name | message | | student1 | forum | Forum discussion 1 | How awesome is this forum discussion? | And the following "mod_forum > posts" exist: | user | parentsubject | subject | message | | student1 | Forum discussion 1 | Actually, I've seen better. | Actually, I've seen better. | And I log in as "student1" When I follow "Profile" in the user menu And I follow "Forum posts" Then I should see "How awesome is this forum discussion?" And I should see "Actually, I've seen better." And I follow "Profile" in the user menu And I follow "Forum discussions" And I should see "How awesome is this forum discussion?" And I should not see "Actually, I've seen better." PK-B\344)tests/behat/discussion_navigation.featurenu[@mod @mod_forum Feature: A user can navigate to previous and next discussions In order to get go the previous discussion As a user I need to click on the previous discussion link Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And the following "groups" exist: | name | course | idnumber | | Group 1 | C1 | G1 | | Group 2 | C1 | G2 | And the following "group members" exist: | user | group | | teacher1 | G1 | | teacher1 | G2 | | student1 | G1 | | student2 | G2 | Scenario: A user can navigate between discussions Given the following "activities" exist: | activity | name | course | idnumber | groupmode | | forum | Test forum name | C1 | forum | 0 | And the following "mod_forum > discussions" exist: | user | forum | name | message | | teacher1 | forum | Discussion 1 | Test post message | | teacher1 | forum | Discussion 2 | Test post message | | teacher1 | forum | Discussion 3 | Test post message | And I am on the "Test forum name" "forum activity" page logged in as teacher1 When I follow "Discussion 3" Then I should not see "Discussion 1" And I should see "Discussion 2" And I follow "Discussion 2" And I should see "Discussion 1" And I should see "Discussion 3" And I follow "Discussion 1" And I should see "Discussion 2" And I should not see "Discussion 3" And I follow "Reply" And I set the following fields to these values: | Message | Answer to discussion | # We need to wait a bit to guarantee that the post is created after the previous ones. # because there is a bug in the forum_get_discussion_neighbours() when all the discussion # last modified times are the same. See MDL-79247 for more details. And I wait "1" seconds And I press "Post to forum" And I should not see "Discussion 2" And I should see "Discussion 3" And I follow "Discussion 3" And I should see "Discussion 1" And I should see "Discussion 2" And I follow "Discussion 2" And I should not see "Discussion 1" And I should see "Discussion 3" Scenario: A user can navigate between discussions with visible groups Given the following "activities" exist: | activity | name | course | idnumber | groupmode | | forum | Test forum name | C1 | forum | 2 | And the following "mod_forum > discussions" exist: | user | forum | name | message | | student1 | forum | Discussion 1 Group 0 | Test post message | | student1 | forum | Discussion 2 Group 0 | Test post message | And the following "mod_forum > discussions" exist: | user | forum | name | message | group | | student1 | forum | Discussion 1 Group 1 | Test post message | G1 | | student1 | forum | Discussion 2 Group 1 | Test post message | G1 | | student1 | forum | Discussion 1 Group 2 | Test post message | G2 | | student1 | forum | Discussion 2 Group 2 | Test post message | G2 | When I am on the "Test forum name" "forum activity" page logged in as student1 And I select "All participants" from the "Visible groups" singleselect And I follow "Discussion 1 Group 0" Then I should see "Discussion 2 Group 0" And I should not see "Group 1" And I should not see "Group 2" And I follow "Discussion 2 Group 0" And I should see "Discussion 1 Group 0" And I should see "Discussion 1 Group 1" And I follow "Discussion 1 Group 1" And I should see "Discussion 2 Group 0" And I should see "Discussion 2 Group 1" And I follow "Test forum name" And I follow "Discussion 1 Group 2" And I should see "Discussion 2 Group 1" And I should see "Discussion 2 Group 2" And I follow "Test forum name" And I select "Group 1" from the "Visible groups" singleselect And I follow "Discussion 1 Group 1" And I should see "Discussion 2 Group 0" And I should see "Discussion 2 Group 1" And I follow "Discussion 2 Group 1" And I should see "Discussion 1 Group 1" And I should not see "Group 2" Scenario: A user can navigate between discussions with separate groups Given the following "activities" exist: | activity | name | course | idnumber | groupmode | | forum | Test forum name | C1 | forum | 1 | And the following "mod_forum > discussions" exist: | user | forum | name | message | | student1 | forum | Discussion 1 Group 0 | Test post message | | student1 | forum | Discussion 2 Group 0 | Test post message | And the following "mod_forum > discussions" exist: | user | forum | name | message | group | | student1 | forum | Discussion 1 Group 1 | Test post message | G1 | | student1 | forum | Discussion 2 Group 1 | Test post message | G1 | | student1 | forum | Discussion 1 Group 2 | Test post message | G2 | | student1 | forum | Discussion 2 Group 2 | Test post message | G2 | When I am on the "Test forum name" "forum activity" page logged in as student1 And I follow "Discussion 1 Group 1" Then I should see "Discussion 2 Group 0" And I should see "Discussion 2 Group 1" And I follow "Discussion 2 Group 1" And I should see "Discussion 1 Group 1" And I should not see "Group 2" PK-B\ag3 3 'tests/behat/posts_ordering_blog.featurenu[@mod @mod_forum Feature: Blog posts are always displayed in reverse chronological order In order to use forum as a blog As a user I need to see most recent blog posts first Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activity" exists: | course | C1 | | activity | forum | | name | Course blog forum | | type | blog | # # We need javascript/wait to prevent creation of the posts in the same second. The threads # would then ignore each other in the prev/next navigation as the Forum is unable to compute # the correct order. # @javascript Scenario: Replying to a blog post or editing it does not affect its display order Given I am on the "Course blog forum" "forum activity" page logged in as student1 # # Add three posts into the blog. # When I add a new topic to "Course blog forum" forum with: | Subject | Blog post 1 | | Message | This is the first post | And I add a new topic to "Course blog forum" forum with: | Subject | Blog post 2 | | Message | This is the second post | And I add a new topic to "Course blog forum" forum with: | Subject | Blog post 3 | | Message | This is the third post | # # Edit one of the blog posts. # And I click on "Edit" "link" in the "//div[@aria-label='Blog post 2 by Student 1']" "xpath_element" And I set the following fields to these values: | Subject | Edited blog post 2 | And I press "Save changes" And I wait to be redirected And I log out # # Reply to another blog post. # And I am on the "Course blog forum" "forum activity" page logged in as teacher1 And I click on "Discuss this topic" "link" in the "//div[@aria-label='Blog post 1 by Student 1']" "xpath_element" And I reply "Blog post 1" post from "Course blog forum" forum with: | Message | Reply to the first post | And I am on the "Course blog forum" "forum activity" page # # Make sure the order of the blog posts is still reverse chronological. # Then I should see "This is the third post" in the "//article[position()=1]" "xpath_element" And I should see "This is the second post" in the "//article[position()=2]" "xpath_element" And I should see "This is the first post" in the "//article[position()=3]" "xpath_element" # # Make sure the next/prev navigation uses the same order of the posts. # And I click on "Discuss this topic" "link" in the "//div[@aria-label='Edited blog post 2 by Student 1']" "xpath_element" And "//a[@aria-label='Next discussion: Blog post 3']" "xpath_element" should exist And "//a[@aria-label='Previous discussion: Blog post 1']" "xpath_element" should exist PK-B\"E,ۊ -tests/behat/forum_one_discussion_type.featurenu[@mod @mod_forum Feature: Each person posts one discussion forum type In order to limit the number of discussions in a forum As a teacher I want to set up a forum that allows each person to post one discussion topic Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | | student3 | Student | 3 | student3@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student2 | C1 | student | | student3 | C1 | student | And the following "activities" exist: | activity | name | course | type | idnumber | | forum | Forum 1 | C1 | eachuser | forum1 | Scenario: Student can only post once on a 'Each person posts one discussion' forum Given the following "mod_forum > discussions" exist: | user | forum | name | message | | student1 | forum1 | student1 discussion | posted by student1 | | student2 | forum1 | student2 discussion | posted by student2 | When I am on the "Forum 1" "forum activity" page logged in as student3 # Only 1 discussion can be added to forum. And I should see "This forum allows each person to start one discussion topic." And I click on "Add discussion topic" "link" And I set the following fields to these values: | Subject | student3 discussion | | Message | posted by student3 | And I press "Post to forum" # Confirm 2nd discussion topic cannot be added. Then "Add discussion topic" "link" should not exist # Confirm user can reply to other posts. And I click on "student1 discussion" "link" And I click on "Reply" "link" And I set the following fields to these values: | Message | Reply to student1 discussion | And I press "Post to forum" And I am on the "Forum 1" "forum activity" page And I click on "student2 discussion" "link" And I click on "Reply" "link" And I set the following fields to these values: | Message | Reply to student2 discussion | And I press "Post to forum" And I am on the "Forum 1" "forum activity" page And I click on "student3 discussion" "link" And I click on "Reply" "link" And I set the following fields to these values: | Message | Reply to student3 discussion | And I press "Post to forum" PK-B\[v#v#8tests/behat/forum_activity_completion_pass_grade.featurenu[@mod @mod_forum @core_completion Feature: Completion pass grade view activity completion in the forum activity In order to have visibility of forum completion requirements As a student I need to be able to view my forum completion progress Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Vinnie | Student1 | student1@example.com | | teacher1 | Darrell | Teacher1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | enablecompletion | | Course 1 | C1 | 0 | 1 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | teacher1 | C1 | editingteacher | And the following "activity" exists: | activity | forum | | course | C1 | | idnumber | mh1 | | name | Music history | And I am on the "Music history" "forum activity editing" page logged in as teacher1 And I expand all fieldsets And I set the following fields to these values: | Whole forum grading > Type | Point | | Whole forum grading > Grade to pass | 50 | | Add requirements | 1 | | View the activity | 1 | | Receive a grade | 1 | | Passing grade | 1 | | completiongradeitemnumber | Whole forum | | completionpostsenabled | 1 | | completionposts | 2 | | completiondiscussionsenabled | 1 | | completiondiscussions | 1 | | completionrepliesenabled | 1 | | completionreplies | 1 | And I press "Save and display" Scenario: Forum module displays completion conditions to teachers Given I am on the "Music history" "forum activity" page logged in as teacher1 Then "Music history" should have the "View" completion condition And "Music history" should have the "Start discussions: 1" completion condition And "Music history" should have the "Make forum posts: 2" completion condition And "Music history" should have the "Post replies: 1" completion condition And "Music history" should have the "Receive a grade" completion condition And "Music history" should have the "Receive a passing grade" completion condition @javascript Scenario: Student cannot complete a forum activity if one of the conditions are not met Given I am on the "Music history" "forum activity" page logged in as student1 And the "View" completion condition of "Music history" is displayed as "done" And the "Start discussions: 1" completion condition of "Music history" is displayed as "todo" And the "Make forum posts: 2" completion condition of "Music history" is displayed as "todo" And the "Post replies: 1" completion condition of "Music history" is displayed as "todo" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And the "Receive a passing grade" completion condition of "Music history" is displayed as "todo" And I add a new discussion to "Music history" forum with: | Subject | Fun instruments | | Message | I like drums | And I am on the "Music history" "forum activity" page And the "View" completion condition of "Music history" is displayed as "done" And the "Start discussions: 1" completion condition of "Music history" is displayed as "done" And the "Make forum posts: 2" completion condition of "Music history" is displayed as "todo" And the "Post replies: 1" completion condition of "Music history" is displayed as "todo" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And the "Receive a passing grade" completion condition of "Music history" is displayed as "todo" And I reply "Fun instruments" post from "Music history" forum with: | Subject | Reply 1 to Fun instruments | | Message | Guitar is also Fun | And I am on the "Music history" "forum activity" page And the "View" completion condition of "Music history" is displayed as "done" And the "Start discussions: 1" completion condition of "Music history" is displayed as "done" And the "Make forum posts: 2" completion condition of "Music history" is displayed as "done" And the "Post replies: 1" completion condition of "Music history" is displayed as "done" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And the "Receive a passing grade" completion condition of "Music history" is displayed as "todo" # Grade the student And I am on the "Music history" "forum activity" page logged in as teacher1 And I press "Grade users" And I set the field "grade" to "33" And I press "Save" And I press "Close grader" # All conditions should now be completed. When I am on the "Music history" "forum activity" page logged in as student1 Then the "View" completion condition of "Music history" is displayed as "done" And the "Start discussions: 1" completion condition of "Music history" is displayed as "done" And the "Make forum posts: 2" completion condition of "Music history" is displayed as "done" And the "Post replies: 1" completion condition of "Music history" is displayed as "done" And the "Receive a grade" completion condition of "Music history" is displayed as "done" And the "Receive a passing grade" completion condition of "Music history" is displayed as "failed" @javascript Scenario: Student can complete a forum activity when all conditions are met Given I am on the "Music history" "forum activity" page logged in as student1 And the "View" completion condition of "Music history" is displayed as "done" And the "Start discussions: 1" completion condition of "Music history" is displayed as "todo" And the "Make forum posts: 2" completion condition of "Music history" is displayed as "todo" And the "Post replies: 1" completion condition of "Music history" is displayed as "todo" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And the "Receive a passing grade" completion condition of "Music history" is displayed as "todo" And I add a new discussion to "Music history" forum with: | Subject | Fun instruments | | Message | I like drums | And I am on the "Music history" "forum activity" page And the "View" completion condition of "Music history" is displayed as "done" And the "Start discussions: 1" completion condition of "Music history" is displayed as "done" And the "Make forum posts: 2" completion condition of "Music history" is displayed as "todo" And the "Post replies: 1" completion condition of "Music history" is displayed as "todo" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And the "Receive a passing grade" completion condition of "Music history" is displayed as "todo" And I reply "Fun instruments" post from "Music history" forum with: | Subject | Reply 1 to Fun instruments | | Message | Guitar is also Fun | And I am on the "Music history" "forum activity" page And the "View" completion condition of "Music history" is displayed as "done" And the "Start discussions: 1" completion condition of "Music history" is displayed as "done" And the "Make forum posts: 2" completion condition of "Music history" is displayed as "done" And the "Post replies: 1" completion condition of "Music history" is displayed as "done" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And the "Receive a passing grade" completion condition of "Music history" is displayed as "todo" # Grade the student And I am on the "Music history" "forum activity" page logged in as teacher1 And I press "Grade users" And I set the field "grade" to "60" And I press "Save" And I press "Close grader" # All conditions should now be completed. When I am on the "Music history" "forum activity" page logged in as student1 Then the "View" completion condition of "Music history" is displayed as "done" And the "Start discussions: 1" completion condition of "Music history" is displayed as "done" And the "Make forum posts: 2" completion condition of "Music history" is displayed as "done" And the "Post replies: 1" completion condition of "Music history" is displayed as "done" And the "Receive a grade" completion condition of "Music history" is displayed as "done" And the "Receive a passing grade" completion condition of "Music history" is displayed as "done" PK-B\xk11$tests/behat/portfolio_export.featurenu[@mod @mod_forum Feature: Ensure only users with appropriate permissions can export forum discussions. Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | Scenario: A teacher can export discussions to a portfolio. Given the following "activities" exist: | activity | name | course | idnumber | groupmode | | forum | Test forum 1 | C1 | forum | 0 | And I log in as "admin" And the following config values are set as admin: | enableportfolios | 1 | And I navigate to "Plugins > Portfolios > Manage portfolios" in site administration And I set portfolio instance "File download" to "Enabled and visible" And I click on "Save" "button" When I am on the "Test forum 1" "forum activity" page logged in as student1 And I add a new discussion to "Test forum 1" forum with: | Subject | Discussion 1 | | Message | Test post message | And I reload the page And I follow "Discussion 1" Then "Export whole discussion to portfolio" "button" should not exist And I am on the "Test forum 1" "forum activity" page logged in as teacher1 And I follow "Discussion 1" And "Export whole discussion to portfolio" "button" should exist And I press "Export whole discussion to portfolio" And I should see "Exporting to portfolio" PK-B\ֺSB#tests/behat/move_discussion.featurenu[@mod @mod_forum Feature: A teacher can move discussions between forums In order to move a discussion As a teacher I need to use the move discussion selector Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | Scenario: A teacher can move discussions Given the following "activities" exist: | activity | name | course | idnumber | groupmode | | forum | Test forum 1 | C1 | forum1 | 0 | | forum | Test forum 2 | C1 | forum2 | 0 | And the following "mod_forum > discussions" exist: | user | forum | name | message | | student1 | forum1 | Discussion 1 | Test post message | And I am on the "Test forum 1" "forum activity" page logged in as teacher1 And I follow "Discussion 1" When I set the field "jump" to "Test forum 2" And I press "Move" Then I should see "This discussion has been moved to 'Test forum 2'." And I press "Move" And I should see "Discussion 1" PK-B\Q ,tests/behat/forum_display_word_count.featurenu[@mod @mod_forum Feature: Forum posts display word count In order to display forum word count As a teacher I need to be able to update forum and set "Display word count" to Yes Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | t1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | Scenario: Forum posts display word count for single simple discussion forum Given the following "activities" exist: | activity | course | name | type | displaywordcount | intro | | forum | C1 | Forum 1 | single | 1 | Single forum post | When I am on the "Forum 1" "forum activity" page logged in as teacher1 Then I should see "3 words" And I am on the "Forum 1" "forum activity editing" page And I expand all fieldsets And I set the field "Display word count" to "No" And I press "Save and display" And I am on the "Forum 1" "forum activity" page And I should not see "3 words" Scenario: Forum posts display word count for blog-like forum Given the following "activities" exist: | activity | course | name | type | displaywordcount | | forum | C1 | Forum 1 | blog | 1 | And the following "mod_forum > discussions" exist: | forum | name | subject | message | | Forum 1 | Blog Forum | Blog Forum | This is a blog forum post | When I am on the "Forum 1" "forum activity" page logged in as teacher1 Then I should see "6 words" And I am on the "Forum 1" "forum activity editing" page And I expand all fieldsets And I set the field "Display word count" to "No" And I press "Save and display" And I am on the "Forum 1" "forum activity" page And I should not see "6 words" Scenario Outline: Forum posts display word count for other forum types Given the following "activities" exist: | activity | course | name | type | displaywordcount | | forum | C1 | Forum 1 | | 1 | And the following "mod_forum > discussions" exist: | forum | name | subject | message | | Forum 1 | | | | When I am on the "Forum 1" "forum activity" page logged in as teacher1 And I follow "" Then I should see " words" And I am on the "Forum 1" "forum activity editing" page And I expand all fieldsets And I set the field "Display word count" to "No" And I press "Save and display" And I am on the "Forum 1" "forum activity" page And I follow "" Then I should not see " words" Examples: | type | typeext | message | count | | general | General Forum | General discussion in forum | 4 | | eachuser | Each User Forum | This is an each user forum post | 7 | | qanda | Q and A Forum | This is a Q and A type forum | 8 | PK-B\)6tests/behat/forum_subscriptions_mode_behaviour.featurenu[@mod @mod_forum Feature: Changes to the subscription mode of a forum can change subcribers of a forum Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher | Teacher | Tom | teacher@example.com | | student1 | Student | 1 | student.1@example.com | | student2 | Student | 2 | student.2@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And the following "activity" exists: | activity | forum | | idnumber | f01 | | course | C1 | | name | Test forum name | | type | general | | forcesubscribe | 1 | Scenario: A change from Forced subscription to Auto subcription causes all participants to be subscribed Given I am on the "Test forum name" "forum activity" page logged in as teacher And I navigate to "Subscriptions" in current page administration When I select "Auto subscription" from the "Subscription mode" singleselect Then I should not see "There are no subscribers yet for this forum." And I navigate to "Subscriptions" in current page administration And the following should exist in the "subscribers-table" table: | Full name | Email address | | Student 1 | student.1@example.com | | Student 2 | student.2@example.com | | Teacher Tom | teacher@example.com | PK-B\Me e 4tests/entities_post_read_receipt_collection_test.phpnu[. namespace mod_forum; use mod_forum\local\entities\post_read_receipt_collection as collection_entity; use mod_forum\local\entities\post as post_entity; /** * The post_read_receipt_collection entity tests. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class entities_post_read_receipt_collection_test extends \advanced_testcase { /** * Test the entity returns expected values. */ public function test_entity(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $missingpost = new post_entity( 4, 1, 0, 1, time(), time(), true, 'post subject', 'post message', 1, true, false, 0, false, false, false, null, null ); $post = new post_entity( 1, 1, 0, 1, time(), time(), true, 'post subject', 'post message', 1, true, false, 0, false, false, false, null, null ); $collection = new collection_entity([ (object) [ 'postid' => 1, 'userid' => $user->id + 1 ], (object) [ 'postid' => 1, 'userid' => $user->id ], (object) [ 'postid' => 4, 'userid' => $user->id + 1 ] ]); $this->assertEquals(true, $collection->has_user_read_post($user, $post)); $this->assertEquals(false, $collection->has_user_read_post($user, $missingpost)); } } PK-B\5htests/qanda_test.phpnu[. namespace mod_forum; use mod_forum_tests_cron_trait; use mod_forum_tests_generator_trait; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/forum/lib.php'); require_once(__DIR__ . '/cron_trait.php'); require_once(__DIR__ . '/generator_trait.php'); /** * The forum module mail generation tests for groups. * * @package mod_forum * @copyright 2013 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class qanda_test extends \advanced_testcase { // Make use of the cron tester trait. use mod_forum_tests_cron_trait; // Make use of the test generator trait. use mod_forum_tests_generator_trait; /** * @var \phpunit_message_sink */ protected $messagesink; /** * @var \phpunit_mailer_sink */ protected $mailsink; public function setUp(): void { global $CFG; parent::setUp(); // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); \mod_forum\subscriptions::reset_discussion_cache(); // Messaging is not compatible with transactions... $this->preventResetByRollback(); // Catch all messages. $this->messagesink = $this->redirectMessages(); $this->mailsink = $this->redirectEmails(); // Forcibly reduce the maxeditingtime to a second in the past to // ensure that messages are sent out. $CFG->maxeditingtime = -1; } public function tearDown(): void { // We must clear the subscription caches. This has to be done both before each test, and after in case of other // tests using these functions. \mod_forum\subscriptions::reset_forum_cache(); $this->messagesink->clear(); $this->messagesink->close(); unset($this->messagesink); $this->mailsink->clear(); $this->mailsink->close(); unset($this->mailsink); parent::tearDown(); } /** * Test that a user who has not posted in a q&a forum does not receive * notificatinos. */ public function test_user_has_not_posted(): void { global $CFG, $DB; $this->resetAfterTest(true); // Create a course, with a forum. $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, 'forcesubscribe' => FORUM_INITIALSUBSCRIBE, 'groupmode' => SEPARATEGROUPS, 'type' => 'qanda', ]); // Create three students: // - author, enrolled in group A; and // - recipient, enrolled in group B; and // - other, enrolled in the course, but no groups. list($author, $recipient, $otheruser) = $this->helper_create_users($course, 3); // Create one editing teacher, not in any group but with accessallgroups capability. list($editingteacher) = $this->helper_create_users($course, 1, 'editingteacher'); // Post a discussion to the forum. list($discussion, $post) = $this->helper_post_to_forum($forum, $editingteacher); $reply = $this->helper_reply_to_post($post, $author); $otherreply = $this->helper_reply_to_post($post, $recipient); $DB->execute("UPDATE {forum_posts} SET modified = modified - 1"); $DB->execute("UPDATE {forum_posts} SET created = created - 1"); $DB->execute("UPDATE {forum_discussions} SET timemodified = timemodified - 1"); // Only the author, recipient, and teachers should receive. $expect = [ 'author' => (object) [ 'userid' => $author->id, 'messages' => 3, ], 'recipient' => (object) [ 'userid' => $recipient->id, 'messages' => 3, ], 'otheruser' => (object) [ 'userid' => $otheruser->id, 'messages' => 3, ], 'editingteacher' => (object) [ 'userid' => $editingteacher->id, 'messages' => 3, ], ]; $this->queue_tasks_and_assert($expect); $posts = [$post, $reply, $otherreply]; // No notifications should be queued. $this->send_notifications_and_assert($author, $posts); $this->send_notifications_and_assert($recipient, $posts); $this->send_notifications_and_assert($otheruser, [$post]); $this->send_notifications_and_assert($editingteacher, $posts); } } PK-B\\ tests/rsslib_test.phpnu[. namespace mod_forum; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once(__DIR__ . '/generator_trait.php'); require_once("{$CFG->dirroot}/mod/forum/rsslib.php"); /** * Tests for the forum implementation of the RSS component. * * @package mod_forum * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class rsslib_test extends \advanced_testcase { // Include the mod_forum test helpers. // This includes functions to create forums, users, discussions, and posts. use \mod_forum_tests_generator_trait; /** * Ensure that deleted posts are not included. */ public function test_forum_rss_feed_discussions_sql_respect_deleted(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm = get_coursemodule_from_instance('forum', $forum->id); list($user, $otheruser) = $this->helper_create_users($course, 2); // Post twice. $this->helper_post_to_forum($forum, $otheruser); list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser); list($sql, $params) = forum_rss_feed_discussions_sql($forum, $cm); $discussions = $DB->get_records_sql($sql, $params); $this->assertCount(2, $discussions); $post->deleted = 1; $DB->update_record('forum_posts', $post); list($sql, $params) = forum_rss_feed_discussions_sql($forum, $cm); $discussions = $DB->get_records_sql($sql, $params); $this->assertCount(1, $discussions); } /** * Ensure that deleted posts are not included. */ public function test_forum_rss_feed_posts_sql_respect_deleted(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm = get_coursemodule_from_instance('forum', $forum->id); list($user, $otheruser) = $this->helper_create_users($course, 2); // Post twice. $this->helper_post_to_forum($forum, $otheruser); list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser); list($sql, $params) = forum_rss_feed_posts_sql($forum, $cm); $posts = $DB->get_records_sql($sql, $params); $this->assertCount(2, $posts); $post->deleted = 1; $DB->update_record('forum_posts', $post); list($sql, $params) = forum_rss_feed_posts_sql($forum, $cm); $posts = $DB->get_records_sql($sql, $params); $this->assertCount(1, $posts); } } PK-B\ǵYYtests/exporters_post_test.phpnu[. /** * The post exporter tests. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum; use mod_forum\local\entities\discussion as discussion_entity; use mod_forum\local\entities\post as post_entity; use mod_forum\local\exporters\post as post_exporter; use mod_forum\local\managers\capability as capability_manager; use mod_forum_tests_generator_trait; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once(__DIR__ . '/generator_trait.php'); require_once($CFG->dirroot . '/rating/lib.php'); /** * The post exporter tests. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class exporters_post_test extends \advanced_testcase { // Make use of the test generator trait. use mod_forum_tests_generator_trait; /** * Test the export function returns expected values. * * @dataProvider export_post_provider * @param bool $istimed True if this is a timed post * @param int $addtime Seconds to be added to the current time */ public function test_export_post($istimed = false, $addtime = 0): void { global $CFG, $PAGE; $this->resetAfterTest(); $CFG->enableportfolios = true; $filestorage = get_file_storage(); $renderer = $PAGE->get_renderer('core'); $datagenerator = $this->getDataGenerator(); $forumgenerator = $this->getDataGenerator()->get_plugin_generator('mod_forum'); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); $coursemodule = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($coursemodule->id); $now = time(); $forumgenparams = [ 'course' => $forum->course, 'userid' => $user->id, 'forum' => $forum->id, ]; if ($istimed) { $forumgenparams['timestart'] = $now + $addtime; } $discussion = $forumgenerator->create_discussion((object) $forumgenparams); $post = $forumgenerator->create_post((object) [ 'discussion' => $discussion->id, 'parent' => 0, 'userid' => $user->id, 'created' => $now, 'modified' => $now, 'subject' => 'This is the subject', 'message' => 'This is the message', 'messagetrust' => 1, 'attachment' => 0, 'totalscore' => 0, 'mailnow' => 1, 'deleted' => 0 ]); \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['foo', 'bar']); $tags = \core_tag_tag::get_item_tags('mod_forum', 'forum_posts', $post->id); $attachment = $filestorage->create_file_from_string( [ 'contextid' => $context->id, 'component' => 'mod_forum', 'filearea' => 'attachment', 'itemid' => $post->id, 'filepath' => '/', 'filename' => 'example1.jpg', ], 'image contents' ); $canview = true; $canedit = true; $candelete = true; $cansplit = true; $canreply = true; $canexport = true; $cancontrolreadstatus = true; $canreplyprivately = true; $canenrol = true; $capabilitymanager = new test_capability_manager( $canview, $canedit, $candelete, $cansplit, $canreply, $canexport, $cancontrolreadstatus, $canreplyprivately, $canenrol ); $managerfactory = \mod_forum\local\container::get_manager_factory(); $entityfactory = \mod_forum\local\container::get_entity_factory(); $forum = $entityfactory->get_forum_from_stdClass($forum, $context, $coursemodule, $course); $discussion = $entityfactory->get_discussion_from_stdClass($discussion); $post = $entityfactory->get_post_from_stdClass($post); $author = $entityfactory->get_author_from_stdClass($user); $authorcontext = \context_user::instance($author->get_id()); $exporter = new post_exporter($post, [ 'legacydatamapperfactory' => \mod_forum\local\container::get_legacy_data_mapper_factory(), 'capabilitymanager' => $capabilitymanager, 'readreceiptcollection' => null, 'urlfactory' => \mod_forum\local\container::get_url_factory(), 'forum' => $forum, 'discussion' => $discussion, 'author' => $author, 'authorcontextid' => $authorcontext->id, 'user' => $user, 'context' => $context, 'authorgroups' => [], 'attachments' => [$attachment], 'tags' => $tags, 'rating' => null, 'includehtml' => true ]); $exportedpost = $exporter->export($renderer); $this->assertEquals('This is the subject', $exportedpost->subject); $this->assertEquals('This is the message', $exportedpost->message); $this->assertEquals($user->id, $exportedpost->author->id); $this->assertEquals($discussion->get_id(), $exportedpost->discussionid); $this->assertEquals(false, $exportedpost->hasparent); $this->assertEquals(null, $exportedpost->parentid); if ($istimed && ($addtime > 0)) { $this->assertEquals($now + $addtime, $exportedpost->timecreated); } else { $this->assertEquals($now, $exportedpost->timecreated); } $this->assertEquals(null, $exportedpost->unread); $this->assertEquals(false, $exportedpost->isdeleted); $this->assertEquals($canview, $exportedpost->capabilities['view']); $this->assertEquals($canedit, $exportedpost->capabilities['edit']); $this->assertEquals($candelete, $exportedpost->capabilities['delete']); $this->assertEquals($cansplit, $exportedpost->capabilities['split']); $this->assertEquals($canreply, $exportedpost->capabilities['reply']); $this->assertEquals($canexport, $exportedpost->capabilities['export']); $this->assertEquals($canenrol, $exportedpost->capabilities['selfenrol']); $this->assertEquals($cancontrolreadstatus, $exportedpost->capabilities['controlreadstatus']); $this->assertNotEmpty($exportedpost->urls['view']); $this->assertNotEmpty($exportedpost->urls['viewisolated']); $this->assertNotEmpty($exportedpost->urls['edit']); $this->assertNotEmpty($exportedpost->urls['delete']); $this->assertNotEmpty($exportedpost->urls['split']); $this->assertNotEmpty($exportedpost->urls['reply']); $this->assertNotEmpty($exportedpost->urls['markasread']); $this->assertNotEmpty($exportedpost->urls['markasunread']); $this->assertCount(1, $exportedpost->attachments); $this->assertEquals('example1.jpg', $exportedpost->attachments[0]->filename); $this->assertCount(2, $exportedpost->tags); $this->assertEquals('foo', $exportedpost->tags[0]['displayname']); $this->assertEquals('bar', $exportedpost->tags[1]['displayname']); $this->assertEquals(null, $exportedpost->html['rating']); $this->assertNotEquals(null, $exportedpost->html['taglist']); $this->assertNotEmpty($exportedpost->html['authorsubheading']); } /** * Data provider for test_export_post(). * * @return array */ public static function export_post_provider(): array { return [ 'Simple export' => [ ], 'Test timed post future' => [ true, 1000 ], 'Test timed post past' => [ true, -1000 ], ]; } /** * Test exporting of a deleted post. */ public function test_export_deleted_post(): void { global $CFG, $PAGE; $this->resetAfterTest(); $CFG->enableportfolios = true; $filestorage = get_file_storage(); $renderer = $PAGE->get_renderer('core'); $datagenerator = $this->getDataGenerator(); $forumgenerator = $this->getDataGenerator()->get_plugin_generator('mod_forum'); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); $coursemodule = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($coursemodule->id); $discussion = $forumgenerator->create_discussion((object) [ 'course' => $forum->course, 'userid' => $user->id, 'forum' => $forum->id ]); $now = time(); $post = $forumgenerator->create_post((object) [ 'discussion' => $discussion->id, 'parent' => 0, 'userid' => $user->id, 'created' => $now, 'modified' => $now, 'subject' => 'This is the subject', 'message' => 'This is the message', 'messagetrust' => 1, 'attachment' => 0, 'totalscore' => 0, 'mailnow' => 1, 'deleted' => 1 ]); \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['foo', 'bar']); $tags = \core_tag_tag::get_item_tags('mod_forum', 'forum_posts', $post->id); $attachment = $filestorage->create_file_from_string( [ 'contextid' => $context->id, 'component' => 'mod_forum', 'filearea' => 'attachment', 'itemid' => $post->id, 'filepath' => '/', 'filename' => 'example1.jpg', ], 'image contents' ); $canview = true; $canedit = true; $candelete = true; $cansplit = true; $canreply = true; $canexport = true; $cancontrolreadstatus = true; $capabilitymanager = new test_capability_manager( $canview, $canedit, $candelete, $cansplit, $canreply, $canexport, $cancontrolreadstatus ); $managerfactory = \mod_forum\local\container::get_manager_factory(); $entityfactory = \mod_forum\local\container::get_entity_factory(); $forum = $entityfactory->get_forum_from_stdClass($forum, $context, $coursemodule, $course); $discussion = $entityfactory->get_discussion_from_stdClass($discussion); $post = $entityfactory->get_post_from_stdClass($post); $author = $entityfactory->get_author_from_stdClass($user); $authorcontext = \context_user::instance($author->get_id()); $exporter = new post_exporter($post, [ 'legacydatamapperfactory' => \mod_forum\local\container::get_legacy_data_mapper_factory(), 'capabilitymanager' => $capabilitymanager, 'readreceiptcollection' => null, 'urlfactory' => \mod_forum\local\container::get_url_factory(), 'forum' => $forum, 'discussion' => $discussion, 'author' => $author, 'authorcontextid' => $authorcontext->id, 'user' => $user, 'context' => $context, 'authorgroups' => [], 'attachments' => [$attachment], 'tags' => $tags, 'rating' => null, 'includehtml' => true ]); $exportedpost = $exporter->export($renderer); $this->assertNotEquals('This is the subject', $exportedpost->subject); $this->assertNotEquals('This is the message', $exportedpost->message); $this->assertEquals(null, $exportedpost->timecreated); $this->assertEquals(null, $exportedpost->unread); $this->assertEquals(true, $exportedpost->isdeleted); $this->assertEquals([], $exportedpost->attachments); $this->assertEquals([], $exportedpost->tags); $this->assertEquals(null, $exportedpost->html['rating']); $this->assertEquals(null, $exportedpost->html['taglist']); $this->assertEquals(null, $exportedpost->html['authorsubheading']); } /** * Test exporting of a post the user can't view. */ public function test_export_post_no_view_capability(): void { global $CFG, $PAGE; $this->resetAfterTest(); $CFG->enableportfolios = true; $filestorage = get_file_storage(); $renderer = $PAGE->get_renderer('core'); $datagenerator = $this->getDataGenerator(); $forumgenerator = $this->getDataGenerator()->get_plugin_generator('mod_forum'); $user = $datagenerator->create_user(); $course = $datagenerator->create_course(); $forum = $datagenerator->create_module('forum', ['course' => $course->id]); $coursemodule = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($coursemodule->id); $discussion = $forumgenerator->create_discussion((object) [ 'course' => $forum->course, 'userid' => $user->id, 'forum' => $forum->id ]); $now = time(); $post = $forumgenerator->create_post((object) [ 'discussion' => $discussion->id, 'parent' => 0, 'userid' => $user->id, 'created' => $now, 'modified' => $now, 'subject' => 'This is the subject', 'message' => 'This is the message', 'messagetrust' => 1, 'attachment' => 0, 'totalscore' => 0, 'mailnow' => 1, 'deleted' => 0 ]); \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['foo', 'bar']); $tags = \core_tag_tag::get_item_tags('mod_forum', 'forum_posts', $post->id); $attachment = $filestorage->create_file_from_string( [ 'contextid' => $context->id, 'component' => 'mod_forum', 'filearea' => 'attachment', 'itemid' => $post->id, 'filepath' => '/', 'filename' => 'example1.jpg', ], 'image contents' ); $canview = false; $canedit = true; $candelete = true; $cansplit = true; $canreply = true; $canexport = true; $cancontrolreadstatus = true; $capabilitymanager = new test_capability_manager( $canview, $canedit, $candelete, $cansplit, $canreply, $canexport, $cancontrolreadstatus ); $managerfactory = \mod_forum\local\container::get_manager_factory(); $entityfactory = \mod_forum\local\container::get_entity_factory(); $forum = $entityfactory->get_forum_from_stdClass($forum, $context, $coursemodule, $course); $discussion = $entityfactory->get_discussion_from_stdClass($discussion); $post = $entityfactory->get_post_from_stdClass($post); $author = $entityfactory->get_author_from_stdClass($user); $authorcontext = \context_user::instance($author->get_id()); $exporter = new post_exporter($post, [ 'legacydatamapperfactory' => \mod_forum\local\container::get_legacy_data_mapper_factory(), 'capabilitymanager' => $capabilitymanager, 'readreceiptcollection' => null, 'urlfactory' => \mod_forum\local\container::get_url_factory(), 'forum' => $forum, 'discussion' => $discussion, 'author' => $author, 'authorcontextid' => $authorcontext->id, 'user' => $user, 'context' => $context, 'authorgroups' => [], 'attachments' => [$attachment], 'tags' => $tags, 'rating' => null, 'includehtml' => true ]); $exportedpost = $exporter->export($renderer); $this->assertNotEquals('This is the subject', $exportedpost->subject); $this->assertNotEquals('This is the message', $exportedpost->message); $this->assertEquals(null, $exportedpost->timecreated); $this->assertEquals(null, $exportedpost->unread); $this->assertEquals(false, $exportedpost->isdeleted); $this->assertEquals([], $exportedpost->attachments); $this->assertEquals([], $exportedpost->tags); $this->assertEquals(null, $exportedpost->html['rating']); $this->assertEquals(null, $exportedpost->html['taglist']); $this->assertEquals(null, $exportedpost->html['authorsubheading']); } } /** * Test implementation of the capability manager. * * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class test_capability_manager extends capability_manager { /** @var bool $view Value for can_view_post */ private $view; /** @var bool $edit Value for can_edit_post */ private $edit; /** @var bool $delete Value for can_delete_post */ private $delete; /** @var bool $split Value for can_split_post */ private $split; /** @var bool $reply Value for can_reply_to_post */ private $reply; /** @var bool $export Value for can_export_post */ private $export; /** @var bool $controlreadstatus Value for can_manually_control_post_read_status */ private $controlreadstatus; /** @var bool $controlreadstatus Value for can_reply_privately_to_post */ private $canreplyprivatelytopost; /** @var bool $canenrol Value for can_self_enrol */ private $canenrol; /** * Constructor. * * @param bool $view Value for can_view_post * @param bool $edit Value for can_edit_post * @param bool $delete Value for can_delete_post * @param bool $split Value for can_split_post * @param bool $reply Value for can_reply_to_post * @param bool $export Value for can_export_post * @param bool $controlreadstatus Value for can_manually_control_post_read_status */ public function __construct( bool $view = true, bool $edit = true, bool $delete = true, bool $split = true, bool $reply = true, bool $export = true, bool $controlreadstatus = true, bool $canreplyprivatelytopost = true, bool $canenrol = true ) { $this->view = $view; $this->edit = $edit; $this->delete = $delete; $this->split = $split; $this->reply = $reply; $this->export = $export; $this->controlreadstatus = $controlreadstatus; $this->canreplyprivatelytopost = $canreplyprivatelytopost; $this->canenrol = $canenrol; } /** * Override can_view_post * * @param \stdClass $user The user * @param discussion_entity $discussion The discussion * @param post_entity $post The post * @return bool */ public function can_view_post(\stdClass $user, discussion_entity $discussion, post_entity $post): bool { return $this->view; } /** * Override can_edit_post * * @param \stdClass $user The user * @param discussion_entity $discussion The discussion * @param post_entity $post The post * @return bool */ public function can_edit_post(\stdClass $user, discussion_entity $discussion, post_entity $post): bool { return $this->edit; } /** * Override can_delete_post * * @param \stdClass $user The user * @param discussion_entity $discussion The discussion * @param post_entity $post The post * @param bool $hasreplies * @return bool */ public function can_delete_post(\stdClass $user, discussion_entity $discussion, post_entity $post, bool $hasreplies = false): bool { return $this->delete; } /** * Override can_split_post * * @param \stdClass $user The user * @param discussion_entity $discussion The discussion * @param post_entity $post The post * @return bool */ public function can_split_post(\stdClass $user, discussion_entity $discussion, post_entity $post): bool { return $this->split; } /** * Override can_reply_to_post * * @param \stdClass $user The user * @param discussion_entity $discussion The discussion * @param post_entity $post The post * @return bool */ public function can_reply_to_post(\stdClass $user, discussion_entity $discussion, post_entity $post): bool { return $this->reply; } /** * Override can_export_post * * @param \stdClass $user The user * @param post_entity $post The post * @return bool */ public function can_export_post(\stdClass $user, post_entity $post): bool { return $this->export; } /** * Override can_manually_control_post_read_status * * @param \stdClass $user The user * @return bool */ public function can_manually_control_post_read_status(\stdClass $user): bool { return $this->controlreadstatus; } /** * Override can_reply_privately_to_post * @param \stdClass $user * @param post_entity $post * @return bool */ public function can_reply_privately_to_post(\stdClass $user, post_entity $post): bool { return $this->canreplyprivatelytopost; } /** * Override can_self_enrol * @param \stdClass $user * @return bool */ public function can_self_enrol(\stdClass $user): bool { return $this->canenrol; } } PK-B\'ǽtests/entities_forum_test.phpnu[. namespace mod_forum; use mod_forum\local\entities\discussion as discussion_entity; use mod_forum\local\entities\forum as forum_entity; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/rating/lib.php'); /** * The forum entity tests. * * @package mod_forum * @copyright 2019 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class entities_forum_test extends \advanced_testcase { /** * Test the entity returns expected values. */ public function test_entity(): void { $this->resetAfterTest(); $time = time() - 10; $discussion = new discussion_entity( 1, 2, 3, 'test discussion', 4, 5, 6, false, $time, $time, 0, 0, false, 0 ); $past = time() - 100; $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $coursemodule = get_coursemodule_from_instance('forum', $forum->id); $context = \context_module::instance($coursemodule->id); $effectivegroupmode = NOGROUPS; $id = 1; $courseid = 2; $type = 'standard'; $name = 'test forum'; $intro = 'this is the intro'; $introformat = FORMAT_MOODLE; $assessed = RATING_AGGREGATE_NONE; $assesstimestart = 0; $assesstimefinish = 0; $scale = 0; $gradeforum = 0; $maxbytes = 200; $maxattachments = 5; $forcesubscribe = 0; $trackingtype = 1; $rsstype = 0; $rssarticles = 0; $timemodified = $past; $warnafter = 0; $blockafter = 0; $blockperiod = 0; $completiondiscussions = 0; $completionreplies = 0; $completionposts = 0; $displaywordcount = false; $lockdiscussionafter = 0; $duedate = 0; $cutoffdate = 0; $sendnotification = false; $forum = new forum_entity( $context, $coursemodule, $course, $effectivegroupmode, $id, $courseid, $type, $name, $intro, $introformat, $assessed, $assesstimestart, $assesstimefinish, $scale, $gradeforum, $sendnotification, $maxbytes, $maxattachments, $forcesubscribe, $trackingtype, $rsstype, $rssarticles, $timemodified, $warnafter, $blockafter, $blockperiod, $completiondiscussions, $completionreplies, $completionposts, $displaywordcount, $lockdiscussionafter, $duedate, $cutoffdate ); $this->assertEquals($context, $forum->get_context()); $this->assertEquals($coursemodule, $forum->get_course_module_record()); $this->assertEquals($coursemodule, $forum->get_course_module_record()); $this->assertEquals($effectivegroupmode, $forum->get_effective_group_mode()); $this->assertEquals(false, $forum->is_in_group_mode()); $this->assertEquals($course, $forum->get_course_record()); $this->assertEquals($id, $forum->get_id()); $this->assertEquals($courseid, $forum->get_course_id()); $this->assertEquals($name, $forum->get_name()); $this->assertEquals($intro, $forum->get_intro()); $this->assertEquals($introformat, $forum->get_intro_format()); $this->assertEquals($assessed, $forum->get_rating_aggregate()); // Rating aggregate is set to none. $this->assertEquals(false, $forum->has_rating_aggregate()); $this->assertEquals($assesstimestart, $forum->get_assess_time_start()); $this->assertEquals($assesstimefinish, $forum->get_assess_time_finish()); $this->assertEquals($scale, $forum->get_scale()); $this->assertEquals($gradeforum, $forum->get_grade_for_forum()); $this->assertEquals($maxbytes, $forum->get_max_bytes()); $this->assertEquals($maxattachments, $forum->get_max_attachments()); $this->assertEquals($forcesubscribe, $forum->get_subscription_mode()); $this->assertEquals($trackingtype, $forum->get_tracking_type()); $this->assertEquals($rsstype, $forum->get_rss_type()); $this->assertEquals($rssarticles, $forum->get_rss_articles()); $this->assertEquals($timemodified, $forum->get_time_modified()); $this->assertEquals($warnafter, $forum->get_warn_after()); $this->assertEquals($blockafter, $forum->get_block_after()); $this->assertEquals($blockperiod, $forum->get_block_period()); $this->assertEquals(false, $forum->has_blocking_enabled()); $this->assertEquals($completiondiscussions, $forum->get_completion_discussions()); $this->assertEquals($completionreplies, $forum->get_completion_replies()); $this->assertEquals($completionposts, $forum->get_completion_posts()); $this->assertEquals($displaywordcount, $forum->should_display_word_count()); $this->assertEquals($lockdiscussionafter, $forum->get_lock_discussions_after()); $this->assertEquals(false, $forum->has_lock_discussions_after()); $this->assertEquals(false, $forum->is_discussion_locked($discussion)); $this->assertEquals(false, $forum->has_due_date()); $this->assertEquals(false, $forum->is_due_date_reached()); $this->assertEquals(false, $forum->has_cutoff_date()); $this->assertEquals(false, $forum->is_cutoff_date_reached()); } } PK-B\E* maildigest.phpnu[. /** * Set the mail digest option in a specific forum for a user. * * @copyright 2013 Andrew Nicols * @package mod_forum * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require(__DIR__.'/../../config.php'); require_once($CFG->dirroot.'/mod/forum/lib.php'); $id = required_param('id', PARAM_INT); $maildigest = required_param('maildigest', PARAM_INT); $backtoindex = optional_param('backtoindex', 0, PARAM_INT); // We must have a valid session key. require_sesskey(); $forum = $DB->get_record('forum', array('id' => $id)); $course = $DB->get_record('course', array('id' => $forum->course), '*', MUST_EXIST); $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id, false, MUST_EXIST); $context = context_module::instance($cm->id); require_login($course, false, $cm); $url = new moodle_url('/mod/forum/maildigest.php', array( 'id' => $id, 'maildigest' => $maildigest, )); $PAGE->set_url($url); $PAGE->set_context($context); $digestoptions = forum_get_user_digest_options(); $info = new stdClass(); $info->name = fullname($USER); $info->forum = format_string($forum->name); forum_set_user_maildigest($forum, $maildigest); $info->maildigest = $maildigest; if ($maildigest === -1) { // Get the default maildigest options. $info->maildigest = $USER->maildigest; $info->maildigesttitle = $digestoptions[$info->maildigest]; $info->maildigestdescription = get_string('emaildigest_' . $info->maildigest, 'mod_forum', $info); $updatemessage = get_string('emaildigestupdated_default', 'forum', $info); } else { $info->maildigesttitle = $digestoptions[$info->maildigest]; $info->maildigestdescription = get_string('emaildigest_' . $info->maildigest, 'mod_forum', $info); $updatemessage = get_string('emaildigestupdated', 'forum', $info); } if ($backtoindex) { $returnto = "index.php?id={$course->id}"; } else { $returnto = "view.php?f={$id}"; } redirect($returnto, $updatemessage, null, \core\output\notification::NOTIFY_SUCCESS); PK-B\kMIIdeprecatedlib.phpnu[. /** * @package mod_forum * @copyright 2014 Andrew Robert Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * This function prints the overview of a discussion in the forum listing. * It needs some discussion information and some post information, these * happen to be combined for efficiency in the $post parameter by the function * that calls this one: forum_print_latest_discussions() * * @deprecated since Moodle 4.3 */ function forum_print_discussion_header() { throw new \coding_exception('forum_print_discussion_header has been deprecated'); } PK-B\J J report/summary/renderer.phpnu[. /** * Provides rendering functionality for the forum summary report subplugin. * * @package forumreport_summary * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); use forumreport_summary\summary_table; /** * Renderer for the forum summary report. * * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class forumreport_summary_renderer extends plugin_renderer_base { /** * Render the filters available for the forum summary report. * * @param stdClass $course The course object. * @param array $cms Array of course module objects. * @param moodle_url $actionurl The form action URL. * @param array $filters Optional array of currently applied filter values. * @return string The filter form HTML. */ public function render_filters_form(stdClass $course, array $cms, moodle_url $actionurl, array $filters = []): string { $renderable = new \forumreport_summary\output\filters($course, $cms, $actionurl, $filters); $templatecontext = $renderable->export_for_template($this); return $this->render_from_template('forumreport_summary/filters', $templatecontext); } /** * Render the summary report table. * * @param summary_table $table The summary table to be rendered. * @return string The report table HTML. */ public function render_summary_table(summary_table $table): string { // Buffer so calling script can output the report as required. ob_start(); // Render table. $table->out($table->get_perpage(), false); $tablehtml = ob_get_contents(); ob_end_clean(); return $this->render_from_template('forumreport_summary/report', ['tablehtml' => $tablehtml]); } } PK-B\SSC  report/summary/db/access.phpnu[. /** * Forum summary report subplugin capabilities. * * @package forumreport_summary * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = [ 'forumreport/summary:view' => [ 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ], ], 'forumreport/summary:viewall' => [ 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ], ], ]; PK-B\fǢ9x x .report/summary/lang/en/forumreport_summary.phpnu[. /** * Strings for the Forum report subplugin. * * @package forumreport_summary * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['attachmentcount'] = 'Number of attachments'; $string['charcount'] = 'Character count'; $string['viewcount'] = 'Number of views'; $string['earliestpost'] = 'Earliest post'; $string['eventreportdownloaded'] = 'Forum summary report downloaded'; $string['eventreportviewed'] = 'Forum summary report viewed'; $string['filter:datesbuttonlabel'] = 'Open the dates filter'; $string['filter:datesname'] = 'Dates'; $string['filter:datesfrom'] = 'From {$a}'; $string['filter:datesfromto'] = '{$a->datefrom} - {$a->dateto}'; $string['filter:datesorderwarning'] = 'The from date can\'t be earlier than the to date.'; $string['filter:datesto'] = 'To {$a}'; $string['filter:groupsbuttonlabel'] = 'Open the groups filter'; $string['filter:groupsname'] = 'Groups'; $string['filter:groupscountall'] = 'Groups (all)'; $string['filter:groupscountnumber'] = 'Groups ({$a})'; $string['forumselectlabel'] = 'Forum selected'; $string['forumselectcourseoption'] = 'All forums in course'; $string['latestpost'] = 'Most recent post'; $string['exportposts'] = 'Export posts'; $string['exportpostslabel'] = 'Export posts for {$a}'; $string['nodetitle'] = 'Forum summary report'; $string['pluginname'] = 'Forum summary report'; $string['postcount'] = 'Number of discussions posted'; $string['privacy:metadata'] = 'The Forum summary report plugin does not store any personal data.'; $string['replycount'] = 'Number of replies posted'; $string['summary:viewall'] = 'Access summary report data for each user within a given forum or forums'; $string['summary:view'] = 'Access summary report within a given forum or forums'; $string['summarytitle'] = 'Forum summary report - {$a}'; $string['viewsdisclaimer'] = 'Number of views column is not filtered by group'; $string['wordcount'] = 'Word count'; PK-B\V.++report/summary/version.phpnu[. /** * Defines the version of the forum report subplugin. * * @package forumreport_summary * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->component = 'forumreport_summary'; PK-B\EE1report/summary/classes/form/dates_filter_form.phpnu[. /** * The mform used by the forum summary report dates filter. * * @package forumreport_summary * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace forumreport_summary\form; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/lib/formslib.php'); /** * The mform class for creating the forum summary report dates filter. * * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class dates_filter_form extends \moodleform { /** * The form definition. * */ public function definition() { $attributes = [ 'class' => 'align-items-center', ]; // From date field. $this->_form->addElement('date_selector', 'filterdatefrompopover', get_string('fromdate'), ['optional' => true], $attributes); // To date field. $this->_form->addElement('date_selector', 'filterdatetopopover', get_string('todate'), ['optional' => true], $attributes); } } PK-B\::+report/summary/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for forumreport_summary subplugin. * * @package forumreport_summary * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace forumreport_summary\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem implementation for forumreport_summary subplugin, implementing null_provider. * * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } PK-B\BEE(report/summary/classes/summary_table.phpnu[. /** * The class for displaying the forum report table. * * @package forumreport_summary * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace forumreport_summary; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/tablelib.php'); use coding_exception; use table_sql; /** * The class for displaying the forum report table. * * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class summary_table extends table_sql { /** Forum filter type */ const FILTER_FORUM = 1; /** Groups filter type */ const FILTER_GROUPS = 2; /** Dates filter type */ const FILTER_DATES = 3; /** Table to store summary data extracted from the log table */ const LOG_SUMMARY_TEMP_TABLE = 'forum_report_summary_counts'; /** Default number of rows to display per page */ const DEFAULT_PER_PAGE = 50; /** @var \stdClass The various SQL segments that will be combined to form queries to fetch various information. */ public $sql; /** @var int The number of rows to be displayed per page. */ protected $perpage = self::DEFAULT_PER_PAGE; /** @var array The values available for pagination size per page. */ protected $perpageoptions = [50, 100, 200]; /** @var int The course ID containing the forum(s) being reported on. */ protected $courseid; /** @var bool True if reporting on all forums in course user has access to, false if reporting on a single forum */ protected $iscoursereport = false; /** @var bool True if user has access to all forums in the course (and is running course report), otherwise false. */ protected $accessallforums = false; /** @var \stdClass The course module object(s) of the forum(s) being reported on. */ protected $cms = []; /** * @var int The user ID if only one user's summary will be generated. * This will apply to users without permission to view others' summaries. */ protected $userid; /** * @var \core\log\sql_reader|null */ protected $logreader = null; /** * @var array of \context objects for the forums included in the report. */ protected $forumcontexts = []; /** * @var context_course|context_module The context where the report is being run (either a specific forum or the course). */ protected $userfieldscontext = null; /** @var bool Whether the user has the capability/capabilities to perform bulk operations. */ protected $allowbulkoperations = false; /** * @var bool */ private $showwordcharcounts = null; /** * @var bool Whether the user can see all private replies or not. */ protected $canseeprivatereplies; /** * @var array Validated filter data, for use in GET parameters by export links. */ protected $exportfilterdata = []; /** * Forum report table constructor. * * @param int $courseid The ID of the course the forum(s) exist within. * @param array $filters Report filters in the format 'type' => [values]. * @param bool $allowbulkoperations Is the user allowed to perform bulk operations? * @param bool $canseeprivatereplies Whether the user can see all private replies or not. * @param int $perpage The number of rows to display per page. * @param bool $canexport Is the user allowed to export records? * @param bool $iscoursereport Whether the user is running a course level report * @param bool $accessallforums If user is running a course level report, do they have access to all forums in the course? */ public function __construct(int $courseid, array $filters, bool $allowbulkoperations, bool $canseeprivatereplies, int $perpage, bool $canexport, bool $iscoursereport, bool $accessallforums) { global $OUTPUT; $uniqueid = $courseid . ($iscoursereport ? '' : '_' . $filters['forums'][0]); parent::__construct("summaryreport_{$uniqueid}"); $this->courseid = $courseid; $this->iscoursereport = $iscoursereport; $this->accessallforums = $accessallforums; $this->allowbulkoperations = $allowbulkoperations; $this->canseeprivatereplies = $canseeprivatereplies; $this->perpage = $perpage; $this->set_forum_properties($filters['forums']); $columnheaders = []; if ($allowbulkoperations) { $mastercheckbox = new \core\output\checkbox_toggleall('summaryreport-table', true, [ 'id' => 'select-all-users', 'name' => 'select-all-users', 'label' => get_string('selectall'), 'labelclasses' => 'visually-hidden', 'classes' => 'm-1', 'checked' => false ]); $columnheaders['select'] = $OUTPUT->render($mastercheckbox); } $columnheaders += [ 'fullname' => get_string('fullnameuser'), 'postcount' => get_string('postcount', 'forumreport_summary'), 'replycount' => get_string('replycount', 'forumreport_summary'), 'attachmentcount' => get_string('attachmentcount', 'forumreport_summary'), ]; $this->logreader = $this->get_internal_log_reader(); if ($this->logreader) { $columnheaders['viewcount'] = get_string('viewcount', 'forumreport_summary'); } if ($this->show_word_char_counts()) { $columnheaders['wordcount'] = get_string('wordcount', 'forumreport_summary'); $columnheaders['charcount'] = get_string('charcount', 'forumreport_summary'); } $columnheaders['earliestpost'] = get_string('earliestpost', 'forumreport_summary'); $columnheaders['latestpost'] = get_string('latestpost', 'forumreport_summary'); if ($canexport) { $columnheaders['export'] = get_string('exportposts', 'forumreport_summary'); } $this->define_columns(array_keys($columnheaders)); $this->define_headers(array_values($columnheaders)); // Define configs. $this->define_table_configs(); // Apply relevant filters. $this->define_base_filter_sql(); $this->apply_filters($filters); // Define the basic SQL data and object format. $this->define_base_sql(); } /** * Sets properties that are determined by forum filter values. * * @param array $forumids The forum IDs passed in by the filter. * @return void */ protected function set_forum_properties(array $forumids): void { global $USER; // Course context if reporting on all forums in the course the user has access to. if ($this->iscoursereport) { $this->userfieldscontext = \context_course::instance($this->courseid); } foreach ($forumids as $forumid) { $cm = get_coursemodule_from_instance('forum', $forumid, $this->courseid); $this->cms[] = $cm; $this->forumcontexts[$cm->id] = \context_module::instance($cm->id); // Set forum context if not reporting on course. if (!isset($this->userfieldscontext)) { $this->userfieldscontext = $this->forumcontexts[$cm->id]; } // Only show own summary unless they have permission to view all in every forum being reported. if (empty($this->userid) && !has_capability('forumreport/summary:viewall', $this->forumcontexts[$cm->id])) { $this->userid = $USER->id; } } } /** * Provides the string name of each filter type, to be used by errors. * Note: This does not use language strings as the value is injected into error strings. * * @param int $filtertype Type of filter * @return string Name of the filter */ protected function get_filter_name(int $filtertype): string { $filternames = [ self::FILTER_FORUM => 'Forum', self::FILTER_GROUPS => 'Groups', self::FILTER_DATES => 'Dates', ]; return $filternames[$filtertype]; } /** * Generate the select column. * * @param \stdClass $data * @return string */ public function col_select($data) { global $OUTPUT; $checkbox = new \core\output\checkbox_toggleall('summaryreport-table', false, [ 'classes' => 'usercheckbox m-1', 'id' => 'user' . $data->userid, 'name' => 'user' . $data->userid, 'checked' => false, 'label' => get_string('selectitem', 'moodle', fullname($data)), 'labelclasses' => 'accesshide', ]); return $OUTPUT->render($checkbox); } /** * Generate the fullname column. * * @param \stdClass $data The row data. * @return string User's full name. */ public function col_fullname($data): string { if ($this->is_downloading()) { return fullname($data); } global $OUTPUT; return $OUTPUT->user_picture($data, array('courseid' => $this->courseid, 'includefullname' => true)); } /** * Generate the postcount column. * * @param \stdClass $data The row data. * @return int number of discussion posts made by user. */ public function col_postcount(\stdClass $data): int { return $data->postcount; } /** * Generate the replycount column. * * @param \stdClass $data The row data. * @return int number of replies made by user. */ public function col_replycount(\stdClass $data): int { return $data->replycount; } /** * Generate the attachmentcount column. * * @param \stdClass $data The row data. * @return int number of files attached to posts by user. */ public function col_attachmentcount(\stdClass $data): int { return $data->attachmentcount; } /** * Generate the earliestpost column. * * @param \stdClass $data The row data. * @return string Timestamp of user's earliest post, or a dash if no posts exist. */ public function col_earliestpost(\stdClass $data): string { global $USER; return empty($data->earliestpost) ? '-' : userdate($data->earliestpost, "", \core_date::get_user_timezone($USER)); } /** * Generate the latestpost column. * * @param \stdClass $data The row data. * @return string Timestamp of user's most recent post, or a dash if no posts exist. */ public function col_latestpost(\stdClass $data): string { global $USER; return empty($data->latestpost) ? '-' : userdate($data->latestpost, "", \core_date::get_user_timezone($USER)); } /** * Generate the export column. * * @param \stdClass $data The row data. * @return string The link to export content belonging to the row. */ public function col_export(\stdClass $data): string { global $OUTPUT; // If no posts, nothing to export. if (empty($data->earliestpost)) { return ''; } $params = [ 'id' => $this->cms[0]->instance, // Forum id. 'userids[]' => $data->userid, // User id. ]; // Add relevant filter params. foreach ($this->exportfilterdata as $name => $filterdata) { if (is_array($filterdata)) { foreach ($filterdata as $key => $value) { $params["{$name}[{$key}]"] = $value; } } else { $params[$name] = $filterdata; } } $buttoncontext = [ 'url' => new \moodle_url('/mod/forum/export.php', $params), 'label' => get_string('exportpostslabel', 'forumreport_summary', fullname($data)), ]; return $OUTPUT->render_from_template('forumreport_summary/export_link_button', $buttoncontext); } /** * Override the default implementation to set a notification. * * @return void. */ public function print_nothing_to_display(): void { global $OUTPUT; echo $OUTPUT->notification(get_string('nothingtodisplay'), 'info', false); } /** * Query the db. Store results in the table object for use by build_table. * * @param int $pagesize Size of page for paginated displayed table. * @param bool $useinitialsbar Overridden but unused. * @return void */ public function query_db($pagesize, $useinitialsbar = false): void { global $DB; // Set up pagination if not downloading the whole report. if (!$this->is_downloading()) { $totalsql = $this->get_full_sql(false); // Set up pagination. $totalrows = $DB->count_records_sql($totalsql, $this->sql->params); $this->pagesize($pagesize, $totalrows); } // Fetch the data. $sql = $this->get_full_sql(); // Only paginate when not downloading. if (!$this->is_downloading()) { $this->rawdata = $DB->get_records_sql($sql, $this->sql->params, $this->get_page_start(), $this->get_page_size()); } else { $this->rawdata = $DB->get_records_sql($sql, $this->sql->params); } } /** * Adds the relevant SQL to apply a filter to the report. * * @param int $filtertype Filter type as defined by class constants. * @param array $values Optional array of values passed into the filter type. * @return void * @throws coding_exception */ public function add_filter(int $filtertype, array $values = []): void { global $DB; $paramcounterror = false; switch($filtertype) { case self::FILTER_FORUM: // Requires at least one forum ID. if (empty($values)) { $paramcounterror = true; } else { // No select fields required - displayed in title. // No extra joins required, forum is already joined. list($forumidin, $forumidparams) = $DB->get_in_or_equal($values, SQL_PARAMS_NAMED); $this->sql->filterwhere .= " AND f.id {$forumidin}"; $this->sql->params += $forumidparams; } break; case self::FILTER_GROUPS: // Filter data to only include content within specified groups (and/or no groups). // Additionally, only display users who can post within the selected option(s). // Only filter by groups the user has access to. $groups = $this->get_filter_groups($values); // Skip adding filter if not applied, or all valid options are selected. if (!empty($groups)) { list($groupidin, $groupidparams) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED); // Posts within selected groups and/or not in any groups (group ID -1) are included. // No user filtering as anyone enrolled can potentially post to unrestricted discussions. if (array_search(-1, $groups) !== false) { $this->sql->filterwhere .= " AND d.groupid {$groupidin}"; $this->sql->params += $groupidparams; } else { // Only posts and users within selected groups are included. list($groupusersin, $groupusersparams) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED); // No joins required (handled by where to prevent data duplication). $this->sql->filterwhere .= " AND u.id IN ( SELECT gm.userid FROM {groups_members} gm WHERE gm.groupid {$groupusersin} ) AND d.groupid {$groupidin}"; $this->sql->params += $groupusersparams + $groupidparams; } } break; case self::FILTER_DATES: if (!isset($values['from']['enabled']) || !isset($values['to']['enabled']) || ($values['from']['enabled'] && !isset($values['from']['timestamp'])) || ($values['to']['enabled'] && !isset($values['to']['timestamp']))) { $paramcounterror = true; } else { $this->sql->filterbase['dates'] = ''; $this->sql->filterbase['dateslog'] = ''; $this->sql->filterbase['dateslogparams'] = []; // From date. if ($values['from']['enabled']) { // If the filter was enabled, include the date restriction. // Needs to form part of the base join to posts, so will be injected by define_base_sql(). $this->sql->filterbase['dates'] .= " AND p.created >= :fromdate"; $this->sql->params['fromdate'] = $values['from']['timestamp']; $this->sql->filterbase['dateslog'] .= ' AND timecreated >= :fromdate'; $this->sql->filterbase['dateslogparams']['fromdate'] = $values['from']['timestamp']; $this->exportfilterdata['timestampfrom'] = $values['from']['timestamp']; } // To date. if ($values['to']['enabled']) { // If the filter was enabled, include the date restriction. // Needs to form part of the base join to posts, so will be injected by define_base_sql(). $this->sql->filterbase['dates'] .= " AND p.created <= :todate"; $this->sql->params['todate'] = $values['to']['timestamp']; $this->sql->filterbase['dateslog'] .= ' AND timecreated <= :todate'; $this->sql->filterbase['dateslogparams']['todate'] = $values['to']['timestamp']; $this->exportfilterdata['timestampto'] = $values['to']['timestamp']; } } break; default: throw new coding_exception("Report filter type '{$filtertype}' not found."); break; } if ($paramcounterror) { $filtername = $this->get_filter_name($filtertype); throw new coding_exception("An invalid number of values have been passed for the '{$filtername}' filter."); } } /** * Define various table config options. * * @return void. */ protected function define_table_configs(): void { $this->collapsible(false); $this->sortable(true, 'firstname', SORT_ASC); $this->pageable(true); $this->is_downloadable(true); $this->no_sorting('select'); $this->no_sorting('export'); $this->set_attribute('id', 'forumreport_summary_table'); $this->sql = new \stdClass(); $this->sql->params = []; } /** * Define the object to store all for the table SQL and initialises the base SQL required. * * @return void. */ protected function define_base_sql(): void { global $USER; // TODO Does not support custom user profile fields (MDL-70456). $userfieldsapi = \core_user\fields::for_identity($this->userfieldscontext, false)->with_userpic(); $userfieldssql = $userfieldsapi->get_sql('u', false, '', '', false)->selects; // Define base SQL query format. $this->sql->basefields = ' u.id AS userid, d.course AS courseid, SUM(CASE WHEN p.parent = 0 THEN 1 ELSE 0 END) AS postcount, SUM(CASE WHEN p.parent != 0 THEN 1 ELSE 0 END) AS replycount, ' . $userfieldssql . ', SUM(CASE WHEN att.attcount IS NULL THEN 0 ELSE att.attcount END) AS attachmentcount, MIN(p.created) AS earliestpost, MAX(p.created) AS latestpost'; // Handle private replies. $privaterepliessql = ''; $privaterepliesparams = []; if (!$this->canseeprivatereplies) { $privaterepliessql = ' AND (p.privatereplyto = :privatereplyto OR p.userid = :privatereplyfrom OR p.privatereplyto = 0)'; $privaterepliesparams['privatereplyto'] = $USER->id; $privaterepliesparams['privatereplyfrom'] = $USER->id; } if ($this->iscoursereport) { $course = get_course($this->courseid); $groupmode = groups_get_course_groupmode($course); } else { $cm = \cm_info::create($this->cms[0]); $groupmode = $cm->effectivegroupmode; } if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $this->get_context())) { $groups = groups_get_all_groups($this->courseid, $USER->id, 0, 'g.id'); $groupids = array_column($groups, 'id'); } else { $groupids = []; } [$enrolleduserssql, $enrolledusersparams] = get_enrolled_sql($this->get_context(), '', $groupids); $this->sql->params += $enrolledusersparams; $queryattachments = 'SELECT COUNT(fi.id) AS attcount, fi.itemid AS postid, fi.userid FROM {files} fi WHERE fi.component = :component AND fi.filesize > 0 GROUP BY fi.itemid, fi.userid'; $this->sql->basefromjoins = ' {user} u JOIN (' . $enrolleduserssql . ') enrolledusers ON enrolledusers.id = u.id JOIN {forum} f ON f.course = :forumcourseid JOIN {forum_discussions} d ON d.forum = f.id LEFT JOIN {forum_posts} p ON p.discussion = d.id AND p.userid = u.id ' . $privaterepliessql . $this->sql->filterbase['dates'] . ' LEFT JOIN (' . $queryattachments . ') att ON att.postid = p.id AND att.userid = u.id'; $this->sql->basewhere = '1 = 1'; $this->sql->basegroupby = "$userfieldssql, d.course"; if ($this->logreader) { $this->fill_log_summary_temp_table(); $this->sql->basefields .= ', CASE WHEN tmp.viewcount IS NOT NULL THEN tmp.viewcount ELSE 0 END AS viewcount'; $this->sql->basefromjoins .= ' LEFT JOIN {' . self::LOG_SUMMARY_TEMP_TABLE . '} tmp ON tmp.userid = u.id '; $this->sql->basegroupby .= ', tmp.viewcount'; } if ($this->show_word_char_counts()) { // All p.wordcount values should be NOT NULL, this CASE WHEN is an extra just-in-case. $this->sql->basefields .= ', SUM(CASE WHEN p.wordcount IS NOT NULL THEN p.wordcount ELSE 0 END) AS wordcount'; $this->sql->basefields .= ', SUM(CASE WHEN p.charcount IS NOT NULL THEN p.charcount ELSE 0 END) AS charcount'; } $this->sql->params += [ 'component' => 'mod_forum', 'forumcourseid' => $this->courseid, ] + $privaterepliesparams; // Handle if a user is limited to viewing their own summary. if (!empty($this->userid)) { $this->sql->basewhere .= ' AND u.id = :userid'; $this->sql->params['userid'] = $this->userid; } } /** * Instantiate the properties to store filter values. * * @return void. */ protected function define_base_filter_sql(): void { // Filter values will be populated separately where required. $this->sql->filterfields = ''; $this->sql->filterfromjoins = ''; $this->sql->filterwhere = ''; $this->sql->filtergroupby = ''; } /** * Overriding the parent method because it should not be used here. * Filters are applied, so the structure of $this->sql is now different to the way this is set up in the parent. * * @param string $fields Unused. * @param string $from Unused. * @param string $where Unused. * @param array $params Unused. * @return void. * * @throws coding_exception */ public function set_sql($fields, $from, $where, array $params = []) { throw new coding_exception('The set_sql method should not be used by the summary_table class.'); } /** * Convenience method to call a number of methods for you to display the table. * Overrides the parent so SQL for filters is handled. * * @param int $pagesize Number of rows to fetch. * @param bool $useinitialsbar Whether to include the initials bar with the table. * @param string $downloadhelpbutton Unused. * * @return void. */ public function out($pagesize, $useinitialsbar, $downloadhelpbutton = ''): void { global $DB; if (!$this->columns) { $sql = $this->get_full_sql(); $onerow = $DB->get_record_sql($sql, $this->sql->params, IGNORE_MULTIPLE); // If columns is not set, define columns as the keys of the rows returned from the db. $this->define_columns(array_keys((array)$onerow)); $this->define_headers(array_keys((array)$onerow)); } $this->setup(); $this->query_db($pagesize, $useinitialsbar); $this->build_table(); $this->close_recordset(); $this->finish_output(); // Drop the temp table when necessary. if ($this->logreader) { $this->drop_log_summary_temp_table(); } } /** * Apply the relevant filters to the report. * * @param array $filters Report filters in the format 'type' => [values]. * @return void. */ protected function apply_filters(array $filters): void { // Apply the forums filter if not reporting on every forum in a course. if (!$this->accessallforums) { $this->add_filter(self::FILTER_FORUM, $filters['forums']); } // Apply groups filter. $this->add_filter(self::FILTER_GROUPS, $filters['groups']); // Apply dates filter. $datevalues = [ 'from' => $filters['datefrom'], 'to' => $filters['dateto'], ]; $this->add_filter(self::FILTER_DATES, $datevalues); } /** * Prepares a complete SQL statement from the base query and any filters defined. * * @param bool $fullselect Whether to select all relevant columns. * False selects a count only (used to calculate pagination). * @return string The complete SQL statement. */ protected function get_full_sql(bool $fullselect = true): string { $groupby = ''; $orderby = ''; if ($fullselect) { $selectfields = "{$this->sql->basefields} {$this->sql->filterfields}"; $groupby = ' GROUP BY ' . $this->sql->basegroupby . $this->sql->filtergroupby; if ($sort = $this->get_sql_sort()) { $orderby = " ORDER BY {$sort}"; } } else { $selectfields = 'COUNT(DISTINCT u.id)'; } $sql = "SELECT {$selectfields} FROM {$this->sql->basefromjoins} {$this->sql->filterfromjoins} WHERE {$this->sql->basewhere} {$this->sql->filterwhere} {$groupby} {$orderby}"; return $sql; } /** * Returns an internal and enabled log reader. * * @return \core\log\sql_reader|false */ protected function get_internal_log_reader(): ?\core\log\sql_reader { global $DB; $readers = get_log_manager()->get_readers('core\log\sql_reader'); foreach ($readers as $reader) { // If reader is not a sql_internal_table_reader and not legacy store then return. if (!($reader instanceof \core\log\sql_internal_table_reader)) { continue; } $logreader = $reader; } if (empty($logreader)) { return null; } return $logreader; } /** * Fills the log summary temp table. * * @return null */ protected function fill_log_summary_temp_table() { global $DB; $this->create_log_summary_temp_table(); $logtable = $this->logreader->get_internal_log_table_name(); $nonanonymous = 'AND anonymous = 0'; // Apply dates filter if applied. $datewhere = $this->sql->filterbase['dateslog'] ?? ''; $dateparams = $this->sql->filterbase['dateslogparams'] ?? []; $contextids = []; foreach ($this->forumcontexts as $forumcontext) { $contextids[] = $forumcontext->id; } list($contextidin, $contextidparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED); $params = $contextidparams + $dateparams; $sql = "INSERT INTO {" . self::LOG_SUMMARY_TEMP_TABLE . "} (userid, viewcount) SELECT userid, COUNT(*) AS viewcount FROM {" . $logtable . "} WHERE contextid {$contextidin} $datewhere $nonanonymous GROUP BY userid"; $DB->execute($sql, $params); } /** * Creates a temp table to store summary data from the log table for this request. * * @return null */ protected function create_log_summary_temp_table() { global $DB; $dbman = $DB->get_manager(); $temptablename = self::LOG_SUMMARY_TEMP_TABLE; $xmldbtable = new \xmldb_table($temptablename); $xmldbtable->add_field('userid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, null); $xmldbtable->add_field('viewcount', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, null); $xmldbtable->add_key('primary', XMLDB_KEY_PRIMARY, array('userid')); $dbman->create_temp_table($xmldbtable); } /** * Drops the temp table. * * This should be called once the processing for the summary table has been done. */ protected function drop_log_summary_temp_table(): void { global $DB; // Drop the temp table if it exists. $temptable = new \xmldb_table(self::LOG_SUMMARY_TEMP_TABLE); $dbman = $DB->get_manager(); if ($dbman->table_exists($temptable)) { $dbman->drop_table($temptable); } } /** * Get the final list of groups to filter by, based on the groups submitted, * and those the user has access to. * * * @param array $groups The group IDs submitted. * @return array Group objects of groups to use in groups filter. * If no filtering required (all groups selected), returns []. */ protected function get_filter_groups(array $groups): array { global $USER; $usergroups = groups_get_all_groups($this->courseid, $USER->id); $coursegroupsobj = groups_get_all_groups($this->courseid); $allgroups = false; $allowedgroupsobj = []; $allowedgroups = []; $filtergroups = []; foreach ($this->cms as $cm) { // Only need to check for all groups access if not confirmed by a previous check. if (!$allgroups) { $groupmode = groups_get_activity_groupmode($cm); // If no groups mode enabled on the forum, nothing to prepare. if (!in_array($groupmode, [VISIBLEGROUPS, SEPARATEGROUPS])) { continue; } $aag = has_capability('moodle/site:accessallgroups', $this->forumcontexts[$cm->id]); if ($groupmode == VISIBLEGROUPS || $aag) { $allgroups = true; // All groups in course fetched, no need to continue checking for others. break; } } } if ($allgroups) { $nogroups = new \stdClass(); $nogroups->id = -1; $nogroups->name = get_string('groupsnone'); // Any groups and no groups. $allowedgroupsobj = $coursegroupsobj + [$nogroups]; } else { $allowedgroupsobj = $usergroups; } foreach ($allowedgroupsobj as $group) { $allowedgroups[] = $group->id; } // If not all groups in course are selected, filter by allowed groups submitted. if (!empty($groups)) { if (!empty(array_diff($allowedgroups, $groups))) { $filtergroups = array_intersect($groups, $allowedgroups); } else { $coursegroups = []; foreach ($coursegroupsobj as $group) { $coursegroups[] = $group->id; } // If user's 'all groups' is a subset of the course groups, filter by all groups available to them. if (!empty(array_diff($coursegroups, $allowedgroups))) { $filtergroups = $allowedgroups; } } } return $filtergroups; } /** * Download the summary report in the selected format. * * @param string $format The format to download the report. */ public function download($format) { $filename = 'summary_report_' . userdate(time(), get_string('backupnameformat', 'langconfig'), 99, false); $this->is_downloading($format, $filename); $this->out($this->perpage, false); } /* * Should the word / char counts be displayed? * * We don't want to show word/char columns if there is any null value because this means * that they have not been calculated yet. * @return bool */ protected function show_word_char_counts(): bool { global $DB; if (is_null($this->showwordcharcounts)) { $forumids = []; foreach ($this->cms as $cm) { $forumids[] = $cm->instance; } list($forumidin, $forumidparams) = $DB->get_in_or_equal($forumids, SQL_PARAMS_NAMED); // This should be really fast. $sql = "SELECT 'x' FROM {forum_posts} fp JOIN {forum_discussions} fd ON fd.id = fp.discussion WHERE fd.forum {$forumidin} AND (fp.wordcount IS NULL OR fp.charcount IS NULL)"; if ($DB->record_exists_sql($sql, $forumidparams)) { $this->showwordcharcounts = false; } else { $this->showwordcharcounts = true; } } return $this->showwordcharcounts; } /** * Fetch the number of items to be displayed per page. * * @return int */ public function get_perpage(): int { return $this->perpage; } /** * Overriding method to render the bulk actions and items per page pagination options directly below the table. * * @return void */ public function wrap_html_finish(): void { global $OUTPUT; $data = new \stdClass(); $data->showbulkactions = $this->allowbulkoperations; if ($data->showbulkactions) { $data->id = 'formactionid'; $data->attributes = [ [ 'name' => 'data-action', 'value' => 'toggle' ], [ 'name' => 'data-togglegroup', 'value' => 'summaryreport-table' ], [ 'name' => 'data-toggle', 'value' => 'action' ], [ 'name' => 'disabled', 'value' => true ] ]; $data->actions = [ [ 'value' => '#messageselect', 'name' => get_string('messageselectadd') ] ]; } // Include the pagination size selector. $perpageoptions = array_combine($this->perpageoptions, $this->perpageoptions); $selected = in_array($this->perpage, $this->perpageoptions) ? $this->perpage : $this->perpageoptions[0]; $perpageselect = new \single_select(new \moodle_url(''), 'perpage', $perpageoptions, $selected, null, 'selectperpage'); $perpageselect->set_label(get_string('perpage', 'moodle')); $data->perpage = $perpageselect->export_for_template($OUTPUT); echo $OUTPUT->render_from_template('forumreport_summary/bulk_action_menu', $data); } } PK-B\L%k4343)report/summary/classes/output/filters.phpnu[. /** * Forum summary report filters renderable. * * @package forumreport_summary * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace forumreport_summary\output; use context_course; use moodle_url; use renderable; use renderer_base; use stdClass; use templatable; use forumreport_summary; /** * Forum summary report filters renderable. * * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class filters implements renderable, templatable { /** * Course modules the report relates to. * Array of stdClass objects * * @var array $cms */ protected $cms; /** * Course ID where the report is being generated. * * @var int $courseid */ protected $courseid; /** * Moodle URL used as the form action on the generate button. * * @var moodle_url $actionurl */ protected $actionurl; /** * Details of groups available for filtering. * Stored in the format groupid => groupname. * * @var array $groupsavailable */ protected $groupsavailable = []; /** * IDs of groups selected for filtering. * * @var array $groupsselected */ protected $groupsselected = []; /** * IDs of discussions required for export links. * If a subset of groups available are selected, this will include the discussion IDs * within that group in the forum. * If all groups are selected, or no groups mode is enabled, this will be empty as * no discussion filtering is required in the export. * * @var array $discussionids */ protected $discussionids = []; /** * HTML for dates filter. * * @var array $datesdata */ protected $datesdata = []; /** * Text to display on the dates filter button. * * @var string $datesbuttontext */ protected $datesbuttontext; /** * Builds renderable filter data. * * @param stdClass $course The course object. * @param array $cms Array of course module objects. * @param moodle_url $actionurl The form action URL. * @param array $filterdata (optional) Associative array of data that has been set on available filters, if any, * in the format filtertype => [values] */ public function __construct(stdClass $course, array $cms, moodle_url $actionurl, array $filterdata = []) { $this->cms = $cms; $this->courseid = $course->id; $this->actionurl = $actionurl; // Prepare groups filter data. $groupsdata = $filterdata['groups'] ?? []; $this->prepare_groups_data($groupsdata); // Prepare dates filter data. $datefromdata = $filterdata['datefrom'] ?? []; $datetodata = $filterdata['dateto'] ?? []; $this->prepare_dates_data($datefromdata, $datetodata); } /** * Prepares groups data and sets relevant property values. * * @param array $groupsdata Groups selected for filtering. * @return void. */ protected function prepare_groups_data(array $groupsdata): void { global $DB, $USER; $groupsavailable = []; $allowedgroupsobj = []; $usergroups = groups_get_all_groups($this->courseid, $USER->id); $coursegroups = groups_get_all_groups($this->courseid); $forumids = []; $allgroups = false; $hasgroups = false; // Check if any forum gives the user access to all groups and no groups. foreach ($this->cms as $cm) { $forumids[] = $cm->instance; // Only need to check for all groups access if not confirmed by a previous check. if (!$allgroups) { $groupmode = groups_get_activity_groupmode($cm); // If no groups mode enabled on the forum, nothing to prepare. if (!in_array($groupmode, [VISIBLEGROUPS, SEPARATEGROUPS])) { continue; } $hasgroups = true; // Fetch for the current cm's forum. $context = \context_module::instance($cm->id); $aag = has_capability('moodle/site:accessallgroups', $context); if ($groupmode == VISIBLEGROUPS || $aag) { $allgroups = true; } } } // If no groups mode enabled, nothing to prepare. if (!$hasgroups) { return; } // Any groups, and no groups. if ($allgroups) { $nogroups = new stdClass(); $nogroups->id = -1; $nogroups->name = get_string('groupsnone'); $allowedgroupsobj = $coursegroups + [$nogroups]; } else { $allowedgroupsobj = $usergroups; } $contextcourse = context_course::instance($this->courseid); foreach ($allowedgroupsobj as $group) { $groupsavailable[$group->id] = format_string($group->name, true, ['context' => $contextcourse]); } // Set valid groups selected. $groupsselected = array_intersect($groupsdata, array_keys($groupsavailable)); // Overwrite groups properties. $this->groupsavailable = $groupsavailable; $this->groupsselected = $groupsselected; $groupsselectedcount = count($groupsselected); if ($groupsselectedcount > 0 && $groupsselectedcount < count($groupsavailable)) { list($forumidin, $forumidparams) = $DB->get_in_or_equal($forumids, SQL_PARAMS_NAMED); list($groupidin, $groupidparams) = $DB->get_in_or_equal($groupsselected, SQL_PARAMS_NAMED); $discussionswhere = "course = :courseid AND forum {$forumidin} AND groupid {$groupidin}"; $discussionsparams = ['courseid' => $this->courseid]; $discussionsparams += $forumidparams + $groupidparams; $discussionids = $DB->get_fieldset_select('forum_discussions', 'DISTINCT id', $discussionswhere, $discussionsparams); foreach ($discussionids as $discussionid) { $this->discussionids[] = ['discid' => $discussionid]; } } } /** * Prepares from date, to date and button text. * Empty data will default to a disabled filter with today's date. * * @param array $datefromdata From date selected for filtering, and whether the filter is enabled. * @param array $datetodata To date selected for filtering, and whether the filter is enabled. * @return void. */ private function prepare_dates_data(array $datefromdata, array $datetodata): void { $timezone = \core_date::get_user_timezone_object(); $calendartype = \core_calendar\type_factory::get_calendar_instance(); $timestamptoday = time(); $datetoday = $calendartype->timestamp_to_date_array($timestamptoday, $timezone); // Prepare date/enabled data. if (empty($datefromdata['enabled'])) { $fromdate = $datetoday; $fromtimestamp = $timestamptoday; $fromenabled = false; } else { $fromdate = $calendartype->timestamp_to_date_array($datefromdata['timestamp'], $timezone); $fromtimestamp = $datefromdata['timestamp']; $fromenabled = true; } if (empty($datetodata['enabled'])) { $todate = $datetoday; $totimestamp = $timestamptoday; $toenabled = false; } else { $todate = $calendartype->timestamp_to_date_array($datetodata['timestamp'], $timezone); $totimestamp = $datetodata['timestamp']; $toenabled = true; } $this->datesdata = [ 'from' => [ 'day' => $fromdate['mday'], 'month' => $fromdate['mon'], 'year' => $fromdate['year'], 'timestamp' => $fromtimestamp, 'enabled' => $fromenabled, ], 'to' => [ 'day' => $todate['mday'], 'month' => $todate['mon'], 'year' => $todate['year'], 'timestamp' => $totimestamp, 'enabled' => $toenabled, ], ]; // Prepare button string data. $displayformat = get_string('strftimedatemonthabbr', 'langconfig'); $fromdatestring = $calendartype->timestamp_to_date_string($fromtimestamp, $displayformat, $timezone, true, true); $todatestring = $calendartype->timestamp_to_date_string($totimestamp, $displayformat, $timezone, true, true); if ($fromenabled && $toenabled) { $datestrings = [ 'datefrom' => $fromdatestring, 'dateto' => $todatestring, ]; $this->datesbuttontext = get_string('filter:datesfromto', 'forumreport_summary', $datestrings); } else if ($fromenabled) { $this->datesbuttontext = get_string('filter:datesfrom', 'forumreport_summary', $fromdatestring); } else if ($toenabled) { $this->datesbuttontext = get_string('filter:datesto', 'forumreport_summary', $todatestring); } else { $this->datesbuttontext = get_string('filter:datesname', 'forumreport_summary'); } } /** * Export data for use as the context of a mustache template. * * @param renderer_base $renderer The renderer to be used to display report filters. * @return array Data in a format compatible with a mustache template. */ public function export_for_template(renderer_base $renderer): stdClass { $output = new stdClass(); // Set formaction URL. $output->actionurl = $this->actionurl->out(false); // Set groups filter data. if (!empty($this->groupsavailable)) { $output->hasgroups = true; $groupscount = count($this->groupsselected); if (count($this->groupsavailable) <= $groupscount) { $output->filtergroupsname = get_string('filter:groupscountall', 'forumreport_summary'); } else if (!empty($this->groupsselected)) { $output->filtergroupsname = get_string('filter:groupscountnumber', 'forumreport_summary', $groupscount); } else { $output->filtergroupsname = get_string('filter:groupsname', 'forumreport_summary'); } // Set groups filter. $groupsdata = []; foreach ($this->groupsavailable as $groupid => $groupname) { $groupsdata[] = [ 'groupid' => $groupid, 'groupname' => $groupname, 'checked' => in_array($groupid, $this->groupsselected), ]; } $output->filtergroups = $groupsdata; } else { $output->hasgroups = false; } // Set discussion IDs for use by export links (always included, as it will be empty if not required). $output->discussionids = $this->discussionids; // Set date button and generate dates popover mform. $datesformdata = []; if ($this->datesdata['from']['enabled']) { $datesformdata['filterdatefrompopover'] = $this->datesdata['from']; } if ($this->datesdata['to']['enabled']) { $datesformdata['filterdatetopopover'] = $this->datesdata['to']; } $output->filterdatesname = $this->datesbuttontext; $datesform = new forumreport_summary\form\dates_filter_form(); $datesform->set_data($datesformdata); $output->filterdatesform = $datesform->render(); // Set dates filter data within filters form. $disableddate = [ 'day' => '', 'month' => '', 'year' => '', 'enabled' => '0', ]; $datefromdata = ['type' => 'from'] + ($this->datesdata['from']['enabled'] ? $this->datesdata['from'] : $disableddate); $datetodata = ['type' => 'to'] + ($this->datesdata['to']['enabled'] ? $this->datesdata['to'] : $disableddate); $output->filterdatesdata = [$datefromdata, $datetodata]; return $output; } } PK-B\_ 2report/summary/classes/event/report_downloaded.phpnu[. /** * The forum summary report downloaded event. * * @package forumreport_summary * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace forumreport_summary\event; defined('MOODLE_INTERNAL') || die(); /** * The forum summary report downloaded event class. * * @package forumreport_summary * @since Moodle 3.8 * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class report_downloaded extends \core\event\base { /** * Set basic properties for the event. */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_TEACHING; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventreportdownloaded', 'forumreport_summary'); } /** * Returns non-localised event description with ids for admin use only. * * @return string */ public function get_description() { $whose = $this->other['hasviewall'] ? 'the' : 'their own'; $description = "The user with id '{$this->userid}' downloaded {$whose} summary report for "; if ($this->other['forumid']) { $description .= "the forum with course module id '{$this->contextinstanceid}'."; } else { $description .= "the course with id '{$this->contextinstanceid}'."; } return $description; } /** * Returns relevant URL. * @return \moodle_url */ public function get_url() { $params = ['courseid' => $this->courseid]; if (!empty($this->other['forumid'])) { $params['forumid'] = $this->other['forumid']; } return new \moodle_url('/mod/forum/report/summary/index.php', $params); } /** * Custom validation. * * @throws \coding_exception */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['hasviewall'])) { throw new \coding_exception('The \'hasviewall\' value must be set'); } } } PK-B\e{Ճ .report/summary/classes/event/report_viewed.phpnu[. /** * The forum summary report viewed event. * * @package forumreport_summary * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace forumreport_summary\event; defined('MOODLE_INTERNAL') || die(); /** * The forum summary report viewed event class. * * @package forumreport_summary * @since Moodle 3.8 * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class report_viewed extends \core\event\base { /** * Set basic properties for the event. */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_TEACHING; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventreportviewed', 'forumreport_summary'); } /** * Returns non-localised event description with ids for admin use only. * * @return string */ public function get_description() { $whose = $this->other['hasviewall'] ? 'the' : 'their own'; $description = "The user with id '{$this->userid}' viewed {$whose} summary report for "; if ($this->other['forumid']) { $description .= "the forum with course module id '{$this->contextinstanceid}'."; } else { $description .= "the course with id '{$this->contextinstanceid}'."; } return $description; } /** * Returns relevant URL. * * @return \moodle_url */ public function get_url() { $params = ['courseid' => $this->courseid]; if (!empty($this->other['forumid'])) { $params['forumid'] = $this->other['forumid']; } return new \moodle_url('/mod/forum/report/summary/index.php', $params); } /** * Custom validation. * * @throws \coding_exception */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['hasviewall'])) { throw new \coding_exception('The \'hasviewall\' value must be set'); } } } PK-B\ p  2report/summary/templates/bulk_action_menu.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template forumreport_summary/bulk_action_menu Summary report filters. Example context (json): { "showbulkactions": true, "id": "formactionid", "attributes": [ { "name": "data-action", "value": "toggle" }, { "name": "data-togglegroup", "value": "summaryreport-table" }, { "name": "data-toggle", "value": "action" } ], "actions": [ { "name": "Choose...", "value": "" }, { "name": "Send a message", "value": "#messageselect" } ], "perpage": { "name": "perpage", "method": "get", "action": "#", "classes": "singleselect", "label": "Per page", "formid": "randomid", "id": "selectperpage", "options":[ {"value": 50, "name": "50", "selected": true}, {"value": 100, "name": "100", "selected":false}, {"value": 200, "name": "200", "selected": false} ] } } }}
{{#showbulkactions}}
{{/showbulkactions}}
{{#perpage}} {{> core/single_select}} {{/perpage}}
{{#js}} {{#id}} require(['jquery', 'core_message/message_send_bulk', 'core/custom_interaction_events'], function($, BulkSender, CustomEvents) { CustomEvents.define('#{{id}}', [CustomEvents.events.accessibleChange]); $('#{{id}}').on(CustomEvents.events.accessibleChange, function(e) { var action = $(e.target).val(); if (action.indexOf('#') !== -1) { e.preventDefault(); var ids = $('#summaryreport input.usercheckbox:checked').map(function(index, element) { return element.name.replace('user', ''); }).get(); if (action == '#messageselect') { BulkSender.showModal(ids, function() { $('#{{id}}').focus(); }); } $('#{{id}} option[value=""]').prop('selected', 'selected'); } }); }); {{/id}} {{/js}} PK-B\U??6report/summary/templates/filter_dates_popover.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template forumreport_summary/filter_dates_popover Summary report dates filter. Example context (json): { "filterdatesform": "
HTML for date filters
" } }} PK-B\_XHH4report/summary/templates/export_link_button.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template forumreport_summary/export_link_button Summary report filters. Example context (json): { "url": "wwwroot/moodle/mod/forum/export.php?userids[]=12", "label": "Export posts for Joey Jojo" } }} PK-B\SY Y /report/summary/templates/filter_groups.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template forumreport_summary/filter_groups Summary report groups filter. Example context (json): { "hasgroups": true, "filtergroupsname" : "Groups (all)", "filtergroups": [ { "groupid": "1", "groupname": "Group A", "checked": true }, { "groupid": "3", "groupname": "Group C", "checked": false } ] } }} {{#hasgroups}} {{! Groups filter values to submit }} {{! Groups filter popover }} {{/hasgroups}} PK-B\ghx )report/summary/templates/filters.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template forumreport_summary/filters Summary report filters. Example context (json): { "actionurl": "https://mymoodlesite.com/mod/forum/report/summary/index.php?courseid=2&forumid=2&perpage=50", "hasgroups": true, "filtergroupsname" : "Groups (all)", "filtergroups": [ { "groupid": "1", "groupname": "Group A", "checked": true }, { "groupid": "3", "groupname": "Group C", "checked": false } ], "filterdatesname": "Dates", "filterdatesform": "
HTML for date filters
", "filterdatesdata": [ { "type": "from", "timestamp": "510969600", "enabled": "1" }, { "type": "to", "timestamp": "725673600", "enabled": "1" } ], "discussionids": [ { "discid": 123 }, { "discid": 456 } ] } }}
{{> forumreport_summary/filter_dates}} {{> forumreport_summary/filter_groups}}
{{#discussionids}} {{/discussionids}}
{{! Dates filter popover - mform must exist outside of the filtersform }} {{> forumreport_summary/filter_dates_popover}}
{{#js}} require(['forumreport_summary/filters'], function(Filters) { Filters.init(document.querySelector("[data-report-id='{{uniqid}}']")); }); {{/js}} PK-B\rH(report/summary/templates/report.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template forumreport_summary/report Summary report filters. Example context (json): { "tablehtml": "TableSQL format HTML for the table" } }}
{{> core/loading }}
PK-B\6v~~.report/summary/templates/filter_dates.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template forumreport_summary/filter_dates Summary report dates filter. Example context (json): { "filterdatesname": "Dates", "filterdatesdata": [ { "type": "from", "timestamp": "1571377510", "enabled": "1" }, { "type": "to", "timestamp": "1571377510", "enabled": "1" } ] } }} {{! Hidden dates fields to populate from visible mform in popover. }} {{#filterdatesdata}} {{/filterdatesdata}} PK-B\\Greport/summary/index.phpnu[. /** * This script displays the forum summary report for the given parameters, within a user's capabilities. * * @package forumreport_summary * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once("../../../../config.php"); if (isguestuser()) { throw new \moodle_exception('noguest'); } $courseid = required_param('courseid', PARAM_INT); $forumid = optional_param('forumid', 0, PARAM_INT); $perpage = optional_param('perpage', \forumreport_summary\summary_table::DEFAULT_PER_PAGE, PARAM_INT); $download = optional_param('download', '', PARAM_ALPHA); $filters = []; $pageurlparams = [ 'courseid' => $courseid, 'perpage' => $perpage, ]; // Establish filter values. $filters['groups'] = optional_param_array('filtergroups', [], PARAM_INT); $filters['datefrom'] = optional_param_array('datefrom', ['enabled' => 0], PARAM_INT); $filters['dateto'] = optional_param_array('dateto', ['enabled' => 0], PARAM_INT); $modinfo = get_fast_modinfo($courseid); $course = $modinfo->get_course(); $courseforums = $modinfo->instances['forum']; $cms = []; // Determine which forums the user has access to in the course. $accessallforums = false; $allforumidsincourse = array_keys($courseforums); $forumsvisibletouser = []; $forumselectoptions = [0 => get_string('forumselectcourseoption', 'forumreport_summary')]; foreach ($courseforums as $courseforumid => $courseforum) { if ($courseforum->uservisible) { $forumsvisibletouser[$courseforumid] = $courseforum; $forumselectoptions[$courseforumid] = $courseforum->get_formatted_name(); } } if ($forumid) { if (!isset($forumsvisibletouser[$forumid])) { throw new \moodle_exception('A valid forum ID is required to generate a summary report.'); } $filters['forums'] = [$forumid]; $title = $forumsvisibletouser[$forumid]->get_formatted_name(); $forumcm = $forumsvisibletouser[$forumid]; $cms[] = $forumcm; require_login($courseid, false, $forumcm); $context = $forumcm->context; $canexport = !$download && has_capability('mod/forum:exportforum', $context); $redirecturl = new moodle_url('/mod/forum/view.php', ['id' => $forumid]); $numforums = 1; $pageurlparams['forumid'] = $forumid; $iscoursereport = false; } else { // Course level report. require_login($courseid, false); $filters['forums'] = array_keys($forumsvisibletouser); // Fetch the forum CMs for the course. foreach ($forumsvisibletouser as $visibleforum) { $cms[] = $visibleforum; } $context = \context_course::instance($courseid); $title = $course->fullname; // Export currently only supports single forum exports. $canexport = false; $redirecturl = new moodle_url('/course/view.php', ['id' => $courseid]); $numforums = count($forumsvisibletouser); $iscoursereport = true; // Specify whether user has access to all forums in the course. $accessallforums = empty(array_diff($allforumidsincourse, $filters['forums'])); } $pageurl = new moodle_url('/mod/forum/report/summary/index.php', $pageurlparams); $PAGE->set_url($pageurl); $PAGE->set_pagelayout('report'); $PAGE->set_title($title); $PAGE->set_heading($course->fullname); $PAGE->activityheader->disable(); $PAGE->navbar->add(get_string('nodetitle', 'forumreport_summary')); // Activate the secondary nav tab. navigation_node::override_active_url(new moodle_url('/mod/forum/report/summary/index.php', ['courseid' => $courseid, 'forumid' => $forumid])); $allowbulkoperations = !$download && !empty($CFG->messaging) && has_capability('moodle/course:bulkmessaging', $context); $canseeprivatereplies = false; $hasviewall = false; $privatereplycapcount = 0; $viewallcount = 0; $canview = false; foreach ($cms as $cm) { $forumcontext = $cm->context; // This capability is required in at least one of the given contexts to view any version of the report. if (has_capability('forumreport/summary:view', $forumcontext)) { $canview = true; } if (has_capability('mod/forum:readprivatereplies', $forumcontext)) { $privatereplycapcount++; } if (has_capability('forumreport/summary:viewall', $forumcontext)) { $viewallcount++; } } if (!$canview) { redirect($redirecturl); } // Only use private replies if user has that cap in all forums in the report. if ($numforums === $privatereplycapcount) { $canseeprivatereplies = true; } // Will only show all users if user has the cap for all forums in the report. if ($numforums === $viewallcount) { $hasviewall = true; } // Prepare and display the report. $table = new \forumreport_summary\summary_table($courseid, $filters, $allowbulkoperations, $canseeprivatereplies, $perpage, $canexport, $iscoursereport, $accessallforums); $table->baseurl = $pageurl; $eventparams = [ 'context' => $context, 'other' => [ 'forumid' => $forumid, 'hasviewall' => $hasviewall, ], ]; if ($download) { \forumreport_summary\event\report_downloaded::create($eventparams)->trigger(); $table->download($download); } else { \forumreport_summary\event\report_viewed::create($eventparams)->trigger(); echo $OUTPUT->header(); if (!empty($filters['groups'])) { \core\notification::info(get_string('viewsdisclaimer', 'forumreport_summary')); } // Allow switching to course report (or other forum user has access to). $reporturl = new moodle_url('/mod/forum/report/summary/index.php', ['courseid' => $courseid]); $forumselect = new single_select($reporturl, 'forumid', $forumselectoptions, $forumid, ''); $forumselect->set_label(get_string('forumselectlabel', 'forumreport_summary')); echo $OUTPUT->render($forumselect); echo $OUTPUT->heading(get_string('nodetitle', 'forumreport_summary'), 2, 'pb-5 mt-3'); // Render the report filters form. $renderer = $PAGE->get_renderer('forumreport_summary'); unset($filters['forums']); echo $renderer->render_filters_form($course, $cms, $pageurl, $filters); $table->show_download_buttons_at(array(TABLE_P_BOTTOM)); echo $renderer->render_summary_table($table); echo $OUTPUT->footer(); } PK-B\&1??!report/summary/amd/src/filters.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Module responsible for handling forum summary report filters. * * @module forumreport_summary/filters * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import $ from 'jquery'; import {createPopper} from 'core/popper2'; import CustomEvents from 'core/custom_interaction_events'; import Selectors from 'forumreport_summary/selectors'; import Ajax from 'core/ajax'; import KeyCodes from 'core/key_codes'; import * as FormChangeChecker from 'core_form/changechecker'; export const init = (root) => { let jqRoot = $(root); // Hide loading spinner and show report once page is ready. // This ensures filters can be applied when sorting by columns. $(document).ready(function() { $('.loading-icon').hide(); $('#summaryreport').removeClass('hidden'); }); // Generic filter handlers. // Called to override click event to trigger a proper generate request with filtering. const generateWithFilters = (event, getparams) => { let currentLink = document.forms.filtersform.action, newLink; if (event) { event.preventDefault(); let currentSplit = currentLink.split('?'), currentstring = currentSplit[1], newparamsarray = getparams.split('&'), paramsstring = '', paramkeys = [], paramvalues = []; // Separate out the existing action GET param string. currentstring.split('&').forEach(function(param) { let splitparam = param.split('='); paramkeys.push(splitparam[0]); paramvalues.push(splitparam[1]); }); newparamsarray.forEach(function(paramstring) { let newparam = paramstring.split('='), existingkey = paramkeys.indexOf(newparam[0]); // Overwrite value if existing, otherwise add new param. if (existingkey > -1) { paramvalues[existingkey] = newparam[1]; } else { paramkeys.push(newparam[0]); paramvalues.push(newparam[1]); } }); // Build URL. paramkeys.forEach(function(name, key) { paramsstring += `&${name}=${paramvalues[key]}`; }); newLink = currentSplit[0] + '?' + paramsstring.substr(1); } else { newLink = currentLink; } document.forms.filtersform.action = newLink; document.forms.filtersform.submit(); }; // Override 'reset table preferences' so it generates with filters. $('.resettable').on("click", "a", function(event) { generateWithFilters(event, event.target.search.substr(1)); }); // Override table heading sort links so they generate with filters. $('thead').on("click", "a", function(event) { generateWithFilters(event, event.target.search.substr(1)); }); // Override pagination page links so they generate with filters. $('.pagination').on("click", "a", function(event) { generateWithFilters(event, event.target.search.substr(1)); }); // Override rows per page submission so it generates with filters. if (document.forms.selectperpage) { document.forms.selectperpage.onsubmit = (event) => { let getparam = 'perpage=' + document.forms.selectperpage.elements.perpage.value; generateWithFilters(event, getparam); }; } // Override download link so the file is generated with filters. const downloadForm = document.getElementById('summaryreport').querySelector('form.dataformatselector'); if (downloadForm) { downloadForm.onsubmit = (event) => { const downloadType = downloadForm.querySelector('#downloadtype_download').value; const getParams = `download=${downloadType}`; const prevAction = document.forms.filtersform.action; generateWithFilters(event, getParams); // Revert action, so re-submitting the form via filter does not trigger a further download. document.forms.filtersform.action = prevAction; }; } // Submit report via filter const submitWithFilter = (containerelement) => { // Disable the dates filter mform checker to prevent any changes triggering a warning to the user. FormChangeChecker.unWatchForm(document.querySelector(containerelement).querySelector('form')); // Close the container (eg popover). $(containerelement).addClass('hidden'); // Submit the filter values and re-generate report. generateWithFilters(false); }; // Use popper to override date mform calendar position. const updateCalendarPosition = (referenceid) => { let referenceElement = document.querySelector(referenceid), popperContent = document.querySelector(Selectors.filters.date.calendar); popperContent.style.removeProperty("z-index"); createPopper(referenceElement, popperContent, {placement: 'bottom-end'}); }; // Close the relevant filter. const closeOpenFilters = (openFilterButton, openFilter) => { openFilter.classList.add('hidden'); openFilter.setAttribute('data-openfilter', 'false'); openFilterButton.classList.add('btn-primary'); openFilterButton.classList.remove('btn-outline-primary'); openFilterButton.setAttribute('aria-expanded', false); }; // Groups filter specific handlers. // Event handler for clicking select all groups. jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.selectall, function() { let deselected = root.querySelectorAll(Selectors.filters.group.checkbox + ':not(:checked)'); deselected.forEach(function(checkbox) { checkbox.checked = true; }); }); // Event handler for clearing filter by clicking option. jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.clear, function() { // Clear checkboxes. let selected = root.querySelectorAll(Selectors.filters.group.checkbox + ':checked'); selected.forEach(function(checkbox) { checkbox.checked = false; }); }); // Event handler for showing groups filter popover. jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.trigger, function() { // Create popover. let referenceElement = root.querySelector(Selectors.filters.group.trigger), popperContent = root.querySelector(Selectors.filters.group.popover); createPopper(referenceElement, popperContent, {placement: 'bottom-end'}); // Show popover. popperContent.classList.remove('hidden'); popperContent.setAttribute('data-openfilter', 'true'); // Change to outlined button. referenceElement.classList.add('btn-outline-primary'); referenceElement.classList.remove('btn-primary'); // Let screen readers know that it's now expanded. referenceElement.setAttribute('aria-expanded', true); // Add listeners to handle closing filter. const closeListener = e => { if (e.target.id !== referenceElement.id && popperContent !== e.target.closest('[data-openfilter="true"]') && (typeof e.keyCode === 'undefined' || e.keyCode === KeyCodes.enter || e.keyCode === KeyCodes.space)) { closeOpenFilters(referenceElement, popperContent); document.removeEventListener('click', closeListener); document.removeEventListener('keyup', closeListener); document.removeEventListener('keyup', escCloseListener); } }; document.addEventListener('click', closeListener); document.addEventListener('keyup', closeListener); const escCloseListener = e => { if (e.keyCode === KeyCodes.escape) { closeOpenFilters(referenceElement, popperContent); document.removeEventListener('keyup', escCloseListener); document.removeEventListener('click', closeListener); } }; document.addEventListener('keyup', escCloseListener); }); // Event handler to click save groups filter. jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.save, function() { // Copy the saved values into the form before submitting. let popcheckboxes = root.querySelectorAll(Selectors.filters.group.checkbox); popcheckboxes.forEach(function(popcheckbox) { let filtersform = document.forms.filtersform, saveid = popcheckbox.getAttribute('data-saveid'); filtersform.querySelector(`#${saveid}`).checked = popcheckbox.checked; }); submitWithFilter('#filter-groups-popover'); }); // Listeners for export buttons. // These allow fetching of the relevant export URL, before submitting the request with // any POST data that is common to all of the export links. This allows filters to be // applied that contain potentially a lot of data (eg discussion IDs for groups filtering). document.querySelectorAll(Selectors.filters.exportlink.link).forEach(function(exportbutton) { exportbutton.addEventListener('click', function(event) { document.forms.exportlinkform.action = event.target.dataset.url; document.forms.exportlinkform.submit(); }); }); // Dates filter specific handlers. // Event handler for showing dates filter popover. jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.trigger, function() { // Create popover. let referenceElement = root.querySelector(Selectors.filters.date.trigger), popperContent = root.querySelector(Selectors.filters.date.popover); createPopper(referenceElement, popperContent, {placement: 'bottom-end'}); // Show popover and move focus. popperContent.classList.remove('hidden'); popperContent.setAttribute('data-openfilter', 'true'); popperContent.querySelector('[name="filterdatefrompopover[enabled]"]').focus(); // Change to outlined button. referenceElement.classList.add('btn-outline-primary'); referenceElement.classList.remove('btn-primary'); // Let screen readers know that it's now expanded. referenceElement.setAttribute('aria-expanded', true); // Add listener to handle closing filter. const closeListener = e => { if (e.target.id !== referenceElement.id && popperContent !== e.target.closest('[data-openfilter="true"]') && (typeof e.keyCode === 'undefined' || e.keyCode === KeyCodes.enter || e.keyCode === KeyCodes.space)) { closeOpenFilters(referenceElement, popperContent); document.removeEventListener('click', closeListener); document.removeEventListener('keyup', closeListener); document.removeEventListener('keyup', escCloseListener); } }; document.addEventListener('click', closeListener); document.addEventListener('keyup', closeListener); const escCloseListener = e => { if (e.keyCode === KeyCodes.escape) { closeOpenFilters(referenceElement, popperContent); document.removeEventListener('keyup', escCloseListener); document.removeEventListener('click', closeListener); } }; document.addEventListener('keyup', escCloseListener); }); // Event handler to save dates filter. jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.save, function() { // Populate the hidden form inputs to submit the data. let filtersForm = document.forms.filtersform; const datesPopover = root.querySelector(Selectors.filters.date.popover); const fromEnabled = datesPopover.querySelector('[name="filterdatefrompopover[enabled]"]').checked ? 1 : 0; const toEnabled = datesPopover.querySelector('[name="filterdatetopopover[enabled]"]').checked ? 1 : 0; if (!fromEnabled && !toEnabled) { // Update the elements in the filter form. filtersForm.elements['datefrom[timestamp]'].value = 0; filtersForm.elements['datefrom[enabled]'].value = fromEnabled; filtersForm.elements['dateto[timestamp]'].value = 0; filtersForm.elements['dateto[enabled]'].value = toEnabled; // Submit the filter values and re-generate report. submitWithFilter('#filter-dates-popover'); } else { let args = {data: []}; if (fromEnabled) { args.data.push({ 'key': 'from', 'year': datesPopover.querySelector('[name="filterdatefrompopover[year]"]').value, 'month': datesPopover.querySelector('[name="filterdatefrompopover[month]"]').value, 'day': datesPopover.querySelector('[name="filterdatefrompopover[day]"]').value, 'hour': 0, 'minute': 0 }); } if (toEnabled) { args.data.push({ 'key': 'to', 'year': datesPopover.querySelector('[name="filterdatetopopover[year]"]').value, 'month': datesPopover.querySelector('[name="filterdatetopopover[month]"]').value, 'day': datesPopover.querySelector('[name="filterdatetopopover[day]"]').value, 'hour': 23, 'minute': 59 }); } const request = { methodname: 'core_calendar_get_timestamps', args: args }; Ajax.call([request])[0].done(function(result) { let fromTimestamp = 0, toTimestamp = 0; result.timestamps.forEach(function(data) { if (data.key === 'from') { fromTimestamp = data.timestamp; } else if (data.key === 'to') { toTimestamp = data.timestamp; } }); // Display an error if the from date is later than the do date. if (toTimestamp > 0 && fromTimestamp > toTimestamp) { const warningdiv = document.getElementById('dates-filter-warning'); warningdiv.classList.remove('hidden'); warningdiv.classList.add('d-block'); } else { filtersForm.elements['datefrom[timestamp]'].value = fromTimestamp; filtersForm.elements['datefrom[enabled]'].value = fromEnabled; filtersForm.elements['dateto[timestamp]'].value = toTimestamp; filtersForm.elements['dateto[enabled]'].value = toEnabled; // Submit the filter values and re-generate report. submitWithFilter('#filter-dates-popover'); } }); } }); jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.calendariconfrom, function() { updateCalendarPosition(Selectors.filters.date.calendariconfrom); }); jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.calendariconto, function() { updateCalendarPosition(Selectors.filters.date.calendariconto); }); }; PK-B\*%%#report/summary/amd/src/selectors.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Module containing the selectors for the forum summary report. * * @module forumreport_summary/selectors * @copyright 2019 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ export default { filters: { group: { checkbox: '[data-region="filter-groups"] input[type="checkbox"]', clear: '[data-region="filter-groups"] .filter-clear', popover: '#filter-groups-popover', save: '[data-region="filter-groups"] .filter-save', selectall: '[data-region="filter-groups"] .select-all', trigger: '#filter-groups-button', }, date: { calendar: '#dateselector-calendar-panel', calendariconfrom: '#id_filterdatefrompopover_calendar', calendariconto: '#id_filterdatetopopover_calendar', popover: '#filter-dates-popover', save: '[data-region="filter-dates"] .filter-save', trigger: '#filter-dates-button', }, exportlink: { link: '#summaryreport #forumreport_summary_table button.export-link' } } }; PK-B\lG<,,'report/summary/amd/build/filters.min.jsnu[define("forumreport_summary/filters",["exports","jquery","core/popper2","core/custom_interaction_events","forumreport_summary/selectors","core/ajax","core/key_codes","core_form/changechecker"],(function(_exports,_jquery,_popper,_custom_interaction_events,_selectors,_ajax,_key_codes,FormChangeChecker){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Module responsible for handling forum summary report filters. * * @module forumreport_summary/filters * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=_interopRequireDefault(_jquery),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),_selectors=_interopRequireDefault(_selectors),_ajax=_interopRequireDefault(_ajax),_key_codes=_interopRequireDefault(_key_codes),FormChangeChecker=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(FormChangeChecker);_exports.init=root=>{let jqRoot=(0,_jquery.default)(root);(0,_jquery.default)(document).ready((function(){(0,_jquery.default)(".loading-icon").hide(),(0,_jquery.default)("#summaryreport").removeClass("hidden")}));const generateWithFilters=(event,getparams)=>{let newLink,currentLink=document.forms.filtersform.action;if(event){event.preventDefault();let currentSplit=currentLink.split("?"),currentstring=currentSplit[1],newparamsarray=getparams.split("&"),paramsstring="",paramkeys=[],paramvalues=[];currentstring.split("&").forEach((function(param){let splitparam=param.split("=");paramkeys.push(splitparam[0]),paramvalues.push(splitparam[1])})),newparamsarray.forEach((function(paramstring){let newparam=paramstring.split("="),existingkey=paramkeys.indexOf(newparam[0]);existingkey>-1?paramvalues[existingkey]=newparam[1]:(paramkeys.push(newparam[0]),paramvalues.push(newparam[1]))})),paramkeys.forEach((function(name,key){paramsstring+="&".concat(name,"=").concat(paramvalues[key])})),newLink=currentSplit[0]+"?"+paramsstring.substr(1)}else newLink=currentLink;document.forms.filtersform.action=newLink,document.forms.filtersform.submit()};(0,_jquery.default)(".resettable").on("click","a",(function(event){generateWithFilters(event,event.target.search.substr(1))})),(0,_jquery.default)("thead").on("click","a",(function(event){generateWithFilters(event,event.target.search.substr(1))})),(0,_jquery.default)(".pagination").on("click","a",(function(event){generateWithFilters(event,event.target.search.substr(1))})),document.forms.selectperpage&&(document.forms.selectperpage.onsubmit=event=>{let getparam="perpage="+document.forms.selectperpage.elements.perpage.value;generateWithFilters(event,getparam)});const downloadForm=document.getElementById("summaryreport").querySelector("form.dataformatselector");downloadForm&&(downloadForm.onsubmit=event=>{const downloadType=downloadForm.querySelector("#downloadtype_download").value,getParams="download=".concat(downloadType),prevAction=document.forms.filtersform.action;generateWithFilters(event,getParams),document.forms.filtersform.action=prevAction});const submitWithFilter=containerelement=>{FormChangeChecker.unWatchForm(document.querySelector(containerelement).querySelector("form")),(0,_jquery.default)(containerelement).addClass("hidden"),generateWithFilters(!1)},updateCalendarPosition=referenceid=>{let referenceElement=document.querySelector(referenceid),popperContent=document.querySelector(_selectors.default.filters.date.calendar);popperContent.style.removeProperty("z-index"),(0,_popper.createPopper)(referenceElement,popperContent,{placement:"bottom-end"})},closeOpenFilters=(openFilterButton,openFilter)=>{openFilter.classList.add("hidden"),openFilter.setAttribute("data-openfilter","false"),openFilterButton.classList.add("btn-primary"),openFilterButton.classList.remove("btn-outline-primary"),openFilterButton.setAttribute("aria-expanded",!1)};jqRoot.on(_custom_interaction_events.default.events.activate,_selectors.default.filters.group.selectall,(function(){root.querySelectorAll(_selectors.default.filters.group.checkbox+":not(:checked)").forEach((function(checkbox){checkbox.checked=!0}))})),jqRoot.on(_custom_interaction_events.default.events.activate,_selectors.default.filters.group.clear,(function(){root.querySelectorAll(_selectors.default.filters.group.checkbox+":checked").forEach((function(checkbox){checkbox.checked=!1}))})),jqRoot.on(_custom_interaction_events.default.events.activate,_selectors.default.filters.group.trigger,(function(){let referenceElement=root.querySelector(_selectors.default.filters.group.trigger),popperContent=root.querySelector(_selectors.default.filters.group.popover);(0,_popper.createPopper)(referenceElement,popperContent,{placement:"bottom-end"}),popperContent.classList.remove("hidden"),popperContent.setAttribute("data-openfilter","true"),referenceElement.classList.add("btn-outline-primary"),referenceElement.classList.remove("btn-primary"),referenceElement.setAttribute("aria-expanded",!0);const closeListener=e=>{e.target.id===referenceElement.id||popperContent===e.target.closest('[data-openfilter="true"]')||void 0!==e.keyCode&&e.keyCode!==_key_codes.default.enter&&e.keyCode!==_key_codes.default.space||(closeOpenFilters(referenceElement,popperContent),document.removeEventListener("click",closeListener),document.removeEventListener("keyup",closeListener),document.removeEventListener("keyup",escCloseListener))};document.addEventListener("click",closeListener),document.addEventListener("keyup",closeListener);const escCloseListener=e=>{e.keyCode===_key_codes.default.escape&&(closeOpenFilters(referenceElement,popperContent),document.removeEventListener("keyup",escCloseListener),document.removeEventListener("click",closeListener))};document.addEventListener("keyup",escCloseListener)})),jqRoot.on(_custom_interaction_events.default.events.activate,_selectors.default.filters.group.save,(function(){root.querySelectorAll(_selectors.default.filters.group.checkbox).forEach((function(popcheckbox){let filtersform=document.forms.filtersform,saveid=popcheckbox.getAttribute("data-saveid");filtersform.querySelector("#".concat(saveid)).checked=popcheckbox.checked})),submitWithFilter("#filter-groups-popover")})),document.querySelectorAll(_selectors.default.filters.exportlink.link).forEach((function(exportbutton){exportbutton.addEventListener("click",(function(event){document.forms.exportlinkform.action=event.target.dataset.url,document.forms.exportlinkform.submit()}))})),jqRoot.on(_custom_interaction_events.default.events.activate,_selectors.default.filters.date.trigger,(function(){let referenceElement=root.querySelector(_selectors.default.filters.date.trigger),popperContent=root.querySelector(_selectors.default.filters.date.popover);(0,_popper.createPopper)(referenceElement,popperContent,{placement:"bottom-end"}),popperContent.classList.remove("hidden"),popperContent.setAttribute("data-openfilter","true"),popperContent.querySelector('[name="filterdatefrompopover[enabled]"]').focus(),referenceElement.classList.add("btn-outline-primary"),referenceElement.classList.remove("btn-primary"),referenceElement.setAttribute("aria-expanded",!0);const closeListener=e=>{e.target.id===referenceElement.id||popperContent===e.target.closest('[data-openfilter="true"]')||void 0!==e.keyCode&&e.keyCode!==_key_codes.default.enter&&e.keyCode!==_key_codes.default.space||(closeOpenFilters(referenceElement,popperContent),document.removeEventListener("click",closeListener),document.removeEventListener("keyup",closeListener),document.removeEventListener("keyup",escCloseListener))};document.addEventListener("click",closeListener),document.addEventListener("keyup",closeListener);const escCloseListener=e=>{e.keyCode===_key_codes.default.escape&&(closeOpenFilters(referenceElement,popperContent),document.removeEventListener("keyup",escCloseListener),document.removeEventListener("click",closeListener))};document.addEventListener("keyup",escCloseListener)})),jqRoot.on(_custom_interaction_events.default.events.activate,_selectors.default.filters.date.save,(function(){let filtersForm=document.forms.filtersform;const datesPopover=root.querySelector(_selectors.default.filters.date.popover),fromEnabled=datesPopover.querySelector('[name="filterdatefrompopover[enabled]"]').checked?1:0,toEnabled=datesPopover.querySelector('[name="filterdatetopopover[enabled]"]').checked?1:0;if(fromEnabled||toEnabled){let args={data:[]};fromEnabled&&args.data.push({key:"from",year:datesPopover.querySelector('[name="filterdatefrompopover[year]"]').value,month:datesPopover.querySelector('[name="filterdatefrompopover[month]"]').value,day:datesPopover.querySelector('[name="filterdatefrompopover[day]"]').value,hour:0,minute:0}),toEnabled&&args.data.push({key:"to",year:datesPopover.querySelector('[name="filterdatetopopover[year]"]').value,month:datesPopover.querySelector('[name="filterdatetopopover[month]"]').value,day:datesPopover.querySelector('[name="filterdatetopopover[day]"]').value,hour:23,minute:59});const request={methodname:"core_calendar_get_timestamps",args:args};_ajax.default.call([request])[0].done((function(result){let fromTimestamp=0,toTimestamp=0;if(result.timestamps.forEach((function(data){"from"===data.key?fromTimestamp=data.timestamp:"to"===data.key&&(toTimestamp=data.timestamp)})),toTimestamp>0&&fromTimestamp>toTimestamp){const warningdiv=document.getElementById("dates-filter-warning");warningdiv.classList.remove("hidden"),warningdiv.classList.add("d-block")}else filtersForm.elements["datefrom[timestamp]"].value=fromTimestamp,filtersForm.elements["datefrom[enabled]"].value=fromEnabled,filtersForm.elements["dateto[timestamp]"].value=toTimestamp,filtersForm.elements["dateto[enabled]"].value=toEnabled,submitWithFilter("#filter-dates-popover")}))}else filtersForm.elements["datefrom[timestamp]"].value=0,filtersForm.elements["datefrom[enabled]"].value=fromEnabled,filtersForm.elements["dateto[timestamp]"].value=0,filtersForm.elements["dateto[enabled]"].value=toEnabled,submitWithFilter("#filter-dates-popover")})),jqRoot.on(_custom_interaction_events.default.events.activate,_selectors.default.filters.date.calendariconfrom,(function(){updateCalendarPosition(_selectors.default.filters.date.calendariconfrom)})),jqRoot.on(_custom_interaction_events.default.events.activate,_selectors.default.filters.date.calendariconto,(function(){updateCalendarPosition(_selectors.default.filters.date.calendariconto)}))}})); //# sourceMappingURL=filters.min.js.mapPK-B\y8> > -report/summary/amd/build/selectors.min.js.mapnu[{"version":3,"file":"selectors.min.js","sources":["../src/selectors.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module containing the selectors for the forum summary report.\n *\n * @module forumreport_summary/selectors\n * @copyright 2019 Jun Pataleta\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n filters: {\n group: {\n checkbox: '[data-region=\"filter-groups\"] input[type=\"checkbox\"]',\n clear: '[data-region=\"filter-groups\"] .filter-clear',\n popover: '#filter-groups-popover',\n save: '[data-region=\"filter-groups\"] .filter-save',\n selectall: '[data-region=\"filter-groups\"] .select-all',\n trigger: '#filter-groups-button',\n },\n date: {\n calendar: '#dateselector-calendar-panel',\n calendariconfrom: '#id_filterdatefrompopover_calendar',\n calendariconto: '#id_filterdatetopopover_calendar',\n popover: '#filter-dates-popover',\n save: '[data-region=\"filter-dates\"] .filter-save',\n trigger: '#filter-dates-button',\n },\n exportlink: {\n link: '#summaryreport #forumreport_summary_table button.export-link'\n }\n }\n};\n"],"names":["filters","group","checkbox","clear","popover","save","selectall","trigger","date","calendar","calendariconfrom","calendariconto","exportlink","link"],"mappings":"+KAuBe,CACXA,QAAS,CACLC,MAAO,CACHC,SAAU,uDACVC,MAAO,8CACPC,QAAS,yBACTC,KAAM,6CACNC,UAAW,4CACXC,QAAS,yBAEbC,KAAM,CACFC,SAAU,+BACVC,iBAAkB,qCAClBC,eAAgB,mCAChBP,QAAS,wBACTC,KAAM,4CACNE,QAAS,wBAEbK,WAAY,CACRC,KAAM"}PK-B\;Nww)report/summary/amd/build/selectors.min.jsnu[define("forumreport_summary/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={filters:{group:{checkbox:'[data-region="filter-groups"] input[type="checkbox"]',clear:'[data-region="filter-groups"] .filter-clear',popover:"#filter-groups-popover",save:'[data-region="filter-groups"] .filter-save',selectall:'[data-region="filter-groups"] .select-all',trigger:"#filter-groups-button"},date:{calendar:"#dateselector-calendar-panel",calendariconfrom:"#id_filterdatefrompopover_calendar",calendariconto:"#id_filterdatetopopover_calendar",popover:"#filter-dates-popover",save:'[data-region="filter-dates"] .filter-save',trigger:"#filter-dates-button"},exportlink:{link:"#summaryreport #forumreport_summary_table button.export-link"}}},_exports.default})); //# sourceMappingURL=selectors.min.js.mapPK-B\&]]+report/summary/amd/build/filters.min.js.mapnu[{"version":3,"file":"filters.min.js","sources":["../src/filters.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module responsible for handling forum summary report filters.\n *\n * @module forumreport_summary/filters\n * @copyright 2019 Michael Hawkins \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport {createPopper} from 'core/popper2';\nimport CustomEvents from 'core/custom_interaction_events';\nimport Selectors from 'forumreport_summary/selectors';\nimport Ajax from 'core/ajax';\nimport KeyCodes from 'core/key_codes';\nimport * as FormChangeChecker from 'core_form/changechecker';\n\nexport const init = (root) => {\n let jqRoot = $(root);\n\n // Hide loading spinner and show report once page is ready.\n // This ensures filters can be applied when sorting by columns.\n $(document).ready(function() {\n $('.loading-icon').hide();\n $('#summaryreport').removeClass('hidden');\n });\n\n // Generic filter handlers.\n\n // Called to override click event to trigger a proper generate request with filtering.\n const generateWithFilters = (event, getparams) => {\n let currentLink = document.forms.filtersform.action,\n newLink;\n\n if (event) {\n event.preventDefault();\n\n let currentSplit = currentLink.split('?'),\n currentstring = currentSplit[1],\n newparamsarray = getparams.split('&'),\n paramsstring = '',\n paramkeys = [],\n paramvalues = [];\n\n // Separate out the existing action GET param string.\n currentstring.split('&').forEach(function(param) {\n let splitparam = param.split('=');\n paramkeys.push(splitparam[0]);\n paramvalues.push(splitparam[1]);\n });\n\n newparamsarray.forEach(function(paramstring) {\n let newparam = paramstring.split('='),\n existingkey = paramkeys.indexOf(newparam[0]);\n\n // Overwrite value if existing, otherwise add new param.\n if (existingkey > -1) {\n paramvalues[existingkey] = newparam[1];\n } else {\n paramkeys.push(newparam[0]);\n paramvalues.push(newparam[1]);\n }\n });\n\n // Build URL.\n paramkeys.forEach(function(name, key) {\n paramsstring += `&${name}=${paramvalues[key]}`;\n });\n\n newLink = currentSplit[0] + '?' + paramsstring.substr(1);\n } else {\n newLink = currentLink;\n }\n\n document.forms.filtersform.action = newLink;\n document.forms.filtersform.submit();\n };\n\n // Override 'reset table preferences' so it generates with filters.\n $('.resettable').on(\"click\", \"a\", function(event) {\n generateWithFilters(event, event.target.search.substr(1));\n });\n\n // Override table heading sort links so they generate with filters.\n $('thead').on(\"click\", \"a\", function(event) {\n generateWithFilters(event, event.target.search.substr(1));\n });\n\n // Override pagination page links so they generate with filters.\n $('.pagination').on(\"click\", \"a\", function(event) {\n generateWithFilters(event, event.target.search.substr(1));\n });\n\n // Override rows per page submission so it generates with filters.\n if (document.forms.selectperpage) {\n document.forms.selectperpage.onsubmit = (event) => {\n let getparam = 'perpage=' + document.forms.selectperpage.elements.perpage.value;\n generateWithFilters(event, getparam);\n };\n }\n\n // Override download link so the file is generated with filters.\n const downloadForm = document.getElementById('summaryreport').querySelector('form.dataformatselector');\n if (downloadForm) {\n downloadForm.onsubmit = (event) => {\n const downloadType = downloadForm.querySelector('#downloadtype_download').value;\n const getParams = `download=${downloadType}`;\n const prevAction = document.forms.filtersform.action;\n\n generateWithFilters(event, getParams);\n\n // Revert action, so re-submitting the form via filter does not trigger a further download.\n document.forms.filtersform.action = prevAction;\n };\n }\n\n // Submit report via filter\n const submitWithFilter = (containerelement) => {\n // Disable the dates filter mform checker to prevent any changes triggering a warning to the user.\n FormChangeChecker.unWatchForm(document.querySelector(containerelement).querySelector('form'));\n // Close the container (eg popover).\n $(containerelement).addClass('hidden');\n\n // Submit the filter values and re-generate report.\n generateWithFilters(false);\n };\n\n // Use popper to override date mform calendar position.\n const updateCalendarPosition = (referenceid) => {\n let referenceElement = document.querySelector(referenceid),\n popperContent = document.querySelector(Selectors.filters.date.calendar);\n\n popperContent.style.removeProperty(\"z-index\");\n createPopper(referenceElement, popperContent, {placement: 'bottom-end'});\n };\n\n // Close the relevant filter.\n const closeOpenFilters = (openFilterButton, openFilter) => {\n openFilter.classList.add('hidden');\n openFilter.setAttribute('data-openfilter', 'false');\n\n openFilterButton.classList.add('btn-primary');\n openFilterButton.classList.remove('btn-outline-primary');\n openFilterButton.setAttribute('aria-expanded', false);\n };\n\n // Groups filter specific handlers.\n\n // Event handler for clicking select all groups.\n jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.selectall, function() {\n let deselected = root.querySelectorAll(Selectors.filters.group.checkbox + ':not(:checked)');\n deselected.forEach(function(checkbox) {\n checkbox.checked = true;\n });\n });\n\n // Event handler for clearing filter by clicking option.\n jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.clear, function() {\n // Clear checkboxes.\n let selected = root.querySelectorAll(Selectors.filters.group.checkbox + ':checked');\n selected.forEach(function(checkbox) {\n checkbox.checked = false;\n });\n });\n\n // Event handler for showing groups filter popover.\n jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.trigger, function() {\n // Create popover.\n let referenceElement = root.querySelector(Selectors.filters.group.trigger),\n popperContent = root.querySelector(Selectors.filters.group.popover);\n\n createPopper(referenceElement, popperContent, {placement: 'bottom-end'});\n\n // Show popover.\n popperContent.classList.remove('hidden');\n popperContent.setAttribute('data-openfilter', 'true');\n\n // Change to outlined button.\n referenceElement.classList.add('btn-outline-primary');\n referenceElement.classList.remove('btn-primary');\n\n // Let screen readers know that it's now expanded.\n referenceElement.setAttribute('aria-expanded', true);\n\n // Add listeners to handle closing filter.\n const closeListener = e => {\n if (e.target.id !== referenceElement.id && popperContent !== e.target.closest('[data-openfilter=\"true\"]') &&\n (typeof e.keyCode === 'undefined' || e.keyCode === KeyCodes.enter || e.keyCode === KeyCodes.space)) {\n closeOpenFilters(referenceElement, popperContent);\n document.removeEventListener('click', closeListener);\n document.removeEventListener('keyup', closeListener);\n document.removeEventListener('keyup', escCloseListener);\n }\n };\n\n document.addEventListener('click', closeListener);\n document.addEventListener('keyup', closeListener);\n\n const escCloseListener = e => {\n if (e.keyCode === KeyCodes.escape) {\n closeOpenFilters(referenceElement, popperContent);\n document.removeEventListener('keyup', escCloseListener);\n document.removeEventListener('click', closeListener);\n }\n };\n\n document.addEventListener('keyup', escCloseListener);\n });\n\n // Event handler to click save groups filter.\n jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.save, function() {\n // Copy the saved values into the form before submitting.\n let popcheckboxes = root.querySelectorAll(Selectors.filters.group.checkbox);\n\n popcheckboxes.forEach(function(popcheckbox) {\n let filtersform = document.forms.filtersform,\n saveid = popcheckbox.getAttribute('data-saveid');\n\n filtersform.querySelector(`#${saveid}`).checked = popcheckbox.checked;\n });\n\n submitWithFilter('#filter-groups-popover');\n });\n\n // Listeners for export buttons.\n // These allow fetching of the relevant export URL, before submitting the request with\n // any POST data that is common to all of the export links. This allows filters to be\n // applied that contain potentially a lot of data (eg discussion IDs for groups filtering).\n document.querySelectorAll(Selectors.filters.exportlink.link).forEach(function(exportbutton) {\n exportbutton.addEventListener('click', function(event) {\n document.forms.exportlinkform.action = event.target.dataset.url;\n document.forms.exportlinkform.submit();\n });\n });\n\n // Dates filter specific handlers.\n\n // Event handler for showing dates filter popover.\n jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.trigger, function() {\n\n // Create popover.\n let referenceElement = root.querySelector(Selectors.filters.date.trigger),\n popperContent = root.querySelector(Selectors.filters.date.popover);\n\n createPopper(referenceElement, popperContent, {placement: 'bottom-end'});\n\n // Show popover and move focus.\n popperContent.classList.remove('hidden');\n popperContent.setAttribute('data-openfilter', 'true');\n popperContent.querySelector('[name=\"filterdatefrompopover[enabled]\"]').focus();\n\n // Change to outlined button.\n referenceElement.classList.add('btn-outline-primary');\n referenceElement.classList.remove('btn-primary');\n\n // Let screen readers know that it's now expanded.\n referenceElement.setAttribute('aria-expanded', true);\n\n // Add listener to handle closing filter.\n const closeListener = e => {\n if (e.target.id !== referenceElement.id && popperContent !== e.target.closest('[data-openfilter=\"true\"]') &&\n (typeof e.keyCode === 'undefined' || e.keyCode === KeyCodes.enter || e.keyCode === KeyCodes.space)) {\n closeOpenFilters(referenceElement, popperContent);\n document.removeEventListener('click', closeListener);\n document.removeEventListener('keyup', closeListener);\n document.removeEventListener('keyup', escCloseListener);\n }\n };\n\n document.addEventListener('click', closeListener);\n document.addEventListener('keyup', closeListener);\n\n const escCloseListener = e => {\n if (e.keyCode === KeyCodes.escape) {\n closeOpenFilters(referenceElement, popperContent);\n document.removeEventListener('keyup', escCloseListener);\n document.removeEventListener('click', closeListener);\n }\n };\n\n document.addEventListener('keyup', escCloseListener);\n });\n\n // Event handler to save dates filter.\n jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.save, function() {\n // Populate the hidden form inputs to submit the data.\n let filtersForm = document.forms.filtersform;\n const datesPopover = root.querySelector(Selectors.filters.date.popover);\n const fromEnabled = datesPopover.querySelector('[name=\"filterdatefrompopover[enabled]\"]').checked ? 1 : 0;\n const toEnabled = datesPopover.querySelector('[name=\"filterdatetopopover[enabled]\"]').checked ? 1 : 0;\n\n if (!fromEnabled && !toEnabled) {\n // Update the elements in the filter form.\n filtersForm.elements['datefrom[timestamp]'].value = 0;\n filtersForm.elements['datefrom[enabled]'].value = fromEnabled;\n filtersForm.elements['dateto[timestamp]'].value = 0;\n filtersForm.elements['dateto[enabled]'].value = toEnabled;\n\n // Submit the filter values and re-generate report.\n submitWithFilter('#filter-dates-popover');\n } else {\n let args = {data: []};\n\n if (fromEnabled) {\n args.data.push({\n 'key': 'from',\n 'year': datesPopover.querySelector('[name=\"filterdatefrompopover[year]\"]').value,\n 'month': datesPopover.querySelector('[name=\"filterdatefrompopover[month]\"]').value,\n 'day': datesPopover.querySelector('[name=\"filterdatefrompopover[day]\"]').value,\n 'hour': 0,\n 'minute': 0\n });\n }\n\n if (toEnabled) {\n args.data.push({\n 'key': 'to',\n 'year': datesPopover.querySelector('[name=\"filterdatetopopover[year]\"]').value,\n 'month': datesPopover.querySelector('[name=\"filterdatetopopover[month]\"]').value,\n 'day': datesPopover.querySelector('[name=\"filterdatetopopover[day]\"]').value,\n 'hour': 23,\n 'minute': 59\n });\n }\n\n const request = {\n methodname: 'core_calendar_get_timestamps',\n args: args\n };\n\n Ajax.call([request])[0].done(function(result) {\n let fromTimestamp = 0,\n toTimestamp = 0;\n\n result.timestamps.forEach(function(data) {\n if (data.key === 'from') {\n fromTimestamp = data.timestamp;\n } else if (data.key === 'to') {\n toTimestamp = data.timestamp;\n }\n });\n\n // Display an error if the from date is later than the do date.\n if (toTimestamp > 0 && fromTimestamp > toTimestamp) {\n const warningdiv = document.getElementById('dates-filter-warning');\n warningdiv.classList.remove('hidden');\n warningdiv.classList.add('d-block');\n } else {\n filtersForm.elements['datefrom[timestamp]'].value = fromTimestamp;\n filtersForm.elements['datefrom[enabled]'].value = fromEnabled;\n filtersForm.elements['dateto[timestamp]'].value = toTimestamp;\n filtersForm.elements['dateto[enabled]'].value = toEnabled;\n\n // Submit the filter values and re-generate report.\n submitWithFilter('#filter-dates-popover');\n }\n });\n }\n });\n\n jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.calendariconfrom, function() {\n updateCalendarPosition(Selectors.filters.date.calendariconfrom);\n });\n\n jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.calendariconto, function() {\n updateCalendarPosition(Selectors.filters.date.calendariconto);\n });\n};\n"],"names":["root","jqRoot","document","ready","hide","removeClass","generateWithFilters","event","getparams","newLink","currentLink","forms","filtersform","action","preventDefault","currentSplit","split","currentstring","newparamsarray","paramsstring","paramkeys","paramvalues","forEach","param","splitparam","push","paramstring","newparam","existingkey","indexOf","name","key","substr","submit","on","target","search","selectperpage","onsubmit","getparam","elements","perpage","value","downloadForm","getElementById","querySelector","downloadType","getParams","prevAction","submitWithFilter","containerelement","FormChangeChecker","unWatchForm","addClass","updateCalendarPosition","referenceid","referenceElement","popperContent","Selectors","filters","date","calendar","style","removeProperty","placement","closeOpenFilters","openFilterButton","openFilter","classList","add","setAttribute","remove","CustomEvents","events","activate","group","selectall","querySelectorAll","checkbox","checked","clear","trigger","popover","closeListener","e","id","closest","keyCode","KeyCodes","enter","space","removeEventListener","escCloseListener","addEventListener","escape","save","popcheckbox","saveid","getAttribute","exportlink","link","exportbutton","exportlinkform","dataset","url","focus","filtersForm","datesPopover","fromEnabled","toEnabled","args","data","request","methodname","call","done","result","fromTimestamp","toTimestamp","timestamps","timestamp","warningdiv","calendariconfrom","calendariconto"],"mappings":";;;;;;;wgCA+BqBA,WACbC,QAAS,mBAAED,0BAIbE,UAAUC,OAAM,+BACZ,iBAAiBC,2BACjB,kBAAkBC,YAAY,mBAM9BC,oBAAsB,CAACC,MAAOC,iBAE5BC,QADAC,YAAcR,SAASS,MAAMC,YAAYC,UAGzCN,MAAO,CACPA,MAAMO,qBAEHC,aAAeL,YAAYM,MAAM,KACjCC,cAAgBF,aAAa,GAC7BG,eAAiBV,UAAUQ,MAAM,KACjCG,aAAe,GACfC,UAAY,GACZC,YAAc,GAGjBJ,cAAcD,MAAM,KAAKM,SAAQ,SAASC,WAClCC,WAAaD,MAAMP,MAAM,KAC7BI,UAAUK,KAAKD,WAAW,IAC1BH,YAAYI,KAAKD,WAAW,OAGhCN,eAAeI,SAAQ,SAASI,iBACxBC,SAAWD,YAAYV,MAAM,KAC7BY,YAAcR,UAAUS,QAAQF,SAAS,IAGzCC,aAAe,EACfP,YAAYO,aAAeD,SAAS,IAEpCP,UAAUK,KAAKE,SAAS,IACxBN,YAAYI,KAAKE,SAAS,QAKlCP,UAAUE,SAAQ,SAASQ,KAAMC,KAC7BZ,yBAAoBW,iBAAQT,YAAYU,SAG5CtB,QAAUM,aAAa,GAAK,IAAMI,aAAaa,OAAO,QAEtDvB,QAAUC,YAGdR,SAASS,MAAMC,YAAYC,OAASJ,QACpCP,SAASS,MAAMC,YAAYqB,8BAI7B,eAAeC,GAAG,QAAS,KAAK,SAAS3B,OACvCD,oBAAoBC,MAAOA,MAAM4B,OAAOC,OAAOJ,OAAO,2BAIxD,SAASE,GAAG,QAAS,KAAK,SAAS3B,OACjCD,oBAAoBC,MAAOA,MAAM4B,OAAOC,OAAOJ,OAAO,2BAIxD,eAAeE,GAAG,QAAS,KAAK,SAAS3B,OACvCD,oBAAoBC,MAAOA,MAAM4B,OAAOC,OAAOJ,OAAO,OAItD9B,SAASS,MAAM0B,gBACfnC,SAASS,MAAM0B,cAAcC,SAAY/B,YACjCgC,SAAW,WAAarC,SAASS,MAAM0B,cAAcG,SAASC,QAAQC,MAC1EpC,oBAAoBC,MAAOgC,kBAK7BI,aAAezC,SAAS0C,eAAe,iBAAiBC,cAAc,2BACxEF,eACAA,aAAaL,SAAY/B,cACfuC,aAAeH,aAAaE,cAAc,0BAA0BH,MACpEK,6BAAwBD,cACxBE,WAAa9C,SAASS,MAAMC,YAAYC,OAE9CP,oBAAoBC,MAAOwC,WAG3B7C,SAASS,MAAMC,YAAYC,OAASmC,mBAKtCC,iBAAoBC,mBAEtBC,kBAAkBC,YAAYlD,SAAS2C,cAAcK,kBAAkBL,cAAc,6BAEnFK,kBAAkBG,SAAS,UAG7B/C,qBAAoB,IAIlBgD,uBAA0BC,kBACxBC,iBAAmBtD,SAAS2C,cAAcU,aAC1CE,cAAgBvD,SAAS2C,cAAca,mBAAUC,QAAQC,KAAKC,UAElEJ,cAAcK,MAAMC,eAAe,oCACtBP,iBAAkBC,cAAe,CAACO,UAAW,gBAIxDC,iBAAmB,CAACC,iBAAkBC,cACxCA,WAAWC,UAAUC,IAAI,UACzBF,WAAWG,aAAa,kBAAmB,SAE3CJ,iBAAiBE,UAAUC,IAAI,eAC/BH,iBAAiBE,UAAUG,OAAO,uBAClCL,iBAAiBI,aAAa,iBAAiB,IAMnDrE,OAAOiC,GAAGsC,mCAAaC,OAAOC,SAAUhB,mBAAUC,QAAQgB,MAAMC,WAAW,WACtD5E,KAAK6E,iBAAiBnB,mBAAUC,QAAQgB,MAAMG,SAAW,kBAC/DxD,SAAQ,SAASwD,UACxBA,SAASC,SAAU,QAK3B9E,OAAOiC,GAAGsC,mCAAaC,OAAOC,SAAUhB,mBAAUC,QAAQgB,MAAMK,OAAO,WAEpDhF,KAAK6E,iBAAiBnB,mBAAUC,QAAQgB,MAAMG,SAAW,YAC/DxD,SAAQ,SAASwD,UACtBA,SAASC,SAAU,QAK3B9E,OAAOiC,GAAGsC,mCAAaC,OAAOC,SAAUhB,mBAAUC,QAAQgB,MAAMM,SAAS,eAEjEzB,iBAAmBxD,KAAK6C,cAAca,mBAAUC,QAAQgB,MAAMM,SAC9DxB,cAAgBzD,KAAK6C,cAAca,mBAAUC,QAAQgB,MAAMO,kCAElD1B,iBAAkBC,cAAe,CAACO,UAAW,eAG1DP,cAAcW,UAAUG,OAAO,UAC/Bd,cAAca,aAAa,kBAAmB,QAG9Cd,iBAAiBY,UAAUC,IAAI,uBAC/Bb,iBAAiBY,UAAUG,OAAO,eAGlCf,iBAAiBc,aAAa,iBAAiB,SAGzCa,cAAgBC,IACdA,EAAEjD,OAAOkD,KAAO7B,iBAAiB6B,IAAM5B,gBAAkB2B,EAAEjD,OAAOmD,QAAQ,kCAChD,IAAdF,EAAEG,SAA2BH,EAAEG,UAAYC,mBAASC,OAASL,EAAEG,UAAYC,mBAASE,QAChGzB,iBAAiBT,iBAAkBC,eACnCvD,SAASyF,oBAAoB,QAASR,eACtCjF,SAASyF,oBAAoB,QAASR,eACtCjF,SAASyF,oBAAoB,QAASC,oBAI9C1F,SAAS2F,iBAAiB,QAASV,eACnCjF,SAAS2F,iBAAiB,QAASV,qBAE7BS,iBAAmBR,IACjBA,EAAEG,UAAYC,mBAASM,SACvB7B,iBAAiBT,iBAAkBC,eACnCvD,SAASyF,oBAAoB,QAASC,kBACtC1F,SAASyF,oBAAoB,QAASR,iBAI9CjF,SAAS2F,iBAAiB,QAASD,qBAIvC3F,OAAOiC,GAAGsC,mCAAaC,OAAOC,SAAUhB,mBAAUC,QAAQgB,MAAMoB,MAAM,WAE9C/F,KAAK6E,iBAAiBnB,mBAAUC,QAAQgB,MAAMG,UAEpDxD,SAAQ,SAAS0E,iBACvBpF,YAAcV,SAASS,MAAMC,YAC7BqF,OAASD,YAAYE,aAAa,eAEtCtF,YAAYiC,yBAAkBoD,SAAUlB,QAAUiB,YAAYjB,WAGlE9B,iBAAiB,6BAOrB/C,SAAS2E,iBAAiBnB,mBAAUC,QAAQwC,WAAWC,MAAM9E,SAAQ,SAAS+E,cAC1EA,aAAaR,iBAAiB,SAAS,SAAStF,OAC5CL,SAASS,MAAM2F,eAAezF,OAASN,MAAM4B,OAAOoE,QAAQC,IAC5DtG,SAASS,MAAM2F,eAAerE,eAOtChC,OAAOiC,GAAGsC,mCAAaC,OAAOC,SAAUhB,mBAAUC,QAAQC,KAAKqB,SAAS,eAGhEzB,iBAAmBxD,KAAK6C,cAAca,mBAAUC,QAAQC,KAAKqB,SAC7DxB,cAAgBzD,KAAK6C,cAAca,mBAAUC,QAAQC,KAAKsB,kCAEjD1B,iBAAkBC,cAAe,CAACO,UAAW,eAG1DP,cAAcW,UAAUG,OAAO,UAC/Bd,cAAca,aAAa,kBAAmB,QAC9Cb,cAAcZ,cAAc,2CAA2C4D,QAGvEjD,iBAAiBY,UAAUC,IAAI,uBAC/Bb,iBAAiBY,UAAUG,OAAO,eAGlCf,iBAAiBc,aAAa,iBAAiB,SAGzCa,cAAgBC,IACdA,EAAEjD,OAAOkD,KAAO7B,iBAAiB6B,IAAM5B,gBAAkB2B,EAAEjD,OAAOmD,QAAQ,kCAChD,IAAdF,EAAEG,SAA2BH,EAAEG,UAAYC,mBAASC,OAASL,EAAEG,UAAYC,mBAASE,QAChGzB,iBAAiBT,iBAAkBC,eACnCvD,SAASyF,oBAAoB,QAASR,eACtCjF,SAASyF,oBAAoB,QAASR,eACtCjF,SAASyF,oBAAoB,QAASC,oBAI9C1F,SAAS2F,iBAAiB,QAASV,eACnCjF,SAAS2F,iBAAiB,QAASV,qBAE7BS,iBAAmBR,IACjBA,EAAEG,UAAYC,mBAASM,SACvB7B,iBAAiBT,iBAAkBC,eACnCvD,SAASyF,oBAAoB,QAASC,kBACtC1F,SAASyF,oBAAoB,QAASR,iBAI9CjF,SAAS2F,iBAAiB,QAASD,qBAIvC3F,OAAOiC,GAAGsC,mCAAaC,OAAOC,SAAUhB,mBAAUC,QAAQC,KAAKmC,MAAM,eAE7DW,YAAcxG,SAASS,MAAMC,kBAC3B+F,aAAe3G,KAAK6C,cAAca,mBAAUC,QAAQC,KAAKsB,SACzD0B,YAAcD,aAAa9D,cAAc,2CAA2CkC,QAAU,EAAI,EAClG8B,UAAYF,aAAa9D,cAAc,yCAAyCkC,QAAU,EAAI,KAE/F6B,aAAgBC,UASd,KACCC,KAAO,CAACC,KAAM,IAEdH,aACAE,KAAKC,KAAKtF,KAAK,KACJ,YACCkF,aAAa9D,cAAc,wCAAwCH,YAClEiE,aAAa9D,cAAc,yCAAyCH,UACtEiE,aAAa9D,cAAc,uCAAuCH,WACjE,SACE,IAIdmE,WACAC,KAAKC,KAAKtF,KAAK,KACJ,UACCkF,aAAa9D,cAAc,sCAAsCH,YAChEiE,aAAa9D,cAAc,uCAAuCH,UACpEiE,aAAa9D,cAAc,qCAAqCH,WAC/D,UACE,WAIZsE,QAAU,CACZC,WAAY,+BACZH,KAAMA,oBAGLI,KAAK,CAACF,UAAU,GAAGG,MAAK,SAASC,YAC9BC,cAAgB,EAChBC,YAAc,KAElBF,OAAOG,WAAWjG,SAAQ,SAASyF,MACd,SAAbA,KAAKhF,IACLsF,cAAgBN,KAAKS,UACD,OAAbT,KAAKhF,MACZuF,YAAcP,KAAKS,cAKvBF,YAAc,GAAKD,cAAgBC,YAAa,OAC1CG,WAAavH,SAAS0C,eAAe,wBAC3C6E,WAAWrD,UAAUG,OAAO,UAC5BkD,WAAWrD,UAAUC,IAAI,gBAEzBqC,YAAYlE,SAAS,uBAAuBE,MAAQ2E,cACpDX,YAAYlE,SAAS,qBAAqBE,MAAQkE,YAClDF,YAAYlE,SAAS,qBAAqBE,MAAQ4E,YAClDZ,YAAYlE,SAAS,mBAAmBE,MAAQmE,UAGhD5D,iBAAiB,iCA7DzByD,YAAYlE,SAAS,uBAAuBE,MAAQ,EACpDgE,YAAYlE,SAAS,qBAAqBE,MAAQkE,YAClDF,YAAYlE,SAAS,qBAAqBE,MAAQ,EAClDgE,YAAYlE,SAAS,mBAAmBE,MAAQmE,UAGhD5D,iBAAiB,4BA6DzBhD,OAAOiC,GAAGsC,mCAAaC,OAAOC,SAAUhB,mBAAUC,QAAQC,KAAK8D,kBAAkB,WAC7EpE,uBAAuBI,mBAAUC,QAAQC,KAAK8D,qBAGlDzH,OAAOiC,GAAGsC,mCAAaC,OAAOC,SAAUhB,mBAAUC,QAAQC,KAAK+D,gBAAgB,WAC3ErE,uBAAuBI,mBAAUC,QAAQC,KAAK+D"}PK-B\)n#kk*report/summary/tests/event/events_test.phpnu[. /** * Tests for forum report summary events. * * @package forumreport_summary * @category test * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace forumreport_summary\event; /** * Tests for forum report summary events. * * @package forumreport_summary * @category test * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class events_test extends \advanced_testcase { /** * Test report_downloaded event. */ public function test_report_downloaded(): void { global $DB; $this->resetAfterTest(); // Create course and teacher user. $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_user(); $roleteacher = $DB->get_record('role', ['shortname' => 'teacher']); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $roleteacher->id); // Create forum. $this->setUser($teacher); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $context = \context_module::instance($forum->cmid); // Trigger and capture event. $eventparams = [ 'context' => $context, 'other' => [ 'forumid' => $forum->id, 'hasviewall' => true, ], ]; $event = \forumreport_summary\event\report_downloaded::create($eventparams); $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $sink->close(); // Check the event contains the expected data. $this->assertInstanceOf('\forumreport_summary\event\report_downloaded', $event); $this->assertEquals($context, $event->get_context()); $this->assertEquals(CONTEXT_MODULE, $event->contextlevel); $this->assertEquals($forum->cmid, $event->contextinstanceid); $this->assertEquals($teacher->id, $event->userid); $url = new \moodle_url('/mod/forum/report/summary/index.php', ['courseid' => $course->id, 'forumid' => $forum->id]); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); $this->assertNotEmpty($event->get_description()); } /** * Test report_viewed event. */ public function test_report_viewed(): void { global $DB; $this->resetAfterTest(); // Create course and teacher user. $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_user(); $roleteacher = $DB->get_record('role', ['shortname' => 'teacher']); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $roleteacher->id); // Create forum. $this->setUser($teacher); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $context = \context_module::instance($forum->cmid); // Trigger and capture event. $eventparams = [ 'context' => $context, 'other' => [ 'forumid' => $forum->id, 'hasviewall' => true, ], ]; $event = \forumreport_summary\event\report_viewed::create($eventparams); $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $sink->close(); // Check the event contains the expected data. $this->assertInstanceOf('\forumreport_summary\event\report_viewed', $event); $this->assertEquals($context, $event->get_context()); $this->assertEquals(CONTEXT_MODULE, $event->contextlevel); $this->assertEquals($forum->cmid, $event->contextinstanceid); $this->assertEquals($teacher->id, $event->userid); $url = new \moodle_url('/mod/forum/report/summary/index.php', ['courseid' => $course->id, 'forumid' => $forum->id]); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); $this->assertNotEmpty($event->get_description()); } } PK-B\E /report/summary/tests/behat/bulk_message.featurenu[@mod @mod_forum @forumreport @forumreport_summary Feature: Message users in the summary report In order to encourage users to participate As a teacher I should be able to send messages to those who are not taking part Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student1@example.com | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And the following "activities" exist: | activity | name | course | idnumber | | forum | forum1 | C1 | forum1 | And the following forum discussions exist in course "Course 1": | user | forum | name | message | | teacher1 | forum1 | discussion1 | t1 earliest | | teacher1 | forum1 | discussion2 | t1 between | | student1 | forum1 | discussion3 | s1 latest | And the following forum replies exist in course "Course 1": | user | forum | discussion | message | | teacher1 | forum1 | discussion1 | t1 between | | teacher1 | forum1 | discussion2 | t1 latest | | student1 | forum1 | discussion1 | s1 earliest | @javascript Scenario: Message some users Given the following "users" exist: | username | firstname | lastname | email | | student3 | Student | 3 | student3@example.com | And the following "course enrolments" exist: | user | course | role | | student3 | C1 | student | When I am on the forum1 "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration And I click on "Select 'Student 1'" "checkbox" And I click on "Select 'Student 3'" "checkbox" And I set the field "With selected users..." to "Send a message" And I set the field "bulk-message" to "blah blah" And I click on "Send message to 2 people" "button" Then I should see "Message sent to 2 people" And I log out And I log in as "student1" And I should see "1" in the "Toggle messaging drawer" "button" And I log out And I log in as "student3" And I should see "1" in the "Toggle messaging drawer" "button" And I log out And I log in as "student2" And I should not see "1" in the "Toggle messaging drawer" "button" @javascript Scenario: Message all users When I am on the forum1 "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration And I click on "Select all" "checkbox" And I set the field "With selected users..." to "Send a message" Then I should see "Send message to 3 people" @javascript Scenario: Ensure no message options when messaging is disabled Given the following config values are set as admin: | messaging | 0 | When I am on the forum1 "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration Then I should not see "With selected users..." And I should not see "Select all" PK-B\ i#6report/summary/tests/behat/summary_data_access.featurenu[@mod @mod_forum @forumreport @forumreport_summary Feature: Report relevant content availability In order to view the appropriate report content As a teacher or student I need to have the associated capabilities Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And the following "activities" exist: | activity | name | course | idnumber | | forum | forum1 | C1 | forum1 | And the following forum discussions exist in course "Course 1": | user | forum | name | message | attachments | inlineattachments | | teacher1 | forum1 | discussion1 | message 1 | | | | student1 | forum1 | discussion2 | message 3 | att1.jpg | in1.jpg | | student2 | forum1 | discussion3 | message 4 | att3.jpg | | And the following forum replies exist in course "Course 1": | user | forum | discussion | message | attachments | inlineattachments | | student1 | forum1 | discussion1 | reply1 | att4.jpg, att5.jpg | in2.jpg | | student2 | forum1 | discussion2 | reply2 | | | @javascript Scenario: Teachers can access report data about other users by default Given I am on the "forum1" "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration Then the following should exist in the "forumreport_summary_table" table: | -2- | -3- | -4- | -5- | -6- | -7- | -8- | | Student 1 | 1 | 1 | 5 | 0 | 3 | 14 | | Student 2 | 1 | 1 | 1 | 0 | 3 | 14 | | Teacher 1 | 1 | 0 | 0 | 1 | 2 | 8 | And "select-all-users" "checkbox" should be visible And "First name" "link" should be visible And "Last name" "link" should be visible And "Number of discussions posted" "link" should be visible And "Number of replies posted" "link" should be visible And "Number of attachments" "link" should be visible And "Number of views" "link" should be visible And "Word count" "link" should be visible And "Character count" "link" should be visible And "Earliest post" "link" should be visible And "Most recent post" "link" should be visible And I should see "Export posts" And "Export posts" "link" should not exist Scenario: Students cannot access the summary report by default Given I am on the "forum1" "forum activity" page logged in as student1 Then "Forum summary report" "link" should not exist in current page administration @javascript Scenario: Students given the view capability can only view their own report data Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | forumreport/summary:view | Allow | student | Course | C1 | When I am on the "forum1" "forum activity" page logged in as student1 And I navigate to "Reports" in current page administration Then the following should exist in the "forumreport_summary_table" table: | -1- | -2- | -3- | -4- | -5- | -6- | -7- | | Student 1 | 1 | 1 | 5 | 1 | 3 | 14 | And the following should not exist in the "forumreport_summary_table" table: | -1- | | Student 2 | | Teacher 1 | And "select-all-users" "checkbox" should not exist And "First name" "link" should be visible And "Last name" "link" should be visible And "Number of discussions posted" "link" should be visible And "Number of replies posted" "link" should be visible And "Number of attachments" "link" should be visible And "Number of views" "link" should be visible And "Word count" "link" should be visible And "Character count" "link" should be visible And "Earliest post" "link" should be visible And "Most recent post" "link" should be visible And I should not see "Export posts" PK-B\Ww  ;report/summary/tests/behat/summary_filter_no_groups.featurenu[@mod @mod_forum @forumreport @forumreport_summary Feature: Groups report filter is not available if no groups exist When no groups exist As a teacher I can view the forum summary report for all users of a forum Scenario: Report data is available without groups filter if no groups exist Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | | Course 2 | C2 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | | teacher1 | C2 | editingteacher | And the following "groups" exist: | name | course | idnumber | | Group A | C2 | G1 | And the following "group members" exist: | user | group | | teacher1 | G1 | And the following "activities" exist: | activity | name | course | idnumber | groupmode | | forum | forum1 | C1 | forum1C1 | 0 | | forum | forum2 | C1 | forum2C1 | 0 | | forum | forum1 | C2 | forum1C2 | 2 | And the following forum discussions exist in course "Course 1": | user | forum | name | message | created | | teacher1 | forum1 | discussion1 | D1 message | ## 1 month ago ## | | teacher1 | forum1 | discussion2 | D2 message | ## 1 week ago ## | | teacher1 | forum2 | discussion3 | D3 message | ## 4 days ago ## | | student1 | forum1 | discussion4 | D4 message | ## 3 days ago ## | | student2 | forum2 | discussion5 | D5 message | ## 2 days ago## | And the following forum replies exist in course "Course 1": | user | forum | discussion | message | created | | teacher1 | forum1 | discussion1 | D1 reply | ## 3 weeks ago ## | | teacher1 | forum1 | discussion2 | D2 reply | ## 6 days ago ## | | teacher1 | forum2 | discussion3 | D3 reply | ## 3 days ago ## | | student1 | forum1 | discussion1 | D1 reply 2 | ## 2 weeks ago ## | | student2 | forum2 | discussion3 | D3 reply | ## 2 days ago ## | And the following forum discussions exist in course "Course 2": | user | forum | name | message | created | | teacher1 | forum1 | discussion1 | D1 other course | ## 1 week ago ## | | teacher1 | forum1 | discussion2 | D2 other course | ## 4 days ago ## | When I am on the forum1C1 "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration Then "Groups" "button" should not exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | | First name | -3- | | Teacher 1 | 2 | | Student 1 | 1 | | Student 2 | 0 | PK-B\aF99:report/summary/tests/behat/summary_data_post_dates.featurenu[@mod @mod_forum @forumreport @forumreport_summary Feature: Post date columns data available In order to determine users' earliest and most recent forum posts As a teacher I need to view that data in the forum summary report Scenario: Add posts and view accurate summary report Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | | Course 2 | C2 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | | teacher1 | C2 | editingteacher | And the following "activities" exist: | activity | name | course | idnumber | | forum | forum1 | C1 | forum1C1 | | forum | forum2 | C1 | forum2C1 | | forum | forum1 | C2 | forum1C2 | And the following forum discussions exist in course "Course 1": | user | forum | name | message | created | | teacher1 | forum1 | discussion1 | t1 earliest | ##2018-01-02 09:00:00## | | teacher1 | forum1 | discussion2 | t1 between | ##2018-03-27 10:00:00## | | teacher1 | forum2 | discussion3 | t1 other forum | ##2018-01-01 11:00:00## | | student1 | forum1 | discussion4 | s1 latest | ##2019-03-27 13:00:00## | | student2 | forum2 | discussion5 | s2 other forum | ##2018-03-27 09:00:00## | And the following forum replies exist in course "Course 1": | user | forum | discussion | message | created | | teacher1 | forum1 | discussion1 | t1 between | ##2018-01-02 10:30:00## | | teacher1 | forum1 | discussion2 | t1 latest | ##2019-09-01 07:00:00## | | teacher1 | forum2 | discussion3 | t1 other forum | ##2019-09-12 08:00:00## | | student1 | forum1 | discussion1 | s1 earliest | ##2019-03-27 04:00:00## | | student2 | forum2 | discussion3 | s2 other forum | ##2018-03-27 10:00:00## | And the following forum discussions exist in course "Course 2": | user | forum | name | message | created | | teacher1 | forum1 | discussion1 | t1 other course | ##2017-01-01 03:00:00## | | teacher1 | forum1 | discussion2 | t1 other course | ##2019-09-13 23:59:00## | When I am on the forum1C1 "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration Then "Teacher 1" row "Earliest post" column of "forumreport_summary_table" table should contain "Tuesday, 2 January 2018, 9:00" Then "Teacher 1" row "Most recent post" column of "forumreport_summary_table" table should contain "Sunday, 1 September 2019, 7:00" Then "Student 1" row "Earliest post" column of "forumreport_summary_table" table should contain "Wednesday, 27 March 2019, 4:00" Then "Student 1" row "Most recent post" column of "forumreport_summary_table" table should contain "Wednesday, 27 March 2019, 1:00" Then "Student 2" row "Earliest post" column of "forumreport_summary_table" table should contain "-" Then "Student 2" row "Most recent post" column of "forumreport_summary_table" table should contain "-" PK-B\;^!^!1report/summary/tests/behat/course_summary.featurenu[@mod @mod_forum @forumreport @forumreport_summary Feature: Course level forum summary report In order to gain an overview of students' forum activities across a course As a teacher I should be able to prepare a summary report of all forums in a course Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | | student3 | Student | 3 | student3@example.com | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | | Course 2 | C2 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher1 | C2 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | | student2 | C2 | student | | student3 | C2 | student | And the following "activities" exist: | activity | name | course | idnumber | | forum | forum1 | C1 | forum1 | | forum | forum2 | C1 | forum2 | | forum | forum3 | C1 | forum3 | | forum | forum4 | C2 | forum4 | And the following forum discussions exist in course "Course 1": | user | forum | name | message | created | | teacher1 | forum1 | discussion1 | Discussion 1 | ##2018-01-14 09:00:00## | | teacher1 | forum2 | discussion2 | Discussion 2 | ##2019-03-27 12:10:00## | | teacher1 | forum3 | discussion3 | Discussion 3 | ##2019-12-25 15:20:00## | | teacher1 | forum3 | discussion4 | Discussion 4 | ##2019-12-26 09:30:00## | | student1 | forum2 | discussion5 | Discussion 5 | ##2019-06-06 18:40:00## | | student1 | forum3 | discussion6 | Discussion 6 | ##2020-01-25 11:50:00## | And the following forum replies exist in course "Course 1": | user | forum | discussion | subject | message | created | | teacher1 | forum1 | discussion1 | Re d1 | Reply 1 | ##2018-02-15 11:10:00## | | teacher1 | forum2 | discussion5 | Re d5 | Reply 2 | ##2019-06-09 18:20:00## | | teacher1 | forum2 | discussion5 | Re d5 | Reply 3 | ##2019-07-10 09:30:00## | | student1 | forum1 | discussion1 | Re d1 | Reply 4 | ##2018-01-25 16:40:00## | | student1 | forum2 | discussion2 | Re d6 | Reply 5 | ##2019-03-28 11:50:00## | | student1 | forum3 | discussion4 | Re d4 | Reply 6 | ##2019-12-30 20:00:00## | And the following forum discussions exist in course "Course 2": | user | forum | name | message | created | | teacher1 | forum4 | discussion7 | Discussion 7 | ##2020-01-29 15:00:00## | | student2 | forum4 | discussion8 | Discussion 8 | ##2020-02-02 16:00:00## | And the following forum replies exist in course "Course 2": | user | forum | discussion | subject | message | created | | teacher1 | forum4 | discussion8 | Re d8 | Reply 7 | ##2020-02-03 09:45:00## | | student2 | forum4 | discussion7 | Re d7 | Reply 8 | ##2020-02-04 13:50:00## | Scenario: Course forum summary report can be viewed by teacher and contains accurate data When I am on the forum2 "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration And I should see "Export posts" And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | | | First name | -3- | -4- | Earliest post | Most recent post | | Student 1 | 1 | 1 | Thursday, 28 March 2019, 11:50 | Thursday, 6 June 2019, 6:40 | | Student 2 | 0 | 0 | - | - | | Teacher 1 | 1 | 2 | Wednesday, 27 March 2019, 12:10 | Wednesday, 10 July 2019, 9:30 | And the following should not exist in the "forumreport_summary_table" table: | First name | | Student 3 | And the "Forum selected" select box should contain "All forums in course" And the "Forum selected" select box should contain "forum1" And the "Forum selected" select box should contain "forum2" And the "Forum selected" select box should contain "forum3" And the "Forum selected" select box should not contain "forum4" Then I select "All forums in course" from the "Forum selected" singleselect And I should not see "Export posts" And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | | | First name | -3- | -4- | Earliest post | Most recent post | | Student 1 | 2 | 3 | Thursday, 25 January 2018, 4:40 | Saturday, 25 January 2020, 11:50 | | Student 2 | 0 | 0 | - | - | | Teacher 1 | 4 | 3 | Sunday, 14 January 2018, 9:00 | Thursday, 26 December 2019, 9:30 | And the following should not exist in the "forumreport_summary_table" table: | First name | | Student 3 | Scenario: Course forum summary report correctly formats forum activity names Given the "multilang" filter is "on" And the "multilang" filter applies to "content and headings" And the following "activity" exists: | activity | forum | | course | C1 | | name | F-EngF-Ger | When I am on the "C1" "Course" page logged in as "teacher1" And I follow "F-Eng" And I navigate to "Reports" in current page administration Then the "Forum selected" select box should contain "F-Eng" Scenario: Students given the view capability can view their own course report data Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | forumreport/summary:view | Allow | student | Course | C1 | When I am on the "forum1" "forum activity" page logged in as student1 And I navigate to "Reports" in current page administration And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | | | First name | -2- | -3- | Earliest post | Most recent post | | Student 1 | 0 | 1 | Thursday, 25 January 2018, 4:40 | Thursday, 25 January 2018, 4:40 | And the following should not exist in the "forumreport_summary_table" table: | First name | | Student 2 | | Student 3 | | Teacher 1 | And the "Forum selected" select box should contain "All forums in course" And the "Forum selected" select box should contain "forum1" And the "Forum selected" select box should contain "forum2" And the "Forum selected" select box should contain "forum3" And the "Forum selected" select box should not contain "forum4" Then I select "All forums in course" from the "Forum selected" singleselect And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | | | First name | -2- | -3- | Earliest post | Most recent post | | Student 1 | 2 | 3 | Thursday, 25 January 2018, 4:40 | Saturday, 25 January 2020, 11:50 | And the following should not exist in the "forumreport_summary_table" table: | First name | | Student 2 | | Student 3 | | Teacher 1 | PK-B\$(j j ;report/summary/tests/behat/summary_data_attachments.featurenu[@mod @mod_forum @forumreport @forumreport_summary Feature: Attachments count column data available In order to gather data on users' forum attachments As a teacher I need to view accurate attachment count data in the forum summary report Scenario: Add discussions and replies with attached files Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | | Course 2 | C2 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | | teacher1 | C2 | editingteacher | And the following "activities" exist: | activity | name | course | idnumber | | forum | forum1 | C1 | forum1C1 | | forum | forum2 | C1 | forum2C1 | | forum | forum1 | C2 | forum1C2 | And the following forum discussions exist in course "Course 1": | user | forum | name | message | attachments | inlineattachments | | teacher1 | forum1 | discussion1 | message1 | att1.jpg, att2.txt | | | teacher1 | forum2 | discussion2 | message2 | att3.jpg | in1.jpg | | student1 | forum1 | discussion3 | message3 | att4.jpg | in2.jpg | | student2 | forum1 | discussion4 | message4 | | | And the following forum replies exist in course "Course 1": | user | forum | discussion | message | attachments | inlineattachments | | teacher1 | forum1 | discussion1 | reply1 | att5.jpg | in3.txt | | teacher1 | forum1 | discussion1 | reply2 | att5.jpg | in3.txt | | teacher1 | forum2 | discussion2 | reply2 | att6.jpg | | | student1 | forum1 | discussion3 | reply3 | att7.jpg, att8.jpg | in2.jpg | | student2 | forum1 | discussion4 | reply4 | | | And the following forum discussions exist in course "Course 2": | user | forum | name | message | attachments | inlineattachments | | teacher1 | forum1 | discussion1 | message1 | att1.jpg, att2.txt | | When I am on the forum1C1 "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration Then "Teacher 1" row "Number of attachments" column of "forumreport_summary_table" table should contain "6" And "Student 1" row "Number of attachments" column of "forumreport_summary_table" table should contain "5" And "Student 2" row "Number of attachments" column of "forumreport_summary_table" table should contain "0" PK-B\>\Areport/summary/tests/behat/double_enrolments_summary_data.featurenu[@mod @mod_forum @forumreport @forumreport_summary Feature: forum report shows post/reply/word counts correctly In order to gather data on users' forum activities As a teacher I need to view accurate forum summary report when students have more than 1 enrolment Scenario: Add discussions and replies with attached files Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | | Course 2 | C2 | 0 | And the following "course enrolments" exist: | user | course | role | enrol | | teacher1 | C1 | editingteacher | manual | | student1 | C1 | student | manual | | student1 | C1 | student | self | | student2 | C1 | student | manual | | student2 | C1 | student | self | | teacher1 | C2 | editingteacher | manual | And the following "activities" exist: | activity | name | course | idnumber | | forum | forum1 | C1 | forum1 | | forum | forum2 | C1 | forum2 | | forum | forum1 | C2 | forum1 | And the following forum discussions exist in course "Course 1": | user | forum | name | message | attachments | inlineattachments | | teacher1 | forum1 | discussion1 | message1 | att1.jpg, att2.txt | | | teacher1 | forum2 | discussion2 | message2 | att3.jpg | in1.jpg | | student1 | forum1 | discussion3 | my message3 | att4.jpg | in2.jpg | | student2 | forum1 | discussion4 | my message4 | | | And the following forum replies exist in course "Course 1": | user | forum | discussion | message | attachments | inlineattachments | | teacher1 | forum1 | discussion1 | reply1 | att5.jpg | in3.txt | | teacher1 | forum1 | discussion1 | reply2 | att5.jpg | in3.txt | | teacher1 | forum2 | discussion2 | reply2 | att6.jpg | | | student1 | forum1 | discussion3 | my reply3 | att7.jpg, att8.jpg | in2.jpg | | student2 | forum1 | discussion4 | my reply4 | | | | student2 | forum1 | discussion4 | my reply5 | | | And the following forum discussions exist in course "Course 2": | user | forum | name | message | attachments | inlineattachments | | teacher1 | forum1 | discussion1 | message1 | att1.jpg, att2.txt | | When I am on the "Course 1" course page logged in as teacher1 And I follow "forum1" And I navigate to "Reports" in current page administration Then "Teacher 1" row "Number of attachments" column of "forumreport_summary_table" table should contain "6" And "Student 1" row "Number of attachments" column of "forumreport_summary_table" table should contain "5" And "Student 1" row "Word count" column of "forumreport_summary_table" table should contain "4" And "Student 1" row "Character count" column of "forumreport_summary_table" table should contain "18" And "Student 1" row "Number of discussions posted" column of "forumreport_summary_table" table should contain "1" And "Student 1" row "Number of replies posted" column of "forumreport_summary_table" table should contain "1" And "Student 2" row "Number of attachments" column of "forumreport_summary_table" table should contain "0" And "Student 2" row "Word count" column of "forumreport_summary_table" table should contain "6" And "Student 2" row "Character count" column of "forumreport_summary_table" table should contain "26" And "Student 2" row "Number of discussions posted" column of "forumreport_summary_table" table should contain "1" And "Student 2" row "Number of replies posted" column of "forumreport_summary_table" table should contain "2" PK-B\8V' ' 2report/summary/tests/behat/private_replies.featurenu[@mod @mod_forum @forumreport @forumreport_summary Feature: Include private replies in the summary report In order to generate accurate reports based on what is visible As a teacher I should have private replies being counted as well Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | teacher2 | Teacher | 2 | teacher2@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student1@example.com | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher2 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And the following "activities" exist: | activity | name | course | idnumber | | forum | forum1 | C1 | forum1 | And the following "mod_forum > discussions" exist: | user | forum | name | message | | teacher1 | forum1 | discussion1 | t1 earliest | | teacher1 | forum1 | discussion2 | t1 between | | teacher1 | forum1 | discussion3 | s1 latest | And the following "mod_forum > posts" exist: | user | parentsubject | subject | message | privatereplyto | | teacher1 | discussion1 | t1 between | t1 between | | | teacher1 | discussion2 | t1 latest | t1 latest | | | student1 | discussion1 | s1 earliest | s1 earliest | | | teacher1 | s1 earliest | This is a private reply | This is a private reply | 1 | Scenario: Private replies are counted for Teacher When I am on the forum1 "forum activity" page logged in as teacher2 And I navigate to "Reports" in current page administration Then "Teacher 1" row "Number of replies posted" column of "forumreport_summary_table" table should contain "3" Scenario: Private replies are not counted when Teacher has not capability Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | mod/forum:readprivatereplies | Prevent | editingteacher | Course | C1 | When I am on the forum1 "forum activity" page logged in as teacher2 And I navigate to "Reports" in current page administration Then "Teacher 1" row "Number of replies posted" column of "forumreport_summary_table" table should contain "2" PK-B\bs 3 38report/summary/tests/behat/summary_filter_groups.featurenu[@mod @mod_forum @forumreport @forumreport_summary Feature: Groups report filter is available if groups exist In order to retrieve targeted forum data As a teacher I can filter the forum summary report by groups of users Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | | Course 2 | C2 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | | teacher1 | C2 | editingteacher | And the following "groups" exist: | name | course | idnumber | | Group A | C1 | G1 | | Group B | C1 | G2 | | Group C | C1 | G3 | | Group D | C1 | G4 | | Group E | C2 | G5 | And the following "group members" exist: | user | group | | teacher1 | G1 | | teacher1 | G2 | | teacher1 | G3 | | student1 | G3 | And the following "activities" exist: | activity | name | course | idnumber | groupmode | | forum | forum1 | C1 | c1forum1 | 1 | | forum | forum2 | C1 | c1forum2 | 2 | | forum | forum1 | C2 | c2forum1 | 0 | And the following forum discussions exist in course "Course 1": | user | forum | name | message | group | created | | teacher1 | forum1 | discussion1 | D1 message | G1 | ## 1 month ago ## | | teacher1 | forum1 | discussion2 | D2 message | G2 | ## 1 week ago ## | | teacher1 | forum2 | discussion3 | D3 message | G1 | ## 6 days ago ## | | teacher1 | forum2 | discussion4 | D4 message | G4 | ## 5 days ago ## | | teacher1 | forum2 | discussion5 | D5 message | All participants | ## 4 days ago ## | | student1 | forum1 | discussion6 | D6 message | G3 | ## 3 days ago ## | | student2 | forum2 | discussion7 | D7 message | All participants | ## 2 days ago ## | And the following forum replies exist in course "Course 1": | user | forum | discussion | message | created | | teacher1 | forum1 | discussion1 | D1 reply | ## 3 weeks ago ## | | teacher1 | forum2 | discussion3 | D3 reply | ## 4 days ago ## | | teacher1 | forum1 | discussion6 | D6 reply | ## 2 days ago ## | | student1 | forum1 | discussion6 | D6 reply 2 | ## 2 days ago ## | | student2 | forum2 | discussion4 | D4 reply | ## 4 days ago ## | | student2 | forum2 | discussion5 | D5 reply | ## 3 days ago ## | And the following forum discussions exist in course "Course 2": | user | forum | name | message | created | | teacher1 | forum1 | discussion1 | D1 other course | ## 1 week ago ## | | teacher1 | forum1 | discussion2 | D2 other course | ## 4 days ago ## | @javascript Scenario: All groups can be selected or cleared together in the groups filter, and are unchecked by default When I am on the "c1forum1" "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration Then "Groups" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 1 | 1 | | Student 2 | 0 | 0 | | Teacher 1 | 2 | 2 | And I click on "Groups" "button" And "Group A" "checkbox" should exist in the "filter-groups-popover" "region" And "Group B" "checkbox" should exist in the "filter-groups-popover" "region" And "Group C" "checkbox" should exist in the "filter-groups-popover" "region" And "Group D" "checkbox" should exist in the "filter-groups-popover" "region" And "No groups" "checkbox" should exist in the "filter-groups-popover" "region" And "Group E" "checkbox" should not exist in the "filter-groups-popover" "region" And the following fields match these values: | Group A | 0 | | Group B | 0 | | Group C | 0 | | Group D | 0 | | No groups | 0 | And I click on "Select all" "button" in the "filter-groups-popover" "region" And the following fields match these values: | Group A | 1 | | Group B | 1 | | Group C | 1 | | Group D | 1 | | No groups | 1 | And I click on "Clear" "button" in the "filter-groups-popover" "region" And the following fields match these values: | Group A | 0 | | Group B | 0 | | Group C | 0 | | Group D | 0 | | No groups | 0 | And I click on "Select all" "button" in the "filter-groups-popover" "region" And I click on "Save" "button" in the "filter-groups-popover" "region" And "Groups (all)" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 1 | 1 | | Student 2 | 0 | 0 | | Teacher 1 | 2 | 2 | @javascript Scenario: The summary report can be filtered by a subset of groups, and re-ordering the results retains the filter When I am on the "c1forum1" "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration Then "Groups" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 1 | 1 | | Student 2 | 0 | 0 | | Teacher 1 | 2 | 2 | And I click on "Groups" "button" And I click on "Clear" "button" in the "filter-groups-popover" "region" And I click on "Group A" "checkbox" in the "filter-groups-popover" "region" And I click on "Group C" "checkbox" in the "filter-groups-popover" "region" And I click on "Group D" "checkbox" in the "filter-groups-popover" "region" And I click on "Save" "button" And "Groups (3)" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 1 | 1 | | Teacher 1 | 1 | 2 | And I should not see "Student 2" # Ensure re-ordering retains filter. And I click on "Number of discussions posted" "link" And "Groups (3)" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 1 | 1 | | Teacher 1 | 1 | 2 | And I should not see "Student 2" @javascript Scenario: The summary report can be filtered as a mixture of groups and no groups When I am on the "c1forum2" "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration Then "Groups" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 0 | 0 | | Student 2 | 1 | 2 | | Teacher 1 | 3 | 1 | And I click on "Groups" "button" And I click on "Clear" "button" in the "filter-groups-popover" "region" And I click on "Group A" "checkbox" in the "filter-groups-popover" "region" And I click on "No groups" "checkbox" in the "filter-groups-popover" "region" And I click on "Save" "button" in the "filter-groups-popover" "region" And "Groups (2)" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 0 | 0 | | Student 2 | 1 | 1 | | Teacher 1 | 2 | 1 | @javascript Scenario: The summary report can be filtered by no groups only When I am on the "c1forum2" "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration Then the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 0 | 0 | | Student 2 | 1 | 2 | | Teacher 1 | 3 | 1 | And I click on "Groups" "button" And I click on "Clear" "button" in the "filter-groups-popover" "region" And I click on "No groups" "checkbox" in the "filter-groups-popover" "region" And I click on "Save" "button" in the "filter-groups-popover" "region" And "Groups (1)" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 0 | 0 | | Student 2 | 1 | 1 | | Teacher 1 | 1 | 0 | @javascript Scenario: Filtering by a group containing no users still allows the page to render # Log in as admin so Teacher 1 not existing on page can be confirmed. When I am on the "c1forum1" "forum activity" page logged in as admin And I navigate to "Reports" in current page administration Then "Groups" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 1 | 1 | | Student 2 | 0 | 0 | | Teacher 1 | 2 | 2 | And I click on "Groups" "button" And I click on "Clear" "button" in the "filter-groups-popover" "region" And I click on "Group D" "checkbox" in the "filter-groups-popover" "region" And I click on "Save" "button" in the "filter-groups-popover" "region" And "Groups (1)" "button" should exist And I should see "Nothing to display" And I should not see "Teacher 1" And I should not see "Student 1" And I should not see "Student 2" And I should not see "With selected users..." And I should not see "Download table data as" @javascript Scenario: Course forum summary report can be filtered by group When I am on the "c1forum2" "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration And I select "All forums in course" from the "Forum selected" singleselect And I click on "Groups" "button" And I click on "Clear" "button" in the "filter-groups-popover" "region" And I click on "Group A" "checkbox" in the "filter-groups-popover" "region" And I click on "Group C" "checkbox" in the "filter-groups-popover" "region" And I click on "Save" "button" And "Groups (2)" "button" should exist Then the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 1 | 1 | | Teacher 1 | 2 | 3 | And I should not see "Student 2" # Ensure re-ordering retains filter. And I click on "Number of discussions posted" "link" And "Groups (2)" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 1 | 1 | | Teacher 1 | 2 | 3 | And I should not see "Student 2" PK-B\d77user.phpnu[. /** * Display user activity reports for a course * * @package mod_forum * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require(__DIR__.'/../../config.php'); require_once($CFG->dirroot.'/mod/forum/lib.php'); require_once($CFG->dirroot.'/rating/lib.php'); require_once($CFG->dirroot.'/user/lib.php'); $courseid = optional_param('course', null, PARAM_INT); // Limit the posts to just this course $userid = optional_param('id', $USER->id, PARAM_INT); // User id whose posts we want to view $mode = optional_param('mode', 'posts', PARAM_ALPHA); // The mode to use. Either posts or discussions $page = optional_param('page', 0, PARAM_INT); // The page number to display $perpage = optional_param('perpage', 5, PARAM_INT); // The number of posts to display per page if (empty($userid)) { if (!isloggedin()) { require_login(); } $userid = $USER->id; } $discussionsonly = ($mode !== 'posts'); $isspecificcourse = !is_null($courseid); $iscurrentuser = ($USER->id == $userid); $url = new moodle_url('/mod/forum/user.php', array('id' => $userid)); if ($isspecificcourse) { $url->param('course', $courseid); } if ($discussionsonly) { $url->param('mode', 'discussions'); } $PAGE->set_url($url); $PAGE->set_pagelayout('standard'); if ($page != 0) { $url->param('page', $page); } if ($perpage != 5) { $url->param('perpage', $perpage); } $user = $DB->get_record("user", array("id" => $userid), '*', MUST_EXIST); $usercontext = context_user::instance($user->id, MUST_EXIST); // Check if the requested user is the guest user if (isguestuser($user)) { // The guest user cannot post, so it is not possible to view any posts. // May as well just bail aggressively here. throw new \moodle_exception('invaliduserid'); } // Make sure the user has not been deleted if ($user->deleted) { $PAGE->set_title(get_string('userdeleted')); $PAGE->set_context(context_system::instance()); echo $OUTPUT->header(); echo $OUTPUT->heading($PAGE->title); echo $OUTPUT->footer(); die; } $isloggedin = isloggedin(); $isguestuser = $isloggedin && isguestuser(); $isparent = !$iscurrentuser && $DB->record_exists('role_assignments', array('userid'=>$USER->id, 'contextid'=>$usercontext->id)); $hasparentaccess = $isparent && has_all_capabilities(array('moodle/user:viewdetails', 'moodle/user:readuserposts'), $usercontext); // Check whether a specific course has been requested if ($isspecificcourse) { // Get the requested course and its context $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); $coursecontext = context_course::instance($courseid, MUST_EXIST); // We have a specific course to search, which we will also assume we are within. if ($hasparentaccess) { // A `parent` role won't likely have access to the course so we won't attempt // to enter it. We will however still make them jump through the normal // login hoops require_login(); $PAGE->set_context($coursecontext); $PAGE->set_course($course); } else { // Enter the course we are searching require_login($course); } // Get the course ready for access checks $courses = array($courseid => $course); } else { // We are going to search for all of the users posts in all courses! // a general require login here as we arn't actually within any course. require_login(); $PAGE->set_context(context_user::instance($user->id)); // Now we need to get all of the courses to search. // All courses where the user has posted within a forum will be returned. $courses = forum_get_courses_user_posted_in($user, $discussionsonly); } $params = array( 'context' => $PAGE->context, 'relateduserid' => $user->id, 'other' => array('reportmode' => $mode), ); $event = \mod_forum\event\user_report_viewed::create($params); $event->trigger(); // Get the posts by the requested user that the current user can access. $result = forum_get_posts_by_user($user, $courses, $isspecificcourse, $discussionsonly, ($page * $perpage), $perpage); // Check whether there are not posts to display. if (empty($result->posts)) { // Ok no posts to display means that either the user has not posted or there // are no posts made by the requested user that the current user is able to // see. // In either case we need to decide whether we can show personal information // about the requested user to the current user so we will execute some checks $canviewuser = user_can_view_profile($user, null, $usercontext); // Prepare the page title $pagetitle = get_string('noposts', 'mod_forum'); // Get the page heading if ($isspecificcourse) { $pageheading = format_string($course->fullname, true, array('context' => $coursecontext)); } else { $pageheading = get_string('pluginname', 'mod_forum'); } // Next we need to set up the loading of the navigation and choose a message // to display to the current user. if ($iscurrentuser) { // No need to extend the navigation it happens automatically for the // current user. if ($discussionsonly) { $notification = get_string('nodiscussionsstartedbyyou', 'forum'); } else { $notification = get_string('nopostsmadebyyou', 'forum'); } // These are the user's forum interactions. // Shut down the navigation 'Users' node. $usernode = $PAGE->navigation->find('users', null); $usernode->make_inactive(); // Edit navbar. if (isset($courseid) && $courseid != SITEID) { // Create as much of the navbar automatically. if ($newusernode = $PAGE->navigation->find('user' . $user->id, null)) { $newusernode->make_active(); } // Check to see if this is a discussion or a post. if ($mode == 'posts') { $navbar = $PAGE->navbar->add(get_string('posts', 'forum'), new moodle_url('/mod/forum/user.php', array('id' => $user->id, 'course' => $courseid))); } else { $navbar = $PAGE->navbar->add(get_string('discussions', 'forum'), new moodle_url('/mod/forum/user.php', array('id' => $user->id, 'course' => $courseid, 'mode' => 'discussions'))); } } } else if ($canviewuser) { $PAGE->navigation->extend_for_user($user); $PAGE->navigation->set_userid_for_parent_checks($user->id); // see MDL-25805 for reasons and for full commit reference for reversal when fixed. // Edit navbar. if (isset($courseid) && $courseid != SITEID) { // Create as much of the navbar automatically. if ($usernode = $PAGE->navigation->find('user' . $user->id, null)) { $usernode->make_active(); } // Check to see if this is a discussion or a post. if ($mode == 'posts') { $navbar = $PAGE->navbar->add(get_string('posts', 'forum'), new moodle_url('/mod/forum/user.php', array('id' => $user->id, 'course' => $courseid))); } else { $navbar = $PAGE->navbar->add(get_string('discussions', 'forum'), new moodle_url('/mod/forum/user.php', array('id' => $user->id, 'course' => $courseid, 'mode' => 'discussions'))); } } $fullname = fullname($user); if ($discussionsonly) { $notification = get_string('nodiscussionsstartedby', 'forum', $fullname); } else { $notification = get_string('nopostsmadebyuser', 'forum', $fullname); } } else { // Don't extend the navigation it would be giving out information that // the current uesr doesn't have access to. $notification = get_string('cannotviewusersposts', 'forum'); if ($isspecificcourse) { $url = new moodle_url('/course/view.php', array('id' => $courseid)); } else { $url = new moodle_url('/'); } navigation_node::override_active_url($url); } // Display a page letting the user know that there's nothing to display; $PAGE->set_title($pagetitle); if ($isspecificcourse) { $PAGE->set_secondary_active_tab('participants'); $PAGE->set_heading($pageheading); } else if ($canviewuser) { $PAGE->set_heading(fullname($user)); } else { $PAGE->set_heading($SITE->fullname); } echo $OUTPUT->header(); if (isset($courseid) && $courseid != SITEID) { $backurl = new moodle_url('/user/view.php', ['id' => $userid, 'course' => $courseid]); echo $OUTPUT->single_button($backurl, get_string('back'), 'get', ['class' => 'mb-3']); } if (!$isspecificcourse) { echo $OUTPUT->heading($pagetitle); } else { $userheading = array( 'heading' => fullname($user), 'user' => $user, 'usercontext' => $usercontext ); echo $OUTPUT->context_header($userheading, 2); } echo $OUTPUT->notification($notification); if (!$url->compare($PAGE->url)) { echo $OUTPUT->continue_button($url); } echo $OUTPUT->footer(); die; } $discussions = array(); foreach ($result->posts as $post) { $discussions[] = $post->discussion; } $discussions = $DB->get_records_list('forum_discussions', 'id', array_unique($discussions)); $entityfactory = mod_forum\local\container::get_entity_factory(); $rendererfactory = mod_forum\local\container::get_renderer_factory(); $postsrenderer = $rendererfactory->get_user_forum_posts_report_renderer(!$isspecificcourse && !$hasparentaccess); $postoutput = $postsrenderer->render( $USER, array_map(function($forum) use ($entityfactory, $result) { $cm = $forum->cm; $context = context_module::instance($cm->id); $course = $result->courses[$forum->course]; return $entityfactory->get_forum_from_stdclass($forum, $context, $cm, $course); }, $result->forums), array_map(function($discussion) use ($entityfactory) { return $entityfactory->get_discussion_from_stdclass($discussion); }, $discussions), array_map(function($post) use ($entityfactory) { return $entityfactory->get_post_from_stdclass($post); }, $result->posts) ); $userfullname = fullname($user); if ($discussionsonly) { $inpageheading = get_string('discussionsstartedby', 'mod_forum', $userfullname); } else { $inpageheading = get_string('postsmadebyuser', 'mod_forum', $userfullname); } if ($isspecificcourse) { $a = new stdClass; $a->fullname = $userfullname; $a->coursename = format_string($course->fullname, true, array('context' => $coursecontext)); $pageheading = $a->coursename; if ($discussionsonly) { $pagetitle = get_string('discussionsstartedbyuserincourse', 'mod_forum', $a); } else { $pagetitle = get_string('postsmadebyuserincourse', 'mod_forum', $a); } } else { $pagetitle = $inpageheading; $pageheading = $userfullname; } $PAGE->set_title($pagetitle); $PAGE->set_heading($pageheading); $PAGE->navigation->extend_for_user($user); $PAGE->navigation->set_userid_for_parent_checks($user->id); // see MDL-25805 for reasons and for full commit reference for reversal when fixed. // Edit navbar. if (isset($courseid) && $courseid != SITEID) { if ($usernode = $PAGE->navigation->find('user' . $user->id , null)) { $usernode->make_active(); } // Check to see if this is a discussion or a post. if ($mode == 'posts') { $navbar = $PAGE->navbar->add(get_string('posts', 'forum'), new moodle_url('/mod/forum/user.php', array('id' => $user->id, 'course' => $courseid))); } else { $navbar = $PAGE->navbar->add(get_string('discussions', 'forum'), new moodle_url('/mod/forum/user.php', array('id' => $user->id, 'course' => $courseid, 'mode' => 'discussions'))); } $PAGE->set_secondary_active_tab('participants'); } echo $OUTPUT->header(); if (isset($courseid) && $courseid != SITEID) { $backurl = new moodle_url('/user/view.php', ['id' => $userid, 'course' => $courseid]); echo $OUTPUT->single_button($backurl, get_string('back'), 'get', ['class' => 'mb-3']); } echo html_writer::start_tag('div', array('class' => 'user-content')); if ($isspecificcourse) { $userheading = array( 'heading' => fullname($user), 'user' => $user, 'usercontext' => $usercontext ); echo $OUTPUT->context_header($userheading, 2); $coursename = format_string($course->fullname, true, array('context' => $coursecontext)); $heading = $mode === 'posts' ? get_string('postsmadeincourse', 'mod_forum', $coursename) : get_string('discussionsstartedincourse', 'mod_forum', $coursename); echo $OUTPUT->heading($heading, 2, 'main mt-4 mb-4'); } else { echo $OUTPUT->heading($inpageheading); } if (!empty($postoutput)) { echo $OUTPUT->paging_bar($result->totalcount, $page, $perpage, $url); echo $postoutput; echo $OUTPUT->paging_bar($result->totalcount, $page, $perpage, $url); } else if ($discussionsonly) { echo $OUTPUT->heading(get_string('nodiscussionsstartedby', 'forum', $userfullname)); } else { echo $OUTPUT->heading(get_string('noposts', 'forum')); } echo html_writer::end_tag('div'); echo $OUTPUT->footer(); PK-B\si88 discuss.phpnu[. /** * Displays a post, and all the posts below it. * If no post is given, displays all posts in a discussion * * @package mod_forum * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../../config.php'); $d = required_param('d', PARAM_INT); // Discussion ID $parent = optional_param('parent', 0, PARAM_INT); // If set, then display this post and all children. $mode = optional_param('mode', 0, PARAM_INT); // If set, changes the layout of the thread $move = optional_param('move', 0, PARAM_INT); // If set, moves this discussion to another forum $mark = optional_param('mark', '', PARAM_ALPHA); // Used for tracking read posts if user initiated. $postid = optional_param('postid', 0, PARAM_INT); // Used for tracking read posts if user initiated. $pin = optional_param('pin', -1, PARAM_INT); // If set, pin or unpin this discussion. $url = new moodle_url('/mod/forum/discuss.php', array('d'=>$d)); if ($parent !== 0) { $url->param('parent', $parent); } $PAGE->set_url($url); $vaultfactory = mod_forum\local\container::get_vault_factory(); $discussionvault = $vaultfactory->get_discussion_vault(); $discussion = $discussionvault->get_from_id($d); if (!$discussion) { throw new \moodle_exception('errordiscussionnotfound', 'mod_forum'); } $forumvault = $vaultfactory->get_forum_vault(); $forum = $forumvault->get_from_id($discussion->get_forum_id()); if (!$forum) { throw new \moodle_exception('errorforumnotfound', 'mod_forum'); } $course = $forum->get_course_record(); $cm = $forum->get_course_module_record(); require_course_login($course, true, $cm); $managerfactory = mod_forum\local\container::get_manager_factory(); $capabilitymanager = $managerfactory->get_capability_manager($forum); $urlfactory = mod_forum\local\container::get_url_factory(); // Make sure we can render. if (!$capabilitymanager->can_view_discussions($USER)) { throw new moodle_exception('noviewdiscussionspermission', 'mod_forum'); } $datamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory(); $forumdatamapper = $datamapperfactory->get_forum_data_mapper(); $forumrecord = $forumdatamapper->to_legacy_object($forum); $discussiondatamapper = $datamapperfactory->get_discussion_data_mapper(); $discussionrecord = $discussiondatamapper->to_legacy_object($discussion); $discussionviewurl = $urlfactory->get_discussion_view_url_from_discussion($discussion); // Set the activity record, to avoid additional calls to the db if the page getter is called. $PAGE->set_activity_record($forumrecord); // move this down fix for MDL-6926 require_once($CFG->dirroot . '/mod/forum/lib.php'); $modcontext = $forum->get_context(); if ( !empty($CFG->enablerssfeeds) && !empty($CFG->forum_enablerssfeeds) && $forum->get_rss_type() && $forum->get_rss_articles() ) { require_once("$CFG->libdir/rsslib.php"); $rsstitle = format_string( $course->shortname, true, ['context' => context_course::instance($course->id)] ); $rsstitle .= ': ' . format_string($forum->get_name()); rss_add_http_header($modcontext, 'mod_forum', $forumrecord, $rsstitle); } // Move discussion if requested. if ($move > 0 && confirm_sesskey()) { $forumid = $forum->get_id(); $discussionid = $discussion->get_id(); $return = $discussionviewurl->out(false); if (!$forumto = $DB->get_record('forum', ['id' => $move])) { throw new \moodle_exception('cannotmovetonotexist', 'forum', $return); } if (!$capabilitymanager->can_move_discussions($USER)) { if ($forum->get_type() == 'single') { throw new \moodle_exception('cannotmovefromsingleforum', 'forum', $return); } else { throw new \moodle_exception('nopermissions', 'error', $return, get_capability_string('mod/forum:movediscussions')); } } if ($forumto->type == 'single') { throw new \moodle_exception('cannotmovetosingleforum', 'forum', $return); } // Get target forum cm and check it is visible to current user. $modinfo = get_fast_modinfo($course); $forums = $modinfo->get_instances_of('forum'); if (!array_key_exists($forumto->id, $forums)) { throw new \moodle_exception('cannotmovetonotfound', 'forum', $return); } $cmto = $forums[$forumto->id]; if (!$cmto->uservisible) { throw new \moodle_exception('cannotmovenotvisible', 'forum', $return); } $destinationctx = context_module::instance($cmto->id); require_capability('mod/forum:startdiscussion', $destinationctx); if (!forum_move_attachments($discussionrecord, $forumid, $forumto->id)) { echo $OUTPUT->notification("Errors occurred while moving attachment directories - check your file permissions"); } // For each subscribed user in this forum and discussion, copy over per-discussion subscriptions if required. $discussiongroup = $discussion->get_group_id() == -1 ? 0 : $discussion->get_group_id(); $potentialsubscribers = \mod_forum\subscriptions::fetch_subscribed_users( $forumrecord, $discussiongroup, $modcontext, 'u.id', true ); // Pre-seed the subscribed_discussion caches. // Firstly for the forum being moved to. \mod_forum\subscriptions::fill_subscription_cache($forumto->id); // And also for the discussion being moved. \mod_forum\subscriptions::fill_subscription_cache($forumid); $subscriptionchanges = []; $subscriptiontime = time(); foreach ($potentialsubscribers as $subuser) { $userid = $subuser->id; $targetsubscription = \mod_forum\subscriptions::is_subscribed($userid, $forumto, null, $cmto); $discussionsubscribed = \mod_forum\subscriptions::is_subscribed($userid, $forumrecord, $discussionid); $forumsubscribed = \mod_forum\subscriptions::is_subscribed($userid, $forumrecord); if ($forumsubscribed && !$discussionsubscribed && $targetsubscription) { // The user has opted out of this discussion and the move would cause them to receive notifications again. // Ensure they are unsubscribed from the discussion still. $subscriptionchanges[$userid] = \mod_forum\subscriptions::FORUM_DISCUSSION_UNSUBSCRIBED; } else if (!$forumsubscribed && $discussionsubscribed && !$targetsubscription) { // The user has opted into this discussion and would otherwise not receive the subscription after the move. // Ensure they are subscribed to the discussion still. $subscriptionchanges[$userid] = $subscriptiontime; } } $DB->set_field('forum_discussions', 'forum', $forumto->id, ['id' => $discussionid]); $DB->set_field('forum_read', 'forumid', $forumto->id, ['discussionid' => $discussionid]); // Delete the existing per-discussion subscriptions and replace them with the newly calculated ones. $DB->delete_records('forum_discussion_subs', ['discussion' => $discussionid]); $newdiscussion = clone $discussionrecord; $newdiscussion->forum = $forumto->id; foreach ($subscriptionchanges as $userid => $preference) { if ($preference != \mod_forum\subscriptions::FORUM_DISCUSSION_UNSUBSCRIBED) { // Users must have viewdiscussion to a discussion. if (has_capability('mod/forum:viewdiscussion', $destinationctx, $userid)) { \mod_forum\subscriptions::subscribe_user_to_discussion($userid, $newdiscussion, $destinationctx); } } else { \mod_forum\subscriptions::unsubscribe_user_from_discussion($userid, $newdiscussion, $destinationctx); } } $params = [ 'context' => $destinationctx, 'objectid' => $discussionid, 'other' => [ 'fromforumid' => $forumid, 'toforumid' => $forumto->id, ] ]; $event = \mod_forum\event\discussion_moved::create($params); $event->add_record_snapshot('forum_discussions', $discussionrecord); $event->add_record_snapshot('forum', $forumrecord); $event->add_record_snapshot('forum', $forumto); $event->trigger(); // Delete the RSS files for the 2 forums to force regeneration of the feeds require_once($CFG->dirroot . '/mod/forum/rsslib.php'); forum_rss_delete_file($forumrecord); forum_rss_delete_file($forumto); redirect($return . '&move=-1&sesskey=' . sesskey()); } // Pin or unpin discussion if requested. if ($pin !== -1 && confirm_sesskey()) { if (!$capabilitymanager->can_pin_discussions($USER)) { throw new \moodle_exception('nopermissions', 'error', $return, get_capability_string('mod/forum:pindiscussions')); } $params = ['context' => $modcontext, 'objectid' => $discussion->get_id(), 'other' => ['forumid' => $forum->get_id()]]; switch ($pin) { case FORUM_DISCUSSION_PINNED: // Pin the discussion and trigger discussion pinned event. forum_discussion_pin($modcontext, $forumrecord, $discussionrecord); break; case FORUM_DISCUSSION_UNPINNED: // Unpin the discussion and trigger discussion unpinned event. forum_discussion_unpin($modcontext, $forumrecord, $discussionrecord); break; default: echo $OUTPUT->notification("Invalid value when attempting to pin/unpin discussion"); break; } redirect($discussionviewurl->out(false)); } // Trigger discussion viewed event. forum_discussion_view($modcontext, $forumrecord, $discussionrecord); unset($SESSION->fromdiscussion); $saveddisplaymode = get_user_preferences('forum_displaymode', $CFG->forum_displaymode); if ($mode) { $displaymode = $mode; } else { $displaymode = $saveddisplaymode; } if (get_user_preferences('forum_useexperimentalui', false)) { if ($displaymode == FORUM_MODE_NESTED) { $displaymode = FORUM_MODE_NESTED_V2; } } else { if ($displaymode == FORUM_MODE_NESTED_V2) { $displaymode = FORUM_MODE_NESTED; } } if ($displaymode != $saveddisplaymode) { set_user_preference('forum_displaymode', $displaymode); } if ($parent) { // If flat AND parent, then force nested display this time if ($displaymode == FORUM_MODE_FLATOLDEST or $displaymode == FORUM_MODE_FLATNEWEST) { $displaymode = FORUM_MODE_NESTED; } } else { $parent = $discussion->get_first_post_id(); } $postvault = $vaultfactory->get_post_vault(); if (!$post = $postvault->get_from_id($parent)) { throw new \moodle_exception("notexists", 'forum', "$CFG->wwwroot/mod/forum/view.php?f={$forum->get_id()}"); } if (!$capabilitymanager->can_view_post($USER, $discussion, $post)) { throw new \moodle_exception('noviewdiscussionspermission', 'forum', "$CFG->wwwroot/mod/forum/view.php?id={$forum->get_id()}"); } $istracked = forum_tp_is_tracked($forumrecord, $USER); if ($mark == 'read'|| $mark == 'unread') { if ($CFG->forum_usermarksread && forum_tp_can_track_forums($forumrecord) && $istracked) { if ($mark == 'read') { forum_tp_add_read_record($USER->id, $postid); } else { // unread forum_tp_delete_read_records($USER->id, $postid); } } } $searchform = forum_search_form($course); $forumnode = $PAGE->navigation->find($cm->id, navigation_node::TYPE_ACTIVITY); if (empty($forumnode)) { $forumnode = $PAGE->navbar; } else { $forumnode->make_active(); } $node = $forumnode->add(format_string($discussion->get_name()), $discussionviewurl); $node->display = false; if ($node && $post->get_id() != $discussion->get_first_post_id()) { $node->add(format_string($post->get_subject()), $PAGE->url); } $isnestedv2displaymode = $displaymode == FORUM_MODE_NESTED_V2; $PAGE->set_title("$course->shortname: " . format_string($discussion->get_name())); $PAGE->set_heading($course->fullname); $PAGE->set_secondary_active_tab('modulepage'); $PAGE->activityheader->disable(); if ($isnestedv2displaymode) { $PAGE->add_body_class('nested-v2-display-mode reset-style'); $settingstrigger = $OUTPUT->render_from_template('mod_forum/settings_drawer_trigger', null); $PAGE->add_header_action($settingstrigger); } else { $PAGE->add_header_action(forum_search_form($course)); } echo $OUTPUT->header(); if (!$isnestedv2displaymode) { if (!$PAGE->has_secondary_navigation()) { echo $OUTPUT->heading(format_string($forum->get_name()), 2); } $headinglevel = $PAGE->activityheader->get_heading_level(); echo $OUTPUT->heading(format_string($discussion->get_name()), $headinglevel, 'discussionname'); } $rendererfactory = mod_forum\local\container::get_renderer_factory(); $discussionrenderer = $rendererfactory->get_discussion_renderer($forum, $discussion, $displaymode); $orderpostsby = $displaymode == FORUM_MODE_FLATNEWEST ? 'created DESC' : 'created ASC'; $replies = $postvault->get_replies_to_post($USER, $post, $capabilitymanager->can_view_any_private_reply($USER), $orderpostsby); if ($move == -1 and confirm_sesskey()) { $forumname = format_string($forum->get_name(), true); echo $OUTPUT->notification(get_string('discussionmoved', 'forum', $forumname), 'notifysuccess'); } echo $discussionrenderer->render($USER, $post, $replies); echo $OUTPUT->footer(); if ($istracked && !$CFG->forum_usermarksread) { if ($displaymode == FORUM_MODE_THREADED) { forum_tp_add_read_record($USER->id, $post->get_id()); } else { $postids = array_map(function($post) { return $post->get_id(); }, array_merge([$post], array_values($replies))); forum_tp_mark_posts_read($USER, $postids); } } PK-B\?}Z settings.phpnu[. /** * @package mod_forum * @copyright 2009 Petr Skoda (http://skodak.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if ($ADMIN->fulltree) { require_once($CFG->dirroot.'/mod/forum/lib.php'); $settings->add(new admin_setting_configselect('forum_displaymode', get_string('displaymode', 'forum'), get_string('configdisplaymode', 'forum'), FORUM_MODE_NESTED, forum_get_layout_modes())); // Less non-HTML characters than this is short $settings->add(new admin_setting_configtext('forum_shortpost', get_string('shortpost', 'forum'), get_string('configshortpost', 'forum'), 300, PARAM_INT)); // More non-HTML characters than this is long $settings->add(new admin_setting_configtext('forum_longpost', get_string('longpost', 'forum'), get_string('configlongpost', 'forum'), 600, PARAM_INT)); // Number of discussions on a page $settings->add(new admin_setting_configtext('forum_manydiscussions', get_string('manydiscussions', 'forum'), get_string('configmanydiscussions', 'forum'), 100, PARAM_INT)); if (isset($CFG->maxbytes)) { $maxbytes = 0; if (isset($CFG->forum_maxbytes)) { $maxbytes = $CFG->forum_maxbytes; } $settings->add(new admin_setting_configselect('forum_maxbytes', get_string('maxattachmentsize', 'forum'), get_string('configmaxbytes', 'forum'), 512000, get_max_upload_sizes($CFG->maxbytes, 0, 0, $maxbytes))); } // Default number of attachments allowed per post in all forums $settings->add(new admin_setting_configtext('forum_maxattachments', get_string('maxattachments', 'forum'), get_string('configmaxattachments', 'forum'), 9, PARAM_INT)); // Default Subscription mode setting. $options = forum_get_subscriptionmode_options(); $settings->add(new admin_setting_configselect('forum_subscription', get_string('subscriptionmode', 'forum'), get_string('configsubscriptiontype', 'forum'), FORUM_CHOOSESUBSCRIBE, $options)); // Default Read Tracking setting. $options = array(); $options[FORUM_TRACKING_OPTIONAL] = get_string('trackingoptional', 'forum'); $options[FORUM_TRACKING_OFF] = get_string('trackingoff', 'forum'); $options[FORUM_TRACKING_FORCED] = get_string('trackingon', 'forum'); $settings->add(new admin_setting_configselect('forum_trackingtype', get_string('trackingtype', 'forum'), get_string('configtrackingtype', 'forum'), FORUM_TRACKING_OPTIONAL, $options)); // Default whether user needs to mark a post as read $settings->add(new admin_setting_configcheckbox('forum_trackreadposts', get_string('trackforum', 'forum'), get_string('configtrackreadposts', 'forum'), 1)); // Default whether user needs to mark a post as read. $settings->add(new admin_setting_configcheckbox('forum_allowforcedreadtracking', get_string('forcedreadtracking', 'forum'), get_string('forcedreadtracking_desc', 'forum'), 0)); // Default number of days that a post is considered old $settings->add(new admin_setting_configtext('forum_oldpostdays', get_string('oldpostdays', 'forum'), get_string('configoldpostdays', 'forum'), 14, PARAM_INT)); // Default whether user needs to mark a post as read $settings->add(new admin_setting_configcheckbox('forum_usermarksread', get_string('usermarksread', 'forum'), get_string('configusermarksread', 'forum'), 0)); $options = array(); for ($i = 0; $i < 24; $i++) { $options[$i] = sprintf("%02d",$i); } // Default time (hour) to execute 'clean_read_records' cron $settings->add(new admin_setting_configselect('forum_cleanreadtime', get_string('cleanreadtime', 'forum'), get_string('configcleanreadtime', 'forum'), 2, $options)); // Default time (hour) to send digest email $settings->add(new admin_setting_configselect('digestmailtime', get_string('digestmailtime', 'forum'), get_string('configdigestmailtime', 'forum'), 17, $options)); if (empty($CFG->enablerssfeeds)) { $options = array(0 => get_string('rssglobaldisabled', 'admin')); $str = get_string('configenablerssfeeds', 'forum').'
'.get_string('configenablerssfeedsdisabled2', 'admin'); } else { $options = array(0=>get_string('no'), 1=>get_string('yes')); $str = get_string('configenablerssfeeds', 'forum'); } $settings->add(new admin_setting_configselect('forum_enablerssfeeds', get_string('enablerssfeeds', 'admin'), $str, 0, $options)); if (!empty($CFG->enablerssfeeds)) { $options = array( 0 => get_string('none'), 1 => get_string('discussions', 'forum'), 2 => get_string('posts', 'forum') ); $settings->add(new admin_setting_configselect('forum_rsstype', get_string('rsstypedefault', 'forum'), get_string('configrsstypedefault', 'forum'), 0, $options)); $settings->hide_if('forum_rsstype', 'forum_enablerssfeeds', 'neq', '1'); $options = array( 0 => '0', 1 => '1', 2 => '2', 3 => '3', 4 => '4', 5 => '5', 10 => '10', 15 => '15', 20 => '20', 25 => '25', 30 => '30', 40 => '40', 50 => '50' ); $settings->add(new admin_setting_configselect('forum_rssarticles', get_string('rssarticles', 'forum'), get_string('configrssarticlesdefault', 'forum'), 0, $options)); $settings->hide_if('forum_rssarticles', 'forum_enablerssfeeds', 'neq', '1'); } $settings->add(new admin_setting_configcheckbox('forum_enabletimedposts', get_string('timedposts', 'forum'), get_string('configenabletimedposts', 'forum'), 1)); $settings->add(new admin_setting_heading('defaultsettings', get_string('announcementsettings', 'mod_forum'), get_string('announcementsettings_help', 'mod_forum'))); // Default number of attachments allowed per post in announcement forums. $settings->add(new admin_setting_configtext('forum_announcementmaxattachments', get_string('maxattachments', 'forum'), get_string('configmaxattachments', 'forum'), 1, PARAM_INT)); // Default Subscription mode setting for announcement forums. $options = forum_get_subscriptionmode_options(); $settings->add(new admin_setting_configselect('forum_announcementsubscription', get_string('subscriptionmode', 'forum'), get_string('configsubscriptiontype', 'forum'), FORUM_FORCESUBSCRIBE, $options)); } PK-B\_`` mod_form.phpnu[. /** * @package mod_forum * @copyright Jamie Pratt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page } require_once ($CFG->dirroot.'/course/moodleform_mod.php'); use core_grades\component_gradeitems; class mod_forum_mod_form extends moodleform_mod { function definition() { global $CFG, $COURSE, $DB; $mform =& $this->_form; //------------------------------------------------------------------------------- $mform->addElement('header', 'general', get_string('general', 'form')); $mform->addElement('text', 'name', get_string('forumname', 'forum'), array('size'=>'64')); if (!empty($CFG->formatstringstriptags)) { $mform->setType('name', PARAM_TEXT); } else { $mform->setType('name', PARAM_CLEANHTML); } $mform->addRule('name', null, 'required', null, 'client'); $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); $this->standard_intro_elements(get_string('forumintro', 'forum')); $forumtypes = forum_get_forum_types(); core_collator::asort($forumtypes, core_collator::SORT_STRING); $mform->addElement('select', 'type', get_string('forumtype', 'forum'), $forumtypes); $mform->addHelpButton('type', 'forumtype', 'forum'); $mform->setDefault('type', 'general'); $mform->addElement('header', 'availability', get_string('availability', 'forum')); $name = get_string('duedate', 'forum'); $mform->addElement('date_time_selector', 'duedate', $name, array('optional' => true)); $mform->addHelpButton('duedate', 'duedate', 'forum'); $name = get_string('cutoffdate', 'forum'); $mform->addElement('date_time_selector', 'cutoffdate', $name, array('optional' => true)); $mform->addHelpButton('cutoffdate', 'cutoffdate', 'forum'); // Attachments and word count. $mform->addElement('header', 'attachmentswordcounthdr', get_string('attachmentswordcount', 'forum')); $choices = get_max_upload_sizes($CFG->maxbytes, $COURSE->maxbytes, 0, $CFG->forum_maxbytes); $choices[1] = get_string('uploadnotallowed'); $mform->addElement('select', 'maxbytes', get_string('maxattachmentsize', 'forum'), $choices); $mform->addHelpButton('maxbytes', 'maxattachmentsize', 'forum'); $mform->setDefault('maxbytes', $CFG->forum_maxbytes); $choices = array( 0 => 0, 1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, 6 => 6, 7 => 7, 8 => 8, 9 => 9, 10 => 10, 20 => 20, 50 => 50, 100 => 100 ); $mform->addElement('select', 'maxattachments', get_string('maxattachments', 'forum'), $choices); $mform->addHelpButton('maxattachments', 'maxattachments', 'forum'); $mform->setDefault('maxattachments', $CFG->forum_maxattachments); $mform->addElement('selectyesno', 'displaywordcount', get_string('displaywordcount', 'forum')); $mform->addHelpButton('displaywordcount', 'displaywordcount', 'forum'); $mform->setDefault('displaywordcount', 0); // Subscription and tracking. $mform->addElement('header', 'subscriptionandtrackinghdr', get_string('subscriptionandtracking', 'forum')); $options = forum_get_subscriptionmode_options(); $mform->addElement('select', 'forcesubscribe', get_string('subscriptionmode', 'forum'), $options); $mform->addHelpButton('forcesubscribe', 'subscriptionmode', 'forum'); if (isset($CFG->forum_subscription)) { $defaultforumsubscription = $CFG->forum_subscription; } else { $defaultforumsubscription = FORUM_CHOOSESUBSCRIBE; } $mform->setDefault('forcesubscribe', $defaultforumsubscription); $options = array(); $options[FORUM_TRACKING_OPTIONAL] = get_string('trackingoptional', 'forum'); $options[FORUM_TRACKING_OFF] = get_string('trackingoff', 'forum'); if ($CFG->forum_allowforcedreadtracking) { $options[FORUM_TRACKING_FORCED] = get_string('trackingon', 'forum'); } $mform->addElement('select', 'trackingtype', get_string('trackingtype', 'forum'), $options); $mform->addHelpButton('trackingtype', 'trackingtype', 'forum'); $default = $CFG->forum_trackingtype; if ((!$CFG->forum_allowforcedreadtracking) && ($default == FORUM_TRACKING_FORCED)) { $default = FORUM_TRACKING_OPTIONAL; } $mform->setDefault('trackingtype', $default); if ($CFG->enablerssfeeds && isset($CFG->forum_enablerssfeeds) && $CFG->forum_enablerssfeeds) { //------------------------------------------------------------------------------- $mform->addElement('header', 'rssheader', get_string('rss')); $choices = array(); $choices[0] = get_string('none'); $choices[1] = get_string('discussions', 'forum'); $choices[2] = get_string('posts', 'forum'); $mform->addElement('select', 'rsstype', get_string('rsstype', 'forum'), $choices); $mform->addHelpButton('rsstype', 'rsstype', 'forum'); if (isset($CFG->forum_rsstype)) { $mform->setDefault('rsstype', $CFG->forum_rsstype); } $choices = array(); $choices[0] = '0'; $choices[1] = '1'; $choices[2] = '2'; $choices[3] = '3'; $choices[4] = '4'; $choices[5] = '5'; $choices[10] = '10'; $choices[15] = '15'; $choices[20] = '20'; $choices[25] = '25'; $choices[30] = '30'; $choices[40] = '40'; $choices[50] = '50'; $mform->addElement('select', 'rssarticles', get_string('rssarticles'), $choices); $mform->addHelpButton('rssarticles', 'rssarticles', 'forum'); $mform->hideIf('rssarticles', 'rsstype', 'eq', '0'); if (isset($CFG->forum_rssarticles)) { $mform->setDefault('rssarticles', $CFG->forum_rssarticles); } } $mform->addElement('header', 'discussionlocking', get_string('discussionlockingheader', 'forum')); $options = [ 0 => get_string('discussionlockingdisabled', 'forum'), 1 * DAYSECS => get_string('numday', 'core', 1), 1 * WEEKSECS => get_string('numweek', 'core', 1), 2 * WEEKSECS => get_string('numweeks', 'core', 2), 30 * DAYSECS => get_string('nummonth', 'core', 1), 60 * DAYSECS => get_string('nummonths', 'core', 2), 90 * DAYSECS => get_string('nummonths', 'core', 3), 180 * DAYSECS => get_string('nummonths', 'core', 6), 1 * YEARSECS => get_string('numyear', 'core', 1), ]; $mform->addElement('select', 'lockdiscussionafter', get_string('lockdiscussionafter', 'forum'), $options); $mform->addHelpButton('lockdiscussionafter', 'lockdiscussionafter', 'forum'); $mform->disabledIf('lockdiscussionafter', 'type', 'eq', 'single'); //------------------------------------------------------------------------------- $mform->addElement('header', 'blockafterheader', get_string('blockafter', 'forum')); $options = array(); $options[0] = get_string('blockperioddisabled','forum'); $options[60*60*24] = '1 '.get_string('day'); $options[60*60*24*2] = '2 '.get_string('days'); $options[60*60*24*3] = '3 '.get_string('days'); $options[60*60*24*4] = '4 '.get_string('days'); $options[60*60*24*5] = '5 '.get_string('days'); $options[60*60*24*6] = '6 '.get_string('days'); $options[60*60*24*7] = '1 '.get_string('week'); $mform->addElement('select', 'blockperiod', get_string('blockperiod', 'forum'), $options); $mform->addHelpButton('blockperiod', 'blockperiod', 'forum'); $mform->addElement('text', 'blockafter', get_string('blockafter', 'forum')); $mform->setType('blockafter', PARAM_INT); $mform->setDefault('blockafter', '0'); $mform->addRule('blockafter', null, 'numeric', null, 'client'); $mform->addHelpButton('blockafter', 'blockafter', 'forum'); $mform->hideIf('blockafter', 'blockperiod', 'eq', 0); $mform->addElement('text', 'warnafter', get_string('warnafter', 'forum')); $mform->setType('warnafter', PARAM_INT); $mform->setDefault('warnafter', '0'); $mform->addRule('warnafter', null, 'numeric', null, 'client'); $mform->addHelpButton('warnafter', 'warnafter', 'forum'); $mform->hideIf('warnafter', 'blockperiod', 'eq', 0); //------------------------------------------------------------------------------- // Add the whole forum grading options. $this->add_forum_grade_settings($mform, 'forum'); $this->standard_coursemodule_elements(); //------------------------------------------------------------------------------- // buttons $this->add_action_buttons(); } /** * Add the whole forum grade settings to the mform. * * @param \mform $mform * @param string $itemname */ private function add_forum_grade_settings($mform, string $itemname) { global $COURSE; $component = "mod_{$this->_modname}"; $defaultgradingvalue = 0; $itemnumber = component_gradeitems::get_itemnumber_from_itemname($component, $itemname); $gradefieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'grade'); $gradecatfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'gradecat'); $gradepassfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'gradepass'); // The advancedgradingmethod is different in that it is suffixed with an area name... which is not the // itemnumber. $methodfieldname = "advancedgradingmethod_{$itemname}"; $headername = "{$gradefieldname}_header"; $mform->addElement('header', $headername, get_string("grade_{$itemname}_header", $component)); $isupdate = !empty($this->_cm); $gradeoptions = [ 'isupdate' => $isupdate, 'currentgrade' => false, 'hasgrades' => false, 'canrescale' => false, 'useratings' => false, ]; if ($isupdate) { $gradeitem = grade_item::fetch([ 'itemtype' => 'mod', 'itemmodule' => $this->_cm->modname, 'iteminstance' => $this->_cm->instance, 'itemnumber' => $itemnumber, 'courseid' => $COURSE->id, ]); if ($gradeitem) { $gradeoptions['currentgrade'] = $gradeitem->grademax; $gradeoptions['currentgradetype'] = $gradeitem->gradetype; $gradeoptions['currentscaleid'] = $gradeitem->scaleid; $gradeoptions['hasgrades'] = $gradeitem->has_grades(); } } $mform->addElement( 'modgrade', $gradefieldname, get_string("{$gradefieldname}_title", $component), $gradeoptions ); $mform->addHelpButton($gradefieldname, 'modgrade', 'grades'); $mform->setDefault($gradefieldname, $defaultgradingvalue); if (!empty($this->current->_advancedgradingdata['methods']) && !empty($this->current->_advancedgradingdata['areas'])) { $areadata = $this->current->_advancedgradingdata['areas'][$itemname]; $mform->addElement( 'select', $methodfieldname, get_string('gradingmethod', 'core_grading'), $this->current->_advancedgradingdata['methods'] ); $mform->addHelpButton($methodfieldname, 'gradingmethod', 'core_grading'); $mform->hideIf($methodfieldname, "{$gradefieldname}[modgrade_type]", 'eq', 'none'); } // Grade category. $mform->addElement( 'select', $gradecatfieldname, get_string('gradecategoryonmodform', 'grades'), grade_get_categories_menu($COURSE->id, $this->_outcomesused) ); $mform->addHelpButton($gradecatfieldname, 'gradecategoryonmodform', 'grades'); $mform->hideIf($gradecatfieldname, "{$gradefieldname}[modgrade_type]", 'eq', 'none'); // Grade to pass. $mform->addElement('text', $gradepassfieldname, get_string('gradepass', 'grades')); $mform->addHelpButton($gradepassfieldname, 'gradepass', 'grades'); $mform->setDefault($gradepassfieldname, ''); $mform->setType($gradepassfieldname, PARAM_RAW); $mform->hideIf($gradepassfieldname, "{$gradefieldname}[modgrade_type]", 'eq', 'none'); $mform->addElement('selectyesno', 'grade_forum_notify', get_string('sendstudentnotificationsdefault', 'forum')); $mform->addHelpButton('grade_forum_notify', 'sendstudentnotificationsdefault', 'forum'); $mform->hideIf('grade_forum_notify', "{$gradefieldname}[modgrade_type]", 'eq', 'none'); } function definition_after_data() { parent::definition_after_data(); $mform =& $this->_form; $type =& $mform->getElement('type'); $typevalue = $mform->getElementValue('type'); //we don't want to have these appear as possible selections in the form but //we want the form to display them if they are set. if ($typevalue[0]=='news') { $type->addOption(get_string('namenews', 'forum'), 'news'); $mform->addHelpButton('type', 'namenews', 'forum'); $type->freeze(); $type->setPersistantFreeze(true); } if ($typevalue[0]=='social') { $type->addOption(get_string('namesocial', 'forum'), 'social'); $type->freeze(); $type->setPersistantFreeze(true); } } public function validation($data, $files) { $errors = parent::validation($data, $files); if ($data['type'] === 'single' && $data['groupmode'] == SEPARATEGROUPS) { $errors['type'] = get_string('cannotusesingletopicandseperategroups', 'forum'); $errors['groupmode'] = get_string('cannotuseseperategroupsandsingletopic', 'forum'); } if ($data['duedate'] && $data['cutoffdate']) { if ($data['duedate'] > $data['cutoffdate']) { $errors['cutoffdate'] = get_string('cutoffdatevalidation', 'forum'); } } $this->validation_forum_grade($data, $files, $errors); return $errors; } /** * Handle definition after data for grade settings. * * @param array $data * @param array $files * @param array $errors */ private function validation_forum_grade(array $data, array $files, array $errors) { global $COURSE; $mform =& $this->_form; $component = "mod_forum"; $itemname = 'forum'; $itemnumber = component_gradeitems::get_itemnumber_from_itemname($component, $itemname); $gradefieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'grade'); $gradepassfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'grade'); $gradeitem = grade_item::fetch([ 'itemtype' => 'mod', 'itemmodule' => $data['modulename'], 'iteminstance' => $data['instance'], 'itemnumber' => $itemnumber, 'courseid' => $COURSE->id, ]); if ($mform->elementExists('cmidnumber') && $this->_cm) { if (!grade_verify_idnumber($data['cmidnumber'], $COURSE->id, $gradeitem, $this->_cm)) { $errors['cmidnumber'] = get_string('idnumbertaken'); } } // Check that the grade pass is a valid number. $gradepassvalid = false; if (isset($data[$gradepassfieldname])) { if (unformat_float($data[$gradepassfieldname], true) === false) { $errors[$gradepassfieldname] = get_string('err_numeric', 'form'); } else { $gradepassvalid = true; } } // Grade to pass: ensure that the grade to pass is valid for points and scales. // If we are working with a scale, convert into a positive number for validation. if ($gradepassvalid && isset($data[$gradepassfieldname]) && (!empty($data[$gradefieldname]))) { $grade = $data[$gradefieldname]; if (unformat_float($data[$gradepassfieldname]) > $grade) { $errors[$gradepassfieldname] = get_string('gradepassgreaterthangrade', 'grades', $grade); } } } public function data_preprocessing(&$defaultvalues) { parent::data_preprocessing($defaultvalues); $suffix = $this->get_suffix(); $completiondiscussionsenabledel = 'completiondiscussionsenabled' . $suffix; $completiondiscussionsel = 'completiondiscussions' . $suffix; $completionrepliesenabledel = 'completionrepliesenabled' . $suffix; $completionrepliesel = 'completionreplies' . $suffix; $completionpostsel = 'completionposts' . $suffix; $completionpostsenabledel = 'completionpostsenabled' . $suffix; // Set up the completion checkboxes which aren't part of standard data. // We also make the default value (if you turn on the checkbox) for those // numbers to be 1, this will not apply unless checkbox is ticked. $defaultvalues[$completiondiscussionsenabledel] = !empty($defaultvalues[$completiondiscussionsel]) ? 1 : 0; if (empty($defaultvalues[$completiondiscussionsel])) { $defaultvalues[$completiondiscussionsel] = 1; } $defaultvalues[$completionrepliesenabledel] = !empty($defaultvalues[$completionrepliesel]) ? 1 : 0; if (empty($defaultvalues[$completionrepliesel])) { $defaultvalues[$completionrepliesel] = 1; } // Tick by default if Add mode or if completion posts settings is set to 1 or more. if (empty($this->_instance) || !empty($defaultvalues[$completionpostsel])) { $defaultvalues[$completionpostsenabledel] = 1; } else { $defaultvalues[$completionpostsenabledel] = 0; } if (empty($defaultvalues[$completionpostsel])) { $defaultvalues[$completionpostsel] = 1; } } /** * Add custom completion rules. * * @return array Array of string IDs of added items, empty array if none */ public function add_completion_rules() { $mform = $this->_form; $suffix = $this->get_suffix(); $group = []; $completionpostsenabledel = 'completionpostsenabled' . $suffix; $group[] =& $mform->createElement('checkbox', $completionpostsenabledel, '', get_string('completionposts', 'forum')); $completionpostsel = 'completionposts' . $suffix; $group[] =& $mform->createElement('text', $completionpostsel, '', ['size' => 3]); $mform->setType($completionpostsel, PARAM_INT); $completionpostsgroupel = 'completionpostsgroup' . $suffix; $mform->addGroup($group, $completionpostsgroupel, '', ' ', false); $mform->hideIf($completionpostsel, $completionpostsenabledel, 'notchecked'); $group = []; $completiondiscussionsenabledel = 'completiondiscussionsenabled' . $suffix; $group[] =& $mform->createElement( 'checkbox', $completiondiscussionsenabledel, '', get_string('completiondiscussions', 'forum') ); $completiondiscussionsel = 'completiondiscussions' . $suffix; $group[] =& $mform->createElement('text', $completiondiscussionsel, '', ['size' => 3]); $mform->setType($completiondiscussionsel, PARAM_INT); $completiondiscussionsgroupel = 'completiondiscussionsgroup' . $suffix; $mform->addGroup($group, $completiondiscussionsgroupel, '', ' ', false); $mform->hideIf($completiondiscussionsel, $completiondiscussionsenabledel, 'notchecked'); $group = []; $completionrepliesenabledel = 'completionrepliesenabled' . $suffix; $group[] =& $mform->createElement('checkbox', $completionrepliesenabledel, '', get_string('completionreplies', 'forum')); $completionrepliesel = 'completionreplies' . $suffix; $group[] =& $mform->createElement('text', $completionrepliesel, '', ['size' => 3]); $mform->setType($completionrepliesel, PARAM_INT); $completionrepliesgroupel = 'completionrepliesgroup' . $suffix; $mform->addGroup($group, $completionrepliesgroupel, '', ' ', false); $mform->hideIf($completionrepliesel, $completionrepliesenabledel, 'notchecked'); return [$completiondiscussionsgroupel, $completionrepliesgroupel, $completionpostsgroupel]; } public function completion_rule_enabled($data) { $suffix = $this->get_suffix(); return (!empty($data['completiondiscussionsenabled' . $suffix]) && $data['completiondiscussions' . $suffix] != 0) || (!empty($data['completionrepliesenabled' . $suffix]) && $data['completionreplies' . $suffix] != 0) || (!empty($data['completionpostsenabled' . $suffix]) && $data['completionposts' . $suffix] != 0); } /** * Return submitted data if properly submitted or returns NULL if validation fails or * if there is no submitted data. * * Do not override this method, override data_postprocessing() instead. * * @return object submitted data; NULL if not valid or not submitted or cancelled */ public function get_data() { $data = parent::get_data(); if ($data) { $itemname = 'forum'; $component = 'mod_forum'; $gradepassfieldname = component_gradeitems::get_field_name_for_itemname($component, $itemname, 'gradepass'); // Convert the grade pass value - we may be using a language which uses commas, // rather than decimal points, in numbers. These need to be converted so that // they can be added to the DB. if (isset($data->{$gradepassfieldname})) { $data->{$gradepassfieldname} = unformat_float($data->{$gradepassfieldname}); } } return $data; } /** * Allows module to modify the data returned by form get_data(). * This method is also called in the bulk activity completion form. * * Only available on moodleform_mod. * * @param stdClass $data the form data to be modified. */ public function data_postprocessing($data) { parent::data_postprocessing($data); // Turn off completion settings if the checkboxes aren't ticked. if (!empty($data->completionunlocked)) { $suffix = $this->get_suffix(); $completion = $data->{'completion' . $suffix}; $autocompletion = !empty($completion) && $completion == COMPLETION_TRACKING_AUTOMATIC; if (empty($data->{'completiondiscussionsenabled' . $suffix}) || !$autocompletion) { $data->{'completiondiscussions' . $suffix} = 0; } if (empty($data->{'completionrepliesenabled' . $suffix}) || !$autocompletion) { $data->{'completionreplies' . $suffix} = 0; } if (empty($data->{'completionpostsenabled' . $suffix}) || !$autocompletion) { $data->{'completionposts' . $suffix} = 0; } } } } PK-B\<44externallib.phpnu[PK-B\Fo]] sstyles.cssnu[PK-B\q post.phpnu[PK-B\h#// 6erenderer.phpnu[PK-B\Ƒ udb/tag.phpnu[PK-B\^_ _ 2db/services.phpnu[PK-B\gйdb/messageinbound_handlers.phpnu[PK-B\7db/subplugins.jsonnu[PK-B\; db/log.phpnu[PK-B\ (db/tasks.phpnu[PK-B\// db/access.phpnu[PK-B\uu:db/upgrade.phpnu[PK-B\"Du=u=db/install.xmlnu[PK-B\@ll Cdb/events.phpnu[PK-B\(IJdb/messages.phpnu[PK-B\6@ [Pdb/caches.phpnu[PK-B\f(.Vlang/en/forum.phpnu[PK-B\r;@@lang/en/deprecated.txtnu[PK-B\#e11)6Abackup/moodle2/restore_forum_stepslib.phpnu[PK-B\A#!#!(rbackup/moodle2/backup_forum_stepslib.phpnu[PK-B\xx+backup/moodle2/backup_forum_settingslib.phpnu[PK-B\#—4backup/moodle2/restore_forum_activity_task.class.phpnu[PK-B\2 3backup/moodle2/backup_forum_activity_task.class.phpnu[PK-B\;backup/moodle1/lib.phpnu[PK-B\4))Yview.phpnu[PK-B\j'@'@ rsslib.phpnu[PK-B\AdXX@pix/i/pinned.svgnu[PK-B\aCpix/hide-grader-panel.svgnu[PK-B\5q7Spix/show-grader-panel-rtl.svgnu[PK-B\e[pix/show-grader-panel.svgnu[PK-B\!sII3cpix/hide-grader-panel.pngnu[PK-B\s%epix/no-search-results.pngnu[PK-B\T+Gpix/no-posts.svgnu[PK-B\Ў}++_pix/no-search-results.svgnu[PK-B\]pix/t/unsubscribed.svgnu[PK-B\$pix/t/selected.svgnu[PK-B\ ZZpix/t/star.svgnu[PK-B\oKKpix/t/subscribed.svgnu[PK-B\qjSS$pix/show-grader-panel-rtl.pngnu[PK-B\KeeĘpix/no-posts.pngnu[PK-B\Q_ddpix/show-grader-panel.pngnu[PK-B\.pix/monologo.svgnu[PK-B\^lib.phpnu[PK-B\)t, , version.phpnu[PK-B\0q00  export.phpnu[PK-B\'C;>> l search.phpnu[PK-B\8te e }L markposts.phpnu[PK-B\gE%%"Y classes/grades/forum_gradeitem.phpnu[PK-B\! '!88 classes/grades/gradeitems.phpnu[PK-B\%GG classes/form/export_form.phpnu[PK-B\Sp@tt classes/privacy/provider.phpnu[PK-B\RL#ځ classes/privacy/subcontext_info.phpnu[PK-B\X2C classes/dates.phpnu[PK-B\*{D{D+ classes/local/renderers/discussion_list.phpnu[PK-B\'AA! classes/local/renderers/posts.phpnu[PK-B\PmII&g classes/local/renderers/discussion.phpnu[PK-B\'Rpp 6 classes/local/exporters/post.phpnu[PK-B\}i^j!j!!٧ classes/local/exporters/forum.phpnu[PK-B\. classes/local/exporters/discussion_summary.phpnu[PK-B\F1! classes/local/exporters/posts.phpnu[PK-B\O. . !classes/local/exporters/group.phpnu[PK-B\^,,&Iclasses/local/exporters/discussion.phpnu[PK-B\z*""05<classes/local/exporters/discussion_summaries.phpnu[PK-B\iUeOO"Uclasses/local/exporters/author.phpnu[PK-B\?.Xuclasses/local/builders/exported_discussion.phpnu[PK-B\D 008classes/local/builders/exported_discussion_summaries.phpnu[PK-B\$LGZGZ)classes/local/builders/exported_posts.phpnu[PK-B\Lmz  *Wclasses/local/data_mappers/legacy/post.phpnu[PK-B\EtSS+#classes/local/data_mappers/legacy/forum.phpnu[PK-B\"9 0}2classes/local/data_mappers/legacy/discussion.phpnu[PK-B\غ_=  ,=classes/local/data_mappers/legacy/author.phpnu[PK-B\-.UUHclasses/local/vaults/post.phpnu[PK-B\W(classes/local/vaults/post_attachment.phpnu[PK-B\&`classes/local/vaults/forum.phpnu[PK-B\SI#555classes/local/vaults/post_read_receipt_collection.phpnu[PK-B\H V V(]classes/local/vaults/discussion_list.phpnu[PK-B\ꥲ31classes/local/vaults/preprocessors/extract_user.phpnu[PK-B\j67;classes/local/vaults/preprocessors/extract_context.phpnu[PK-B\\%5mBclasses/local/vaults/preprocessors/extract_record.phpnu[PK-B\JCaa'Kclasses/local/vaults/db_table_vault.phpnu[PK-B\ R`GG#[cclasses/local/vaults/discussion.phpnu[PK-B\Fvzclasses/local/vaults/author.phpnu[PK-B\AA(classes/local/container.phpnu[PK-B\Ә!&!&classes/local/entities/post.phpnu[PK-B\#GG $classes/local/entities/forum.phpnu[PK-B\D D 7 classes/local/entities/post_read_receipt_collection.phpnu[PK-B\_ -<classes/local/entities/discussion_summary.phpnu[PK-B\SS!{"classes/local/entities/sorter.phpnu[PK-B\##%7classes/local/entities/discussion.phpnu[PK-B\#(CC!{Zclasses/local/entities/author.phpnu[PK-B\#pclasses/local/factories/builder.phpnu[PK-B\CJ">n>n$Vclasses/local/factories/renderer.phpnu[PK-B\iCv"""classes/local/factories/entity.phpnu[PK-B\]K!Pclasses/local/factories/vault.phpnu[PK-B\,,$T'classes/local/factories/exporter.phpnu[PK-B\;K .Sclasses/local/factories/legacy_data_mapper.phpnu[PK-B\SY #$`classes/local/factories/manager.phpnu[PK-B\CAAmjclasses/local/factories/url.phpnu[PK-B\\-SeSe%classes/local/managers/capability.phpnu[PK-B\z)8classes/potential_subscriber_selector.phpnu[PK-B\*#'classes/prune_form.phpnu[PK-B\rP (/classes/existing_subscriber_selector.phpnu[PK-B\ 55):classes/message/inbound/reply_handler.phpnu[PK-B\GBGBqclasses/post_form.phpnu[PK-B\OPq q classes/h5p/canedit.phpnu[PK-B\ʊxYclasses/observer.phpnu[PK-B\2 RR"classes/plugininfo/forumreport.phpnu[PK-B\~))Fclasses/search/post.phpnu[PK-B\Ôxx classes/search/activity.phpnu[PK-B\c c $Cclasses/subscriber_selector_base.phpnu[PK-B\/G/Gclasses/output/forum_post.phpnu[PK-B\HVV"vbclasses/output/forum_actionbar.phpnu[PK-B\&!!)vclasses/output/subscription_actionbar.phpnu[PK-B\O !classes/output/email/renderer.phpnu[PK-B\qcc+classes/output/email/renderer_textemail.phpnu[PK-B\hN[#qclasses/output/forum_post_email.phpnu[PK-B\h틝,hclasses/output/emaildigestbasic/renderer.phpnu[PK-B\66aclasses/output/emaildigestbasic/renderer_textemail.phpnu[PK-B\ &fg "ضclasses/output/big_search_form.phpnu[PK-B\f $classes/output/quick_search_form.phpnu[PK-B\kNK+classes/output/emaildigestfull/renderer.phpnu[PK-B\q5classes/output/emaildigestfull/renderer_textemail.phpnu[PK-B\, -Wclasses/output/courseformat/activitybadge.phpnu[PK-B\ 661jclasses/event/discussion_subscription_created.phpnu[PK-B\LzM M )classes/event/discussion_lock_updated.phpnu[PK-B\Jk' ' &classes/event/subscription_created.phpnu[PK-B\1R{ $$"classes/event/discussion_deleted.phpnu[PK-B\v %.classes/event/discussion_unpinned.phpnu[PK-B\q(Q !;classes/event/course_searched.phpnu[PK-B\AHHGclasses/event/post_updated.phpnu[PK-B\d #uXclasses/event/discussion_viewed.phpnu[PK-B\ Ucclasses/event/post_deleted.phpnu[PK-B\#g g &sclasses/event/readtracking_enabled.phpnu[PK-B\[-w w $qclasses/event/discussion_updated.phpnu[PK-B\]N'  $<classes/event/discussion_created.phpnu[PK-B\fd>>1classes/event/discussion_subscription_deleted.phpnu[PK-B\o 66&classes/event/course_module_viewed.phpnu[PK-B\ʌc #:classes/event/discussion_pinned.phpnu[PK-B\ j j '4classes/event/readtracking_disabled.phpnu[PK-B\~HHclasses/event/post_created.phpnu[PK-B\rU $classes/event/subscribers_viewed.phpnu[PK-B\}- - &classes/event/subscription_deleted.phpnu[PK-B\% $+classes/event/user_report_viewed.phpnu[PK-B\!;  %Rclasses/event/assessable_uploaded.phpnu[PK-B\M +classes/event/subscription_mode_updated.phpnu[PK-B\Un "classes/event/discussion_moved.phpnu[PK-B\BA;``4+classes/event/course_module_instance_list_viewed.phpnu[PK-B\a`OO0classes/task/cron_task.phpnu[PK-B\8YVV"classes/task/send_user_digests.phpnu[PK-B\b.Y.Y(classes/task/send_user_notifications.phpnu[PK-B\L59uuo1classes/subscriptions.phpnu[PK-B\vt%(-classes/completion/custom_completion.phpnu[PK-B\qr6++/classes/analytics/indicator/cognitive_depth.phpnu[PK-B\OH  -classes/analytics/indicator/activity_base.phpnu[PK-B\/.classes/analytics/indicator/social_breadth.phpnu[PK-B\fN settracking.phpnu[PK-B\f0templates/form-user-selector-suggestion.mustachenu[PK-B\yl#templates/discussion_times.mustachenu[PK-B\vLЂ1templates/frontpage_news_discussion_list.mustachenu[PK-B\g JJ. templates/discussion_favourite_toggle.mustachenu[PK-B\-ttemplates/discussion_settings_drawer.mustachenu[PK-B\ &templates/grades/grade_button.mustachenu[PK-B\o+"templates/grades/view_grade_button.mustachenu[PK-B\v{'51O(templates/grades/grader/discussion/posts.mustachenu[PK-B\DkLL6`:templates/grades/grader/discussion/post_modal.mustachenu[PK-B\7@templates/forum_post_emaildigestfull_htmlemail.mustachenu[PK-B\2:dd"\Otemplates/discussion_list.mustachenu[PK-B\al!templates/setting_switch.mustachenu[PK-B\F||'ɹtemplates/blog_discussion_list.mustachenu[PK-B\;`3templates/local/grades/local/grader/status.mustachenu[PK-B\w| | 8templates/local/grades/local/grader/user_picker.mustachenu[PK-B\*"*"Gtemplates/local/grades/local/grader/module_content_placeholder.mustachenu[PK-B\}4templates/local/grades/local/grader/content.mustachenu[PK-B\fDtemplates/local/grades/local/grader/user_picker_placeholder.mustachenu[PK-B\ DYtemplates/local/grades/local/grader/user_picker/user_search.mustachenu[PK-B\_)OO= templates/local/grades/local/grader/user_picker/user.mustachenu[PK-B\/7[templates/local/grades/local/grader/navigation.mustachenu[PK-B\499>e$templates/local/grades/local/grader/grade_placeholder.mustachenu[PK-B\4nRR?^^templates/local/grades/local/grader/status_placeholder.mustachenu[PK-B\:C?ctemplates/local/grades/local/grader/gradingpanel/error.mustachenu[PK-B\0334lhtemplates/local/grades/local/grader/grading.mustachenu[PK-B\iǥN{ { *templates/local/grades/view_grade.mustachenu[PK-B\0  &؎templates/local/grades/grader.mustachenu[PK-B\vB(=templates/qanda_discussion_list.mustachenu[PK-B\L͐$ntemplates/forum_action_menu.mustachenu[PK-B\]zhh7Rtemplates/forum_post_emaildigestfull_textemail.mustachenu[PK-B\@ "!templates/forum_actionbar.mustachenu[PK-B\jh)Dtemplates/forum_discussion_posts.mustachenu[PK-B\T8templates/forum_post_emaildigestbasic_htmlemail.mustachenu[PK-B\č"templates/inpage_reply_v2.mustachenu[PK-B\F  1templates/discussion_subscription_toggle.mustachenu[PK-B\4LZ4templates/forum_discussion_favourite_toggle.mustachenu[PK-B\J  8templates/forum_discussion_nested_v2_post_reply.mustachenu[PK-B\"templates/settings_header.mustachenu[PK-B\'ʷ -templates/forum_post_email_textemail.mustachenu[PK-B\1mm$ templates/quick_search_form.mustachenu[PK-B\(888templates/forum_post_emaildigestbasic_textemail.mustachenu[PK-B\/<;;(ktemplates/discussion_pin_toggle.mustachenu[PK-B\uN88"templates/big_search_form.mustachenu[PK-B\>templates/mark_as_read.mustachenu[PK-B\8Ctemplates/forum_post_subject_with_context_links.mustachenu[PK-B\8It0Ltemplates/forum_discussion_nested_posts.mustachenu[PK-B\3'uu1Qtemplates/forum_posts_with_context_links.mustachenu[PK-B\ -Vtemplates/forum_discussion_nested_v2.mustachenu[PK-B\@2Hatemplates/forum_post_email_htmlemail_body.mustachenu[PK-B\0mJJ(ytemplates/forum_discussion_post.mustachenu[PK-B\ A`3;templates/frontpage_social_discussion_list.mustachenu[PK-B\o)7templates/single_discussion_list.mustachenu[PK-B\ templates/inpage_reply.mustachenu[PK-B\9/{{2vtemplates/forum_discussion_threaded_posts.mustachenu[PK-B\ ,Stemplates/forum_subscription_action.mustachenu[PK-B\F>  'templates/forum_search_results.mustachenu[PK-B\3templates/discussion_settings_body_content.mustachenu[PK-B\ ᳌*Y templates/settings_drawer_trigger.mustachenu[PK-B\MM8?templates/forum_discussion_nested_v2_first_post.mustachenu[PK-B\j3/i^templates/forum_discussion_nested_post.mustachenu[PK-B\F׎1ctemplates/forum_new_discussion_actionbar.mustachenu[PK-B\w'ktemplates/news_discussion_list.mustachenu[PK-B\Y3ptemplates/forum_discussion_nested_v2_posts.mustachenu[PK-B\1A)utemplates/discussion_lock_toggle.mustachenu[PK-B\;#{templates/forum_discussion.mustachenu[PK-B\1Otemplates/forum_discussion_threaded_post.mustachenu[PK-B\V. . )Ltemplates/social_discussion_list.mustachenu[PK-B\LPݻ-Ӛtemplates/forum_post_email_htmlemail.mustachenu[PK-B\so&o& upgrade.txtnu[PK-B\&vv locallib.phpnu[PK-B\,++ Jsubscribe.phpnu[PK-B\o %% wsubscribers.phpnu[PK-B\>[oOoO pindex.phpnu[PK-B\ij((amd/src/discussion.jsnu[PK-B\e$ amd/src/grades/expandconversation.jsnu[PK-B\jܐrramd/src/grades/grader.jsnu[PK-B\nC":7amd/src/grades/grader/selectors.jsnu[PK-B\P 'Z#Z#<amd/src/inpage_reply.jsnu[PK-B\*0;`amd/src/local/grades/local/grader/user_picker.jsnu[PK-B\dñ:8zamd/src/local/grades/local/grader/user_picker/selectors.jsnu[PK-B\U[8^ ^ .Samd/src/local/grades/local/grader/selectors.jsnu[PK-B\xA1amd/src/local/grades/local/grader/gradingpanel.jsnu[PK-B\Q?NNamd/src/local/grades/grader.jsnu[PK-B\xttMamd/src/local/layouts.jsnu[PK-B\X֮==" amd/src/local/layout/fullscreen.jsnu[PK-B\+amd/src/discussion_list.jsnu[PK-B\^!_; ; amd/src/subscription_toggle.jsnu[PK-B\5F F 6"amd/src/pin_toggle.jsnu[PK-B\6-amd/src/repository.jsnu[PK-B\b=__Damd/src/lock_toggle.jsnu[PK-B\_466>Mamd/src/posts_list.jsnu[PK-B\6[ [ ^amd/src/selectors.jsnu[PK-B\གྷ>>Xlamd/src/discussion_nested_v2.jsnu[PK-B\iV amd/src/form-user-selector.jsnu[PK-B\mJamd/src/forum_events.jsnu[PK-B\N>? Ǻamd/src/favourite_toggle.jsnu[PK-B\'%kkamd/build/pin_toggle.min.js.mapnu[PK-B\EJ$($(%qamd/build/discussion_nested_v2.min.jsnu[PK-B\p¯.amd/build/grades/expandconversation.min.js.mapnu[PK-B\E| *6amd/build/grades/expandconversation.min.jsnu[PK-B\<ꚧ}},s$amd/build/grades/grader/selectors.min.js.mapnu[PK-B\-M(L+amd/build/grades/grader/selectors.min.jsnu[PK-B\B#-amd/build/grades/grader.min.jsnu[PK-B\fX@++"Bamd/build/grades/grader.min.js.mapnu[PK-B\dnZ Z namd/build/repository.min.jsnu[PK-B\${xamd/build/subscription_toggle.min.jsnu[PK-B\( 33!i}amd/build/inpage_reply.min.js.mapnu[PK-B\ȴ66ΰamd/build/inpage_reply.min.jsnu[PK-B\np2  Qamd/build/lock_toggle.min.js.mapnu[PK-B\k448 amd/build/local/grades/local/grader/selectors.min.js.mapnu[PK-B\\7amd/build/local/grades/local/grader/gradingpanel.min.jsnu[PK-B\n * * ;amd/build/local/grades/local/grader/gradingpanel.min.js.mapnu[PK-B\#Dzamd/build/local/grades/local/grader/user_picker/selectors.min.js.mapnu[PK-B\wS8@amd/build/local/grades/local/grader/user_picker/selectors.min.jsnu[PK-B\Ҹ8}##: amd/build/local/grades/local/grader/user_picker.min.js.mapnu[PK-B\qX>>4} amd/build/local/grades/local/grader/selectors.min.jsnu[PK-B\S- 6# amd/build/local/grades/local/grader/user_picker.min.jsnu[PK-B\--$-0 amd/build/local/grades/grader.min.jsnu[PK-B\Bnn(X^ amd/build/local/grades/grader.min.js.mapnu[PK-B\zw-(g amd/build/local/layout/fullscreen.min.jsnu[PK-B\d'',C amd/build/local/layout/fullscreen.min.js.mapnu[PK-B\s$__"b!amd/build/local/layouts.min.js.mapnu[PK-B\#@@!amd/build/local/layouts.min.jsnu[PK-B\3+9QQ !amd/build/pin_toggle.min.jsnu[PK-B\UUyy#=!amd/build/form-user-selector.min.jsnu[PK-B\Ct}`` !amd/build/posts_list.min.js.mapnu[PK-B\"qv**-!amd/build/discussion.min.jsnu[PK-B\GT%-@!amd/build/favourite_toggle.min.js.mapnu[PK-B\+FO!amd/build/repository.min.js.mapnu[PK-B\vm!amd/build/selectors.min.js.mapnu[PK-B\D/::!amd/build/discussion.min.js.mapnu[PK-B\WC͇!&!amd/build/favourite_toggle.min.jsnu[PK-B\0nn!amd/build/selectors.min.jsnu[PK-B\wf,v$v$$!amd/build/discussion_list.min.js.mapnu[PK-B\ [l~~!amd/build/forum_events.min.jsnu[PK-B\{!K!amd/build/forum_events.min.js.mapnu[PK-B\qv>@{!amd/build/posts_list.min.jsnu[PK-B\ 99(!amd/build/subscription_toggle.min.js.mapnu[PK-B\%pff& "amd/build/lock_toggle.min.jsnu[PK-B\W "amd/build/discussion_list.min.jsnu[PK-B\ɰ'"amd/build/form-user-selector.min.js.mapnu[PK-B\ȫs WW)-"amd/build/discussion_nested_v2.min.js.mapnu[PK-B\Qf!0 0 "unsubscribeall.phpnu[PK-B\a\"tests/output_email_test.phpnu[PK-B\ ZUU("tests/vaults_post_test.phpnu[PK-B\S"LJ#tests/managers_capability_test.phpnu[PK-B\bE"i$tests/backup/restore_date_test.phpnu[PK-B\ d00z$tests/generator/lib.phpnu[PK-B\q  -٫$tests/generator/behat_mod_forum_generator.phpnu[PK-B\   A$tests/subscriptions_test.phpnu[PK-B\J(Y(Yn%tests/privacy/provider_test.phpnu[PK-B\4""'tests/entities_discussion_test.phpnu[PK-B\ww%;'tests/vaults_discussion_list_test.phpnu[PK-B\l /'tests/vaults_author_test.phpnu[PK-B\w(('tests/exporters_author_test.phpnu[PK-B\f!!#'tests/exporters_discussion_test.phpnu[PK-B\<'tests/lib_test.phpnu[PK-B\|g g *@*tests/entities_discussion_summary_test.phpnu[PK-B\&8Y 66%*tests/vaults_post_attachment_test.phpnu[PK-B\.͇^{{*tests/maildigest_test.phpnu[PK-B\G-/ / \w+tests/entities_sorter_test.phpnu[PK-B\ֈف+tests/entities_author_test.phpnu[PK-B\tz""+tests/mail_group_test.phpnu[PK-B\-,+tests/message/inbound/reply_handler_test.phpnu[PK-B\$GGF+tests/h5p/canedit_test.phpnu[PK-B\Atq q +tests/locallib_test.phpnu[PK-B\U,(,( +tests/custom_completion_test.phpnu[PK-B\Iww) ,tests/backup_forum_activity_task_test.phpnu[PK-B\QWW),tests/search/search_test.phpnu[PK-B\v&D,tests/portfolio_caller_test.phpnu[PK-B\ C,tests/local_container_test.phpnu[PK-B\} 6,tests/dates_test.phpnu[PK-B\r!e!e,tests/externallib_test.phpnu[PK-B\qj}''/tests/private_replies_test.phpnu[PK-B\E4DM=/tests/generator_trait.phpnu[PK-B\TssU/tests/cron_trait.phpnu[PK-B\ ݹݹ@j/tests/event/events_test.phpnu[PK-B\b+h$1tests/task/send_user_notifications_test.phpnu[PK-B\&RRaA1tests/mail_test.phpnu[PK-B\0  52tests/exporters_forum_test.phpnu[PK-B\D-- KB2tests/vaults_discussion_test.phpnu[PK-B\%2U2tests/vaults_post_read_receipt_collection_test.phpnu[PK-B\Udy,y,$ e2tests/grade/forum_gradeitem_test.phpnu[PK-B\$ $ ؑ2tests/grade/gradeitems_test.phpnu[PK-B\ZH--K2tests/generator_test.phpnu[PK-B\`oxWxW&2tests/builders_exported_posts_test.phpnu[PK-B\Or> > #3tests/entities_post_test.phpnu[PK-B\?n/3tests/vaults_forum_test.phpnu[PK-B\+Y 'F3tests/behat/no_groups_in_course.featurenu[PK-B\3o.T3tests/behat/separate_group_grade_forum.featurenu[PK-B\=G G e3tests/behat/add_forum.featurenu[PK-B\iJJo o $Wp3tests/behat/track_read_posts.featurenu[PK-B\<3[h 23tests/behat/forum_subscriptions_management.featurenu[PK-B\y > > %3tests/behat/edit_post_teacher.featurenu[PK-B\{ N88.3tests/behat/separate_group_discussions.featurenu[PK-B\}-%3tests/behat/timed_discussions.featurenu[PK-B\--&3tests/behat/discussion_display.featurenu[PK-B\ǺQQU3tests/behat/behat_mod_forum.phpnu[PK-B\k .]P4tests/behat/forum_activity_breadcrumbs.featurenu[PK-B\)QY4 4 %{[4tests/behat/edit_post_student.featurenu[PK-B\ˈ#g4tests/behat/discussion_lock.featurenu[PK-B\:\w5iv4tests/behat/discussion_post_threshold_warning.featurenu[PK-B\/5; ; ;ӌ4tests/behat/separate_group_single_group_discussions.featurenu[PK-B\88-y4tests/behat/visible_group_discussions.featurenu[PK-B\֬ܒ@@/y4tests/behat/forum_subscriptions_default.featurenu[PK-B\ C3 3 74tests/behat/groups_in_course_no_groups_in_forum.featurenu[PK-B\1h&& 4tests/behat/forum_export.featurenu[PK-B\Ϥh-5tests/behat/forum_activity_completion.featurenu[PK-B\ *25tests/behat/posts_ordering_general.featurenu[PK-B\v4@5tests/behat/forum_subscriptions_availability.featurenu[PK-B\MVt& & S5tests/behat/edit_tags.featurenu[PK-B\p\GG,Q`5tests/behat/discussion_subscriptions.featurenu[PK-B\F""ç5tests/behat/grade_forum.featurenu[PK-B\  #5tests/behat/advanced_search.featurenu[PK-B\9 #o5tests/behat/private_replies.featurenu[PK-B\mR.5tests/behat/h5p_inline_editing_content.featurenu[PK-B\hP+$6tests/behat/add_forum_inline.featurenu[PK-B\}+ *O6tests/behat/split_forum_discussion.featurenu[PK-B\a )6tests/behat/grade_view_discussion.featurenu[PK-B\%H 0#6tests/behat/forum_subscribers_navigation.featurenu[PK-B\s/6tests/behat/guest_users.featurenu[PK-B\+c.@J6tests/behat/forum_default_rss_settings.featurenu[PK-B\L+N6tests/behat/single_forum_discussion.featurenu[PK-B\NOO V6tests/behat/inpage_reply.featurenu[PK-B\=MH'''^6tests/behat/forum_subscriptions.featurenu[PK-B\QW"3m6tests/behat/forum_subscription_landing_page.featurenu[PK-B\DNv.qq#r6tests/behat/recent_activity.featurenu[PK-B\IA+T6tests/behat/post_to_multiple_groups.featurenu[PK-B\]](\6tests/behat/favourite_discussion.featurenu[PK-B\z"6tests/behat/my_forum_posts.featurenu[PK-B\344)$6tests/behat/discussion_navigation.featurenu[PK-B\ag3 3 '6tests/behat/posts_ordering_blog.featurenu[PK-B\"E,ۊ -;6tests/behat/forum_one_discussion_type.featurenu[PK-B\[v#v#8"6tests/behat/forum_activity_completion_pass_grade.featurenu[PK-B\xk11$7tests/behat/portfolio_export.featurenu[PK-B\ֺSB#7tests/behat/move_discussion.featurenu[PK-B\Q ,7tests/behat/forum_display_word_count.featurenu[PK-B\)6"7tests/behat/forum_subscriptions_mode_behaviour.featurenu[PK-B\Me e 4I*7tests/entities_post_read_receipt_collection_test.phpnu[PK-B\5h57tests/qanda_test.phpnu[PK-B\\ gJ7tests/rsslib_test.phpnu[PK-B\ǵYYYX7tests/exporters_post_test.phpnu[PK-B\'ǽñ7tests/entities_forum_test.phpnu[PK-B\E* 7maildigest.phpnu[PK-B\kMII7deprecatedlib.phpnu[PK-B\J J F7report/summary/renderer.phpnu[PK-B\SSC  7report/summary/db/access.phpnu[PK-B\fǢ9x x .47report/summary/lang/en/forumreport_summary.phpnu[PK-B\V.++ 7report/summary/version.phpnu[PK-B\EE17report/summary/classes/form/dates_filter_form.phpnu[PK-B\::+%8report/summary/classes/privacy/provider.phpnu[PK-B\BEE( 8report/summary/classes/summary_table.phpnu[PK-B\L%k4343)W8report/summary/classes/output/filters.phpnu[PK-B\_ 28report/summary/classes/event/report_downloaded.phpnu[PK-B\e{Ճ .8report/summary/classes/event/report_viewed.phpnu[PK-B\ p  28report/summary/templates/bulk_action_menu.mustachenu[PK-B\U??6%8report/summary/templates/filter_dates_popover.mustachenu[PK-B\_XHH49report/summary/templates/export_link_button.mustachenu[PK-B\SY Y /v9report/summary/templates/filter_groups.mustachenu[PK-B\ghx ).9report/summary/templates/filters.mustachenu[PK-B\rH(09report/summary/templates/report.mustachenu[PK-B\6v~~.l!9report/summary/templates/filter_dates.mustachenu[PK-B\\GH(9report/summary/index.phpnu[PK-B\&1??!C9report/summary/amd/src/filters.jsnu[PK-B\*%%#9report/summary/amd/src/selectors.jsnu[PK-B\lG<,,'m9report/summary/amd/build/filters.min.jsnu[PK-B\y8> > -f9report/summary/amd/build/selectors.min.js.mapnu[PK-B\;Nww)9report/summary/amd/build/selectors.min.jsnu[PK-B\&]]+9report/summary/amd/build/filters.min.js.mapnu[PK-B\)n#kk*:#:report/summary/tests/event/events_test.phpnu[PK-B\E /7:report/summary/tests/behat/bulk_message.featurenu[PK-B\ i#6 F:report/summary/tests/behat/summary_data_access.featurenu[PK-B\Ww  ;mX:report/summary/tests/behat/summary_filter_no_groups.featurenu[PK-B\aF99:e:report/summary/tests/behat/summary_data_post_dates.featurenu[PK-B\;^!^!1t:report/summary/tests/behat/course_summary.featurenu[PK-B\$(j j ;U:report/summary/tests/behat/summary_data_attachments.featurenu[PK-B\>\A*:report/summary/tests/behat/double_enrolments_summary_data.featurenu[PK-B\8V' ' 2:report/summary/tests/behat/private_replies.featurenu[PK-B\bs 3 38(:report/summary/tests/behat/summary_filter_groups.featurenu[PK-B\d77:user.phpnu[PK-B\si88 +;discuss.phpnu[PK-B\?}Z td;settings.phpnu[PK-B\_`` h;mod_form.phpnu[PKޫ;