MDL-83893 mod_forum: Add WS calls to support toggle

This commit is contained in:
Sara Arjona
2025-07-16 07:42:20 +02:00
parent 3ec78bc079
commit ee7dbad82f
16 changed files with 928 additions and 8 deletions
+10
View File
@@ -0,0 +1,10 @@
define("mod_forum/forum_overview_toggle",["exports","core/notification","core/str","mod_forum/repository"],(function(_exports,_notification,_str,_repository){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Handle forum subscription/tracking toggling.
*
* @module mod_forum/forum_overview_toggle
* @copyright 2025 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/function registerEventListeners(toggleElement){toggleElement.addEventListener("change",(()=>{"forum-subscription-toggle"===toggleElement.dataset.type&&async function(toggleElement){const forumId=toggleElement.dataset.forumid,newState=toggleElement.dataset.targetstate;if(!forumId||!newState)return;try{const newTargetState=!!(await _repository.default.setForumSubscriptionState(forumId,newState)).userstate.subscribed;updateSwitchState(toggleElement,newTargetState,newTargetState?"subscribe":"unsubscribe")}catch(error){_notification.default.exception(error)}}(toggleElement),"forum-track-toggle"===toggleElement.dataset.type&&async function(toggleElement){const forumId=toggleElement.dataset.forumid,newState=toggleElement.dataset.targetstate;if(!forumId||!newState)return;try{const newTargetState=!!(await _repository.default.setForumTrackingState(forumId,newState)).userstate.tracked;updateSwitchState(toggleElement,newTargetState,newTargetState?"trackingon":"trackingoff")}catch(error){_notification.default.exception(error)}}(toggleElement)}))}async function updateSwitchState(toggleElement,newTargetState,stringKey){toggleElement.dataset.targetstate=newTargetState?0:1;const string=await(0,_str.getString)(stringKey,"mod_forum");toggleElement.closest("td").querySelector('label[for="'.concat(toggleElement.id,'"] span')).textContent=string}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=_interopRequireDefault(_notification),_repository=_interopRequireDefault(_repository);_exports.init=toggleSelector=>{const toggleElement=document.querySelector(toggleSelector);toggleElement&&registerEventListeners(toggleElement)}}));
//# sourceMappingURL=forum_overview_toggle.min.js.map
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -6,6 +6,6 @@
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @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]}}}));
define("mod_forum/repository",["core/ajax"],(function(Ajax){return{setForumSubscriptionState:function(forumId,targetState){const request={methodname:"mod_forum_set_forum_subscription",args:{forumid:forumId,targetstate:targetState}};return Ajax.call([request])[0]},setForumTrackingState:function(forumId,targetState){const request={methodname:"mod_forum_set_forum_tracking",args:{forumid:forumId,targetstate:targetState}};return Ajax.call([request])[0]},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.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,123 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Handle forum subscription/tracking toggling.
*
* @module mod_forum/forum_overview_toggle
* @copyright 2025 Sara Arjona <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Notification from 'core/notification';
import {getString} from 'core/str';
import Repository from 'mod_forum/repository';
/**
* Register event listeners for the subscription/tracking toggles in the overview.
* @param {HTMLElement} toggleElement The toggle root element
*/
function registerEventListeners(toggleElement) {
toggleElement.addEventListener('change', () => {
if (toggleElement.dataset.type === 'forum-subscription-toggle') {
subscriptionToggleClickHandler(toggleElement);
}
if (toggleElement.dataset.type === 'forum-track-toggle') {
trackToggleClickHanldler(toggleElement);
}
});
}
/**
* Toggle subscription element click handler.
*
* @param {HTMLElement} toggleElement The toggle element that was clicked
* @return {Promise<void>}
*/
async function subscriptionToggleClickHandler(toggleElement) {
const forumId = toggleElement.dataset.forumid;
const newState = toggleElement.dataset.targetstate;
if (!forumId || !newState) {
return;
}
try {
const context = await Repository.setForumSubscriptionState(forumId, newState);
const newTargetState = !!context.userstate.subscribed;
updateSwitchState(
toggleElement,
newTargetState,
newTargetState ? 'subscribe' : 'unsubscribe',
);
} catch (error) {
Notification.exception(error);
}
}
/**
* Toggle track element click handler.
*
* @param {HTMLElement} toggleElement The toggle element that was clicked
* @return {Promise<void>}
*/
async function trackToggleClickHanldler(toggleElement) {
const forumId = toggleElement.dataset.forumid;
const newState = toggleElement.dataset.targetstate;
if (!forumId || !newState) {
return;
}
try {
const context = await Repository.setForumTrackingState(forumId, newState);
const newTargetState = !!context.userstate.tracked;
updateSwitchState(
toggleElement,
newTargetState,
newTargetState ? 'trackingon' : 'trackingoff',
);
} catch (error) {
Notification.exception(error);
}
}
/**
* Update the switch state of the toggle element.
*
* @param {HTMLElement} toggleElement The toggle element to update
* @param {Boolean} newTargetState The new target state to set (true for subscribed, false for unsubscribed)
* @param {string} stringKey The string key to retrieve the label text
* @return {Promise<void>}
*/
async function updateSwitchState(toggleElement, newTargetState, stringKey) {
toggleElement.dataset.targetstate = newTargetState ? 0 : 1;
const string = await getString(stringKey, 'mod_forum');
const label = toggleElement.closest('td').querySelector(`label[for="${toggleElement.id}"] span`);
label.textContent = string;
}
/**
* Initialize the forum overview toggle functionality.
*
* @param {string} toggleSelector The CSS selector for the toggle element to initialize
* @throws {Error} If no elements are found with the provided selector
*/
export const init = (toggleSelector) => {
const toggleElement = document.querySelector(toggleSelector);
if (!toggleElement) {
// If the user cannot track/subscribe to any course forum, the toggle will not be present.
return;
}
registerEventListeners(toggleElement);
};
+38
View File
@@ -22,6 +22,42 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['core/ajax'], function(Ajax) {
/**
* Set the subscription state for forum.
*
* @param {number} forumId ID of the forum to set the subscription state for
* @param {boolean} targetState Set the subscribed state. True == subscribed; false == unsubscribed.
* @return {object} jQuery promise
*/
const setForumSubscriptionState = function(forumId, targetState) {
const request = {
methodname: 'mod_forum_set_forum_subscription',
args: {
forumid: forumId,
targetstate: targetState
}
};
return Ajax.call([request])[0];
};
/**
* Set the tracking state for forum.
*
* @param {number} forumId ID of the forum to set the tracking state for
* @param {boolean} targetState Set the tracking state for unread messages. True == track; false == untrack.
* @return {object} jQuery promise
*/
const setForumTrackingState = function(forumId, targetState) {
const request = {
methodname: 'mod_forum_set_forum_tracking',
args: {
forumid: forumId,
targetstate: targetState
}
};
return Ajax.call([request])[0];
};
/**
* Set the subscription state for a discussion in a forum.
*
@@ -154,6 +190,8 @@ define(['core/ajax'], function(Ajax) {
};
return {
setForumSubscriptionState: setForumSubscriptionState,
setForumTrackingState: setForumTrackingState,
setDiscussionSubscriptionState: setDiscussionSubscriptionState,
addDiscussionPost: addDiscussionPost,
setDiscussionLockState: setDiscussionLockState,
@@ -0,0 +1,122 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace mod_forum\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
use mod_forum\local\exporters\forum as forum_exporter;
/**
* Web Service to control the state of a forum subscription
*
* @package mod_forum
* @category external
* @copyright 2025 Sara Arjona <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class set_forum_subscription extends external_api {
/**
* Returns description of method parameters
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters(
[
'forumid' => new external_value(PARAM_INT, 'Forum that the user wants to subscribe or unsubscribe from'),
'targetstate' => new external_value(PARAM_BOOL, 'The target state'),
]
);
}
/**
* Set the forum subscription state.
*
* @param int $forumid The forum identifier.
* @param bool $targetstate Whether to subscribe or unsubscribe the user to the forum.
* @return \stdClass
*/
public static function execute(
int $forumid,
bool $targetstate,
): \stdClass {
global $PAGE, $USER;
$params = self::validate_parameters(self::execute_parameters(), [
'forumid' => $forumid,
'targetstate' => $targetstate,
]);
$vaultfactory = \mod_forum\local\container::get_vault_factory();
$forumvault = $vaultfactory->get_forum_vault();
$forum = $forumvault->get_from_id($params['forumid']);
if (!$forum) {
throw new \moodle_exception('invalidforumid', 'mod_forum', '', $params['forumid']);
}
$coursemodule = $forum->get_course_module_record();
$context = $forum->get_context();
self::validate_context($context);
$legacydatamapperfactory = \mod_forum\local\container::get_legacy_data_mapper_factory();
$forumrecord = $legacydatamapperfactory->get_forum_data_mapper()->to_legacy_object($forum);
if (
!\mod_forum\subscriptions::is_subscribable($forumrecord)
&& !has_capability('mod/forum:managesubscriptions', $context)
) {
// 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,
null,
$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($USER->id, $forumrecord, $context, true);
} else {
\mod_forum\subscriptions::unsubscribe_user($USER->id, $forumrecord, $context, true);
}
}
/** @var \mod_forum\local\factories\exporter $exporterfactory */
$exporterfactory = \mod_forum\local\container::get_exporter_factory();
$exporter = $exporterfactory->get_forum_exporter(
user: $USER,
forum: $forum,
currentgroup: null,
);
return $exporter->export($PAGE->get_renderer('mod_forum'));
}
/**
* Describe the return structure of the external service.
*
* @return external_single_structure
*/
public static function execute_returns(): external_single_structure {
return forum_exporter::get_read_structure();
}
}
+116
View File
@@ -0,0 +1,116 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace mod_forum\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
use mod_forum\local\exporters\forum as forum_exporter;
/**
* Web Service to control the state of a forum tracking.
*
* @package mod_forum
* @category external
* @copyright 2025 Sara Arjona <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class set_forum_tracking extends external_api {
/**
* Returns description of method parameters
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters(
[
'forumid' => new external_value(PARAM_INT, 'Forum that the user wants tracking for'),
'targetstate' => new external_value(PARAM_BOOL, 'The target state'),
]
);
}
/**
* Set the forum tracking state.
*
* @param int $forumid The forum identifier.
* @param bool $targetstate Whether to track or not the unread posts in the forum.
* @return \stdClass
*/
public static function execute(
int $forumid,
bool $targetstate,
): \stdClass {
global $PAGE, $USER;
$params = self::validate_parameters(self::execute_parameters(), [
'forumid' => $forumid,
'targetstate' => $targetstate,
]);
$vaultfactory = \mod_forum\local\container::get_vault_factory();
$forumvault = $vaultfactory->get_forum_vault();
$forum = $forumvault->get_from_id($params['forumid']);
if (!$forum) {
throw new \moodle_exception('invalidforumid', 'mod_forum', '', $params['forumid']);
}
$context = $forum->get_context();
self::validate_context($context);
$legacydatamapperfactory = \mod_forum\local\container::get_legacy_data_mapper_factory();
$forumrecord = $legacydatamapperfactory->get_forum_data_mapper()->to_legacy_object($forum);
$usetracking = forum_tp_can_track_forums($forum);
if (!$usetracking) {
// Nothing to do. We won't actually output any content here though.
throw new \moodle_exception('cannottrack', 'mod_forum');
}
$istracked = forum_tp_is_tracked($forumrecord);
// If the current state doesn't equal the desired state then update the current
// state to the desired state.
if ($istracked != (bool) $params['targetstate']) {
if ($params['targetstate']) {
forum_tp_start_tracking($forumrecord->id);
} else {
forum_tp_stop_tracking($forumrecord->id);
}
$cache = \cache::make('mod_forum', 'forum_is_tracked');
$cache->purge();
}
/** @var \mod_forum\local\factories\exporter $exporterfactory */
$exporterfactory = \mod_forum\local\container::get_exporter_factory();
$exporter = $exporterfactory->get_forum_exporter(
user: $USER,
forum: $forum,
currentgroup: null,
);
return $exporter->export($PAGE->get_renderer('mod_forum'));
}
/**
* Describe the return structure of the external service.
*
* @return external_single_structure
*/
public static function execute_returns(): external_single_structure {
return forum_exporter::get_read_structure();
}
}
@@ -71,6 +71,7 @@ class forum extends exporter {
'userstate' => [
'type' => [
'tracked' => ['type' => PARAM_INT],
'subscribed' => ['type' => PARAM_INT],
],
],
'capabilities' => [
@@ -125,7 +126,11 @@ class forum extends exporter {
'gradingenabled' => $this->forum->is_grading_enabled()
],
'userstate' => [
'tracked' => forum_tp_is_tracked($this->get_forum_record(), $this->related['user']),
'tracked' => (int) forum_tp_is_tracked($this->get_forum_record(), $this->related['user']),
'subscribed' => (int) \mod_forum\subscriptions::is_subscribed(
$this->related['user']->id,
$this->get_forum_record(),
),
],
'capabilities' => [
'viewdiscussions' => $capabilitymanager->can_view_discussions($user),
+17
View File
@@ -198,4 +198,21 @@ $functions = array(
'type' => 'write',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_forum_set_forum_subscription' => [
'classname' => 'mod_forum\external\set_forum_subscription',
'methodname' => 'execute',
'description' => 'Subscribe or unsubscribe the user to a forum.',
'type' => 'write',
'ajax' => true,
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
],
'mod_forum_set_forum_tracking' => [
'classname' => 'mod_forum\external\set_forum_tracking',
'methodname' => 'execute',
'description' => 'Track or not unread messages in a forum for the user.',
'type' => 'write',
'ajax' => true,
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
],
);
+8 -2
View File
@@ -4660,8 +4660,14 @@ function forum_tp_can_track_forums($forum=false, $user=false) {
$forum = $DB->get_record('forum', array('id' => $forum), '', 'id,trackingtype');
}
$forumallows = ($forum->trackingtype == FORUM_TRACKING_OPTIONAL);
$forumforced = ($forum->trackingtype == FORUM_TRACKING_FORCED);
if (method_exists($forum, 'get_tracking_type')) {
$trackingtype = $forum->get_tracking_type();
} else {
$trackingtype = $forum->trackingtype;
}
$forumallows = ($trackingtype == FORUM_TRACKING_OPTIONAL);
$forumforced = ($trackingtype == FORUM_TRACKING_FORCED);
if ($CFG->forum_allowforcedreadtracking) {
// If we allow forcing, then forced forums takes procidence over user setting.
@@ -16,7 +16,11 @@
namespace mod_forum;
use mod_forum\local\entities\forum as forum_entity;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/forum/lib.php');
use mod_forum\local\exporters\forum as forum_exporter;
/**
@@ -27,6 +31,23 @@ use mod_forum\local\exporters\forum as forum_exporter;
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class exporters_forum_test extends \advanced_testcase {
#[\Override]
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.
subscriptions::reset_forum_cache();
}
#[\Override]
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.
subscriptions::reset_forum_cache();
parent::tearDown();
}
/**
* Test the export function returns expected values.
*/
@@ -40,7 +61,8 @@ final class exporters_forum_test extends \advanced_testcase {
$course = $datagenerator->create_course();
$forum = $datagenerator->create_module('forum', [
'course' => $course->id,
'groupmode' => VISIBLEGROUPS
'groupmode' => VISIBLEGROUPS,
'forcesubscribe' => FORUM_FORCESUBSCRIBE,
]);
$coursemodule = get_coursemodule_from_instance('forum', $forum->id);
$context = \context_module::instance($coursemodule->id);
@@ -61,10 +83,17 @@ final class exporters_forum_test extends \advanced_testcase {
$this->assertEquals($forum->get_id(), $exportedforum->id);
$this->assertEquals(VISIBLEGROUPS, $exportedforum->state['groupmode']);
$this->assertEquals(false, $exportedforum->userstate['tracked']);
$this->assertEquals(false, $exportedforum->userstate['subscribed']);
$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']);
// Enrol the user in the course and check the capabilities and user state.
$datagenerator->enrol_user($user->id, $course->id);
$exportedforum = $exporter->export($renderer);
$this->assertEquals(true, $exportedforum->userstate['subscribed']);
$this->assertEquals(true, $exportedforum->capabilities['viewdiscussions']);
}
}
@@ -0,0 +1,237 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace 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');
use core_external\external_api;
use mod_forum\external\set_forum_subscription;
use mod_forum\subscriptions;
/**
* Tests for the set_forum_subscription external function.
*
* @package mod_forum
* @category test
* @copyright 2025 Sara Arjona <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_forum\external\set_forum_subscription
*/
final class set_forum_subscription_test extends \externallib_advanced_testcase {
#[\Override]
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.
subscriptions::reset_forum_cache();
}
#[\Override]
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.
subscriptions::reset_forum_cache();
parent::tearDown();
}
/**
* Test execute method.
*
* @dataProvider execute_provider
* @covers ::execute
*
* @param bool|null $initialstate Initialise subscription state, null means no initial state.
* @param bool $targetstate Expected target state of the subscription.
* @param int $subscriptionmode Subscription mode for the forum.
* @param bool $expectedexception Whether an exception is expected.
*/
public function test_execute(
?bool $initialstate,
bool $targetstate,
int $subscriptionmode = FORUM_CHOOSESUBSCRIBE,
bool $expectedexception = false,
): void {
$this->resetAfterTest();
$user = self::getDataGenerator()->create_user();
$course = self::getDataGenerator()->create_course();
$this->getDataGenerator()->enrol_user($user->id, $course->id);
$this->setUser($user);
$forum = self::getDataGenerator()->create_module(
'forum',
[
'course' => $course->id,
'forcesubscribe' => $subscriptionmode,
]
);
if ($expectedexception) {
$this->expectException(\moodle_exception::class);
} else if ($initialstate !== null) {
// Set the initial state of the subscription.
if ($initialstate) {
subscriptions::subscribe_user($user->id, $forum);
} else {
subscriptions::unsubscribe_user($user->id, $forum);
}
$this->assertEquals($initialstate, subscriptions::is_subscribed($user->id, $forum));
}
$return = external_api::clean_returnvalue(
set_forum_subscription::execute_returns(),
set_forum_subscription::execute($forum->id, $targetstate),
);
$this->assertEquals($targetstate, $return['userstate']['subscribed']);
$this->assertEquals($targetstate, subscriptions::is_subscribed($user->id, $forum));
}
/**
* Data provider for test_execute.
*
* @return array The data provider array.
*/
public static function execute_provider(): array {
return [
'Subscription initially false, set to true' => [
'initialstate' => false,
'targetstate' => true,
],
'Subscription initially true, set to false' => [
'initialstate' => true,
'targetstate' => false,
],
'Subscription initially false, set to false' => [
'initialstate' => false,
'targetstate' => false,
],
'Subscription initially true, set to true' => [
'initialstate' => true,
'targetstate' => true,
],
'Subscription forced on' => [
'initialstate' => null,
'targetstate' => true,
'subscriptionmode' => FORUM_FORCESUBSCRIBE,
'expectedexception' => true,
],
'Subscription forced off' => [
'initialstate' => null,
'targetstate' => false,
'subscriptionmode' => FORUM_DISALLOWSUBSCRIBE,
'expectedexception' => true,
],
'Subscription initial on' => [
'initialstate' => true,
'targetstate' => false,
'subscriptionmode' => FORUM_INITIALSUBSCRIBE,
],
];
}
/**
* Test execute method when forum is not subscribable.
*
* @covers ::execute
*/
public function test_execute_not_subscribable(): void {
$this->resetAfterTest();
$admin = get_admin();
$teacher = self::getDataGenerator()->create_user();
$student = self::getDataGenerator()->create_user();
$course = self::getDataGenerator()->create_course();
$this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher');
$this->getDataGenerator()->enrol_user($student->id, $course->id);
$forum = self::getDataGenerator()->create_module('forum', [
'course' => $course->id,
'forcesubscribe' => FORUM_DISALLOWSUBSCRIBE,
]);
// Admin user can subscribe to a forum that does not allow subscriptions.
$this->setAdminUser();
$return = external_api::clean_returnvalue(
set_forum_subscription::execute_returns(),
set_forum_subscription::execute($forum->id, true),
);
$this->assertEquals(true, $return['userstate']['subscribed']);
$this->assertEquals(true, subscriptions::is_subscribed($admin->id, $forum));
// Teacher user can subscribe to a forum that does not allow subscriptions because they have the capability.
$this->setUser($teacher);
$return = external_api::clean_returnvalue(
set_forum_subscription::execute_returns(),
set_forum_subscription::execute($forum->id, true),
);
$this->assertEquals(true, $return['userstate']['subscribed']);
$this->assertEquals(true, subscriptions::is_subscribed($teacher->id, $forum));
// Attempt to subscribe to a forum that does not allow subscriptions without the required capability.
$this->setUser($student);
$this->expectException(\moodle_exception::class);
external_api::clean_returnvalue(
set_forum_subscription::execute_returns(),
set_forum_subscription::execute($forum->id, true),
);
}
/**
* Test execute method when forum does not exist.
*
* @covers ::execute
*/
public function test_execute_unexisting_forum(): void {
$this->resetAfterTest();
$this->setAdminUser();
$this->expectException(\moodle_exception::class);
external_api::clean_returnvalue(
set_forum_subscription::execute_returns(),
set_forum_subscription::execute(9999, true),
);
}
/**
* Test execute method when user is not enrolled in the course of the forum.
*
* @covers ::execute
*/
public function test_execute_unenrolled_user(): void {
$this->resetAfterTest();
$user = self::getDataGenerator()->create_user();
$course = self::getDataGenerator()->create_course();
$this->setUser($user);
$forum = self::getDataGenerator()->create_module('forum', ['course' => $course->id]);
$this->expectException(\moodle_exception::class);
external_api::clean_returnvalue(
set_forum_subscription::execute_returns(),
set_forum_subscription::execute($forum->id, true),
);
}
}
@@ -0,0 +1,210 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace 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');
use core_external\external_api;
use mod_forum\external\set_forum_tracking;
use mod_forum\subscriptions;
/**
* Tests for the set_forum_tracking external function.
*
* @package mod_forum
* @category test
* @copyright 2025 Sara Arjona <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_forum\external\set_forum_tracking
*/
final class set_forum_tracking_test extends \externallib_advanced_testcase {
#[\Override]
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.
subscriptions::reset_forum_cache();
}
#[\Override]
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.
subscriptions::reset_forum_cache();
parent::tearDown();
}
/**
* Test execute method.
*
* @dataProvider execute_provider
* @covers ::execute
*
* @param bool|null $initialstate Initialise tracking state, null means no initial state.
* @param bool $targetstate Expected target state of the tracking.
* @param int $forumtype Tracking mode for the forum.
* @param bool $expectedexception Whether an exception is expected.
*/
public function test_execute(
?bool $initialstate,
bool $targetstate,
int $forumtype = FORUM_TRACKING_OPTIONAL,
bool $expectedexception = false,
): void {
global $CFG;
$this->resetAfterTest();
// Allow force.
$CFG->forum_allowforcedreadtracking = 1;
$user = self::getDataGenerator()->create_user(['trackforums' => 1]);
$course = self::getDataGenerator()->create_course();
$this->getDataGenerator()->enrol_user($user->id, $course->id);
$forum = self::getDataGenerator()->create_module(
'forum',
[
'course' => $course->id,
'trackingtype' => $forumtype,
],
);
$this->setUser($user);
if ($expectedexception) {
$this->expectException(\moodle_exception::class);
} else if ($initialstate !== null) {
// Set the initial state of the subscription.
if ($initialstate) {
forum_tp_start_tracking($forum->id);
} else {
forum_tp_stop_tracking($forum->id);
}
$this->assertEquals($initialstate, forum_tp_is_tracked($forum));
}
$return = external_api::clean_returnvalue(
set_forum_tracking::execute_returns(),
set_forum_tracking::execute($forum->id, $targetstate),
);
$this->assertEquals($targetstate, $return['userstate']['tracked']);
$this->assertEquals($targetstate, forum_tp_is_tracked($forum));
}
/**
* Data provider for test_execute.
*
* @return array The data provider array.
*/
public static function execute_provider(): array {
return [
'Initially false, set to true' => [
'initialstate' => false,
'targetstate' => true,
],
'Initially false, set to false' => [
'initialstate' => false,
'targetstate' => false,
],
'Initially true, set to false' => [
'initialstate' => true,
'targetstate' => false,
],
'Initially true, set to true' => [
'initialstate' => true,
'targetstate' => true,
],
'Forced off' => [
'initialstate' => null,
'targetstate' => false,
'forumtype' => FORUM_TRACKING_OFF,
'expectedexception' => true,
],
'Forced on' => [
'initialstate' => null,
'targetstate' => true,
'forumtype' => FORUM_TRACKING_FORCED,
],
];
}
/**
* Test execute method when tracking is not enabled for the user.
*
* @covers ::execute
*/
public function test_execute_no_tracking(): void {
$this->resetAfterTest();
$user = self::getDataGenerator()->create_user(['trackforums' => 0]);
$course = self::getDataGenerator()->create_course();
$this->getDataGenerator()->enrol_user($user->id, $course->id);
$forum = self::getDataGenerator()->create_module('forum', [ 'course' => $course->id]);
$this->setUser($user);
$this->expectException(\moodle_exception::class);
external_api::clean_returnvalue(
set_forum_tracking::execute_returns(),
set_forum_tracking::execute($forum->id, true),
);
}
/**
* Test execute method when forum does not exist.
*
* @covers ::execute
*/
public function test_execute_unexisting_forum(): void {
$this->resetAfterTest();
$this->setAdminUser();
$this->expectException(\moodle_exception::class);
external_api::clean_returnvalue(
set_forum_tracking::execute_returns(),
set_forum_tracking::execute(9999, true),
);
}
/**
* Test execute method when user is not enrolled in the course of the forum.
*
* @covers ::execute
*/
public function test_execute_unenrolled_user(): void {
$this->resetAfterTest();
$user = self::getDataGenerator()->create_user();
$course = self::getDataGenerator()->create_course();
$this->setUser($user);
$forum = self::getDataGenerator()->create_module('forum', ['course' => $course->id]);
$this->expectException(\moodle_exception::class);
external_api::clean_returnvalue(
set_forum_tracking::execute_returns(),
set_forum_tracking::execute($forum->id, true),
);
}
}
@@ -195,6 +195,12 @@ final class externallib_test extends externallib_advanced_testcase {
$expectedforums[$forum1->id] = (array) $forum1;
$expectedforums[$forum2->id] = (array) $forum2;
// Reset static cache.
$forum1cm = get_coursemodule_from_id('forum', $forum1->cmid);
forum_tp_count_forum_unread_posts($forum1cm, $course1, true);
$forum2cm = get_coursemodule_from_id('forum', $forum2->cmid);
forum_tp_count_forum_unread_posts($forum2cm, $course2, true);
// 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);
+1 -1
View File
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2025041401; // The current module version (Date: YYYYMMDDXX).
$plugin->version = 2025041402; // 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)