Merge branch 'MDL-77558-501' of https://github.com/davewoloszyn/moodle into MOODLE_501_STABLE

This commit is contained in:
Huong Nguyen
2026-03-25 14:37:56 +07:00
15 changed files with 252 additions and 19 deletions
@@ -1,10 +1,10 @@
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}
define("mod_forum/grades/expandconversation",["exports","./grader/selectors","mod_forum/repository","core/notification","core/templates","core/modal_cancel","core/modal_events","core/ajax"],(function(_exports,ForumSelectors,_repository,_notification,_templates,_modal_cancel,ModalEvents,_ajax){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 <mathew.solutions>
* @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)}}}))}}));
*/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),_ajax=_interopRequireDefault(_ajax);_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)}}}));document.querySelectorAll(ForumSelectors.showContent).forEach((element=>{element.addEventListener("click",(e=>{const target=e.currentTarget,postId=parseInt(target.dataset.parentPostid,10),discussionId=parseInt(target.dataset.parentDiscussionid,10);markPostsRead([postId],discussionId)}))}))};const markPostsRead=(postIds,discussionId)=>{_ajax.default.call([{methodname:"mod_forum_mark_posts_read",args:{postids:postIds,discussionid:discussionId}}])[0].catch(_notification.exception.exception)}}));
//# sourceMappingURL=expandconversation.min.js.map
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,3 +1,3 @@
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}));
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"]',showContent:".show-content-button"},_exports.default}));
//# sourceMappingURL=selectors.min.js.map
@@ -1 +1 @@
{"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 <http://www.gnu.org/licenses/>.\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 <[email protected]>\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"}
{"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 <http://www.gnu.org/licenses/>.\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 <[email protected]>\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 showContent: '.show-content-button',\n};\n"],"names":["launch","gradableItem","gradableItems","wholeForum","expandConversation","posts","viewGrade","showContent"],"mappings":"mLAsBe,CACXA,OAAQ,+BACRC,aAAc,2BACdC,cAAe,CACXC,WAAY,oCAEhBC,mBAAoB,+BACpBC,MAAO,wBACPC,UAAW,6BACXC,YAAa"}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -26,6 +26,7 @@ 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';
import Ajax from 'core/ajax';
/**
* Find the Node containing the gradable details from the provided node by searching up the tree.
@@ -124,4 +125,31 @@ export const registerEventListeners = (rootNode) => {
}
}
});
// Each parent post can mark its own read state when it is clicked.
const parentContent = document.querySelectorAll(ForumSelectors.showContent);
parentContent.forEach(element => {
element.addEventListener('click', (e) => {
const target = e.currentTarget;
const postId = parseInt(target.dataset.parentPostid, 10);
const discussionId = parseInt(target.dataset.parentDiscussionid, 10);
markPostsRead([postId], discussionId);
});
});
};
/**
* Mark posts as read.
*
* @param {number[]} postIds The posts to mark as read
* @param {number} discussionId The discussion the posts belong to
*/
const markPostsRead = (postIds, discussionId) => {
Ajax.call([{
methodname: 'mod_forum_mark_posts_read',
args: {
postids: postIds,
discussionid: discussionId
},
}])[0].catch(showException.exception);
};
@@ -29,4 +29,5 @@ export default {
expandConversation: '[data-action="view-context"]',
posts: '[data-region="posts"]',
viewGrade: '[data-grade-action="view"]',
showContent: '.show-content-button',
};
@@ -257,6 +257,8 @@ const registerEventListeners = (graderLayout, userPicker, saveGradeFunction, use
e.preventDefault();
graderLayout.close();
// Keep the underlying forum page up-to-date by forcing a refresh.
window.location.reload();
return;
}
+10 -1
View File
@@ -206,7 +206,6 @@ $functions = array(
'ajax' => true,
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
],
'mod_forum_set_forum_tracking' => [
'classname' => 'mod_forum\external\set_forum_tracking',
'methodname' => 'execute',
@@ -215,4 +214,14 @@ $functions = array(
'ajax' => true,
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
],
'mod_forum_mark_posts_read' => [
'classname' => 'mod_forum_external',
'methodname' => 'mark_posts_read',
'classpath' => 'mod/forum/externallib.php',
'description' => 'Mark forum posts as read.',
'type' => 'write',
'ajax' => true,
'capabilities' => 'mod/forum:viewdiscussion',
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
],
);
+101 -3
View File
@@ -191,7 +191,7 @@ class mod_forum_external extends external_api {
* @return array
*/
public static function get_discussion_posts(int $discussionid, ?string $sortby, ?string $sortdirection, bool $includeinlineattachments = false) {
global $USER;
global $USER, $CFG;
// Validate the parameter.
$params = self::validate_parameters(self::get_discussion_posts_parameters(), [
'discussionid' => $discussionid,
@@ -247,8 +247,20 @@ class mod_forum_external extends external_api {
$legacydatamapper = mod_forum\local\container::get_legacy_data_mapper_factory();
// Export the posts for the template before we mark them as read.
// This allows the template to indicate which are the new/unread ones
// whilst marking them as read right here and now.
$exportedposts = $postbuilder->build($USER, [$forum], [$discussion], $posts, $includeinlineattachments);
// Mark these posts as read.
if (!empty($posts)) {
if (!$CFG->forum_usermarksread && forum_tp_is_tracked($forum->get_id())) {
forum_tp_mark_posts_read($USER, array_keys($posts));
}
}
return [
'posts' => $postbuilder->build($USER, [$forum], [$discussion], $posts, $includeinlineattachments),
'posts' => $exportedposts,
'forumid' => $discussion->get_forum_id(),
'courseid' => $discussion->get_course_id(),
'ratinginfo' => \core_rating\external\util::get_rating_info(
@@ -1720,7 +1732,7 @@ class mod_forum_external extends external_api {
* @return array
*/
public static function get_discussion_posts_by_userid(int $userid, int $cmid, ?string $sortby, ?string $sortdirection) {
global $USER, $DB;
global $USER, $DB, $CFG;
// Validate the parameter.
$params = self::validate_parameters(self::get_discussion_posts_by_userid_parameters(), [
'userid' => $userid,
@@ -1817,6 +1829,13 @@ class mod_forum_external extends external_api {
'parentposts' => $parentposts,
],
];
// Mark these posts as read.
if (!empty($posts)) {
if (!$CFG->forum_usermarksread && forum_tp_is_tracked($forum->get_id())) {
forum_tp_mark_posts_read($USER, array_keys($posts));
}
}
}
return [
@@ -2314,4 +2333,83 @@ class mod_forum_external extends external_api {
]
);
}
/**
* Returns description of mark_posts_read parameters.
*
* @return external_function_parameters
*/
public static function mark_posts_read_parameters(): external_function_parameters {
return new external_function_parameters([
'postids' => new external_multiple_structure(
new external_value(PARAM_INT, 'The post ID to mark as read'),
'List of post IDs to mark as read',
),
'discussionid' => new external_value(PARAM_INT, 'The discussion the posts belong to'),
]);
}
/**
* Trigger the posts viewed event.
*
* This method differs from view_forum_discussion in that it is only
* interested in marking certain posts as viewed, and not an
* entire discussion.
*
* @param array $postids The post ids
* @param int $discussionid The discussion id
* @return bool True on success
*/
public static function mark_posts_read(array $postids, int $discussionid): bool {
global $CFG, $DB, $USER;
require_once($CFG->dirroot . "/mod/forum/lib.php");
$params = self::validate_parameters(
self::mark_posts_read_parameters(),
[
'postids' => $postids,
'discussionid' => $discussionid,
]
);
// Check permissions.
$discussion = $DB->get_record('forum_discussions', ['id' => $discussionid], '*', MUST_EXIST);
$cm = get_coursemodule_from_instance('forum', $discussion->forum, $discussion->course, false, MUST_EXIST);
$modcontext = context_module::instance($cm->id);
self::validate_context($modcontext);
require_capability('mod/forum:viewdiscussion', $modcontext, null, true, 'noviewdiscussionspermission', 'forum');
// Check the posts belong to the discussion.
[$insql, $params] = $DB->get_in_or_equal($postids, SQL_PARAMS_NAMED);
$params['discussionid'] = $discussionid;
$validposts = $DB->get_records_select(
'forum_posts',
"id $insql AND discussion = :discussionid",
$params,
'',
'id'
);
// Keep only valid posts.
$postids = array_intersect($postids, array_keys($validposts));
if (empty($postids)) {
return false;
}
// Mark as read if required.
$forum = $DB->get_record('forum', ['id' => $discussion->forum], '*', MUST_EXIST);
if (!$CFG->forum_usermarksread && forum_tp_is_tracked($forum)) {
return forum_tp_mark_posts_read($USER, $postids);
}
return false;
}
/**
* Describes the data returned from mark_posts_read.
*/
public static function mark_posts_read_returns(): external_value {
return new external_value(PARAM_BOOL, 'Returns true on success');
}
}
@@ -53,6 +53,8 @@
aria-expanded="false"
aria-controls="parent-post-content-{{id}}"
data-bs-toggle="collapse"
data-parent-postid="{{id}}"
data-parent-discussionid="{{discussionid}}"
>
{{#str}} viewparentpost, mod_forum {{/str}}
</button>
@@ -6,16 +6,18 @@ Feature: A teacher can set one of 3 possible options for tracking read forum pos
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 |
| username | firstname | lastname | email | trackforums |
| student1 | Student | 1 | student1@example.com | 1 |
| student2 | Student | 2 | student2@example.com | 0 |
| teacher1 | Teacher | 1 | teacher1@example.com | 1 |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| student2 | C1 | student |
| user | course | role |
| student1 | C1 | student |
| student2 | C1 | student |
| teacher1 | C1 | editingteacher |
Scenario: Tracking forum posts off
Given the following "activity" exists:
@@ -185,3 +187,44 @@ Feature: A teacher can set one of 3 possible options for tracking read forum pos
Then I should not see "1 unread post"
And I follow "Test forum name"
And I should not see "Track unread posts"
@javascript
Scenario: Forum grader panel marks posts as read
Given the following "activity" exists:
| course | C1 |
| activity | forum |
| name | Test forum name |
| idnumber | forum |
| grade_forum | 100 |
| scale | 100 |
And the following "mod_forum > discussions" exist:
| user | forum | name | subject | message |
| student1 | forum | Discussion 1 | Discussion 1 | student1's topic |
| student2 | forum | Discussion 2 | Discussion 2 | student2's topic |
And the following "mod_forum > posts" exist:
| user | parentsubject | subject | message |
| student2 | Discussion 1 | Reply to discussion 1 | student2's reply |
| student1 | Discussion 2 | Reply to discussion 2 | student1's reply |
When I am on the "Course 1" course page logged in as teacher1
# We have a discussion created by each student, and each has replied to the other.
Then I should see "4 unread posts"
And I am on the "Test forum name" "forum activity" page
And I press "Grade users"
And I press "Close grader"
And I am on the "Course 1" course page
# 4 posts minus the 2 viewed in the grader.
And I should see "2 unread posts"
And I am on the "Test forum name" "forum activity" page
And I press "Grade users"
# Let's look at a discussion in student1's grader that features a post from student2.
And I press "View discussion"
And I click on "Cancel" "button" in the "Discussion 1" "dialogue"
And I am on the "Course 1" course page
# Remaining 2 unread posts minus the 1 viewed in the discussion context.
And I should see "1 unread post"
And I am on the "Test forum name" "forum activity" page
And I press "Grade users"
# Let's look at the parent post that student1 replied to.
And I press "View parent post"
And I am on the "Course 1" course page
And I should not see "1 unread post"
@@ -3224,4 +3224,54 @@ final class externallib_test extends \core_external\tests\externallib_testcase {
'messageformat' => FORMAT_MOODLE,
], $updatedpost);
}
/**
* Test marking individual posts as read.
*/
public function test_mark_posts_read(): void {
global $DB;
$this->resetAfterTest(true);
// Setup test data.
$course = $this->getDataGenerator()->create_course();
$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
$user = $this->getDataGenerator()->create_user(['trackforums' => 1]);
$role = $DB->get_record('role', ['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', ['discussion' => $discussion->id]);
// Generate some posts.
$record = new \stdClass();
$record->course = $course->id;
$record->userid = $user->id;
$record->forum = $forum->id;
$record->discussion = $discussion->id;
$record->parent = $parentpost->id;
$post1 = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
$post2 = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
$post3 = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
// Track the forum.
$this->setUser($user);
forum_tp_start_tracking($forum->id, $user->id);
// There should be 4 unread posts (1 parent + 3 child posts).
$unreadposts = forum_tp_get_course_unread_posts($user->id, $course->id);
$this->assertEquals(4, $unreadposts[$forum->id]->unread);
// Marks some posts as read for this user.
$result = mod_forum_external::mark_posts_read([$post1->id, $post2->id], $discussion->id);
$this->assertTrue($result);
// There should be 2 unread posts left (1 parent + 1 child post).
$unreadposts = forum_tp_get_course_unread_posts($user->id, $course->id);
$this->assertEquals(2, $unreadposts[$forum->id]->unread);
}
}
+1 -1
View File
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2025100601; // The current module version (Date: YYYYMMDDXX).
$plugin->version = 2025100602; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2025092600; // Requires this Moodle version.
$plugin->component = 'mod_forum'; // Full name of the plugin (used for diagnostics)