MDL-78551 core_communication: Add hook listeners

This commit is contained in:
Safat
2024-03-26 13:53:39 +11:00
parent fc7127e867
commit abed8cddbf
10 changed files with 2285 additions and 25 deletions
+167 -10
View File
@@ -118,9 +118,9 @@ class api {
/**
* Return the underlying communication processor object.
*
* @return processor
* @return ?processor
*/
public function get_processor(): processor {
public function get_processor(): ?processor {
return $this->communication;
}
@@ -405,6 +405,114 @@ class api {
return $this->communication->get_provider();
}
/**
* Configure the room and membership by provider selected for the communication instance.
*
* This method will add a task to the queue to configure the room and membership by comparing the change of provider.
* There are some major cases to consider for this method to allow minimum duplication when this api is used.
* Some of the major cases are:
* 1. If the communication instance is not created at all, then create it and add members.
* 2. If the current provider is none and the new provider is also none, then nothing to do.
* 3. If the current and existing provider is the same, don't need to do anything.
* 4. If provider set to none, remove all the members.
* 5. If previous provider was not none and current provider is not none, but a different provider, remove members and add
* for the new one.
* 6. If previous provider was none and current provider is not none, don't need to remove, just
* update the selected provider and add users to that provider. Do not queue the task to add members to room as the room
* might not have created yet. The add room task adds the task to add members to room anyway.
* 7. If it's a new provider, never used/created, now create the room after considering all these cases for a new provider.
*
* @param string $provider The provider name
* @param \stdClass $instance The instance object
* @param string $communicationroomname The communication room name
* @param array $users The user ids to add to the room
* @param null|\stored_file $instanceimage The stored file for the avatar
*/
public function configure_room_and_membership_by_provider(
string $provider,
stdClass $instance,
string $communicationroomname,
array $users,
?\stored_file $instanceimage = null,
): void {
// If the current provider is inactive and the new provider is also none, then nothing to do.
if (
$this->communication !== null &&
$this->communication->get_provider_status() === processor::PROVIDER_INACTIVE &&
$provider === processor::PROVIDER_NONE
) {
return;
}
// If provider set to none, remove all the members.
if (
$this->communication !== null &&
$this->communication->get_provider_status() === processor::PROVIDER_ACTIVE &&
$provider === processor::PROVIDER_NONE
) {
$this->remove_all_members_from_room();
$this->update_room(
active: processor::PROVIDER_INACTIVE,
communicationroomname: $communicationroomname,
avatar: $instanceimage,
instance: $instance,
);
return;
}
if (
// If previous provider was active and not none and current provider is not none, but a different provider,
// remove members and de-activate the previous provider.
$this->communication !== null &&
$this->communication->get_provider_status() === processor::PROVIDER_ACTIVE &&
$provider !== $this->get_provider()
) {
$this->remove_all_members_from_room();
// Now deactivate the previous provider.
$this->update_room(
active: processor::PROVIDER_INACTIVE,
communicationroomname: $communicationroomname,
avatar: $instanceimage,
instance: $instance,
);
}
// Now re-init the constructor for the new provider.
$this->__construct(
context: $this->context,
component: $this->component,
instancetype: $this->instancetype,
instanceid: $this->instanceid,
provider: $provider,
);
// If it's a new provider, never used/created, now create the room.
if ($this->communication === null) {
$this->create_and_configure_room(
communicationroomname: $communicationroomname,
avatar: $instanceimage,
instance: $instance,
);
$queue = false;
} else {
// Otherwise update the room.
$this->update_room(
active: processor::PROVIDER_ACTIVE,
communicationroomname: $communicationroomname,
avatar: $instanceimage,
instance: $instance,
);
$queue = true;
}
// Now add the members.
$this->add_members_to_room(
userids: $users,
queue: $queue,
);
}
/**
* Create a communication ad-hoc task for create operation.
* This method will add a task to the queue to create the room.
@@ -412,16 +520,17 @@ class api {
* @param string $communicationroomname The communication room name
* @param null|\stored_file $avatar The stored file for the avatar
* @param \stdClass|null $instance The actual instance object
* @param bool $queue Whether to queue the task or not
*/
public function create_and_configure_room(
string $communicationroomname,
?\stored_file $avatar = null,
?\stdClass $instance = null,
bool $queue = true,
): void {
if ($this->provider === processor::PROVIDER_NONE || $this->provider === '') {
return;
}
// Create communication record.
$this->communication = processor::create_instance(
context: $this->context,
@@ -442,6 +551,11 @@ class api {
$this->set_avatar($avatar);
}
// Nothing else to do if the queue is false.
if (!$queue) {
return;
}
// Add ad-hoc task to create the provider room.
create_and_configure_room_task::queue(
$this->communication,
@@ -456,13 +570,19 @@ class api {
* @param null|string $communicationroomname The communication room name
* @param null|\stored_file $avatar The stored file for the avatar
* @param \stdClass|null $instance The actual instance object
* @param bool $queue Whether to queue the task or not
*/
public function update_room(
?int $active = null,
?string $communicationroomname = null,
?\stored_file $avatar = null,
?\stdClass $instance = null,
bool $queue = true,
): void {
if (!$this->communication) {
return;
}
// If the provider is none, we don't need to do anything from room point of view.
if ($this->communication->get_provider() === processor::PROVIDER_NONE) {
return;
@@ -503,6 +623,11 @@ class api {
// If the value is `null`, then unset the avatar.
$this->set_avatar($avatar);
// Nothing else to do if the queue is false.
if (!$queue) {
return;
}
// Always queue a room update, even if none of the above standard fields have changed.
// It is possible for providers to have custom fields that have been updated.
update_room_task::queue(
@@ -613,6 +738,35 @@ class api {
}
}
/**
* Remove all users from the room.
*
* @param bool $queue Whether to queue the task or not
*/
public function remove_all_members_from_room(bool $queue = true): void {
// No communication object? something not done right.
if (!$this->communication) {
return;
}
if ($this->communication->get_provider() === processor::PROVIDER_NONE) {
return;
}
// This provider does not manage users? No action required.
if (!$this->communication->supports_user_features()) {
return;
}
$this->communication->add_delete_user_flag($this->communication->get_all_userids_for_instance());
if ($queue) {
remove_members_from_room::queue(
$this->communication
);
}
}
/**
* Display the communication room status notification.
*/
@@ -626,20 +780,23 @@ class api {
return;
}
$roomstatus = $this->get_communication_room_url() ? 'ready' : 'pending';
$roomstatus = $this->get_communication_room_url()
? constants::COMMUNICATION_STATUS_READY
: constants::COMMUNICATION_STATUS_PENDING;
$pluginname = get_string('pluginname', $this->get_provider());
$message = get_string('communicationroom' . $roomstatus, 'communication', $pluginname);
// We only show the ready notification once per user.
// We check this with a custom user preference.
$roomreadypreference = "{$this->component}_{$this->instancetype}_{$this->instanceid}_room_ready";
switch ($roomstatus) {
case 'pending':
case constants::COMMUNICATION_STATUS_PENDING:
\core\notification::add($message, \core\notification::INFO);
unset_user_preference($roomreadypreference);
break;
case 'ready':
// We only show the ready notification once per user.
// We check this with a custom user preference.
$roomreadypreference = "{$this->component}_{$this->instancetype}_{$this->instanceid}_room_ready";
case constants::COMMUNICATION_STATUS_READY:
if (empty(get_user_preferences($roomreadypreference))) {
\core\notification::add($message, \core\notification::SUCCESS);
set_user_preference($roomreadypreference, true);
+45
View File
@@ -0,0 +1,45 @@
<?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 core_communication;
/**
* Constants for communication api.
*
* @package core_communication
* @copyright 2024 Safat Shahin <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class constants {
/** @var string GROUP_COMMUNICATION_INSTANCETYPE The group communication instance type. */
public const GROUP_COMMUNICATION_INSTANCETYPE = 'groupcommunication';
/** @var string GROUP_COMMUNICATION_COMPONENT The group communication component. */
public const GROUP_COMMUNICATION_COMPONENT = 'core_group';
/** @var string COURSE_COMMUNICATION_INSTANCETYPE The course communication instance type. */
public const COURSE_COMMUNICATION_INSTANCETYPE = 'coursecommunication';
/** @var string COURSE_COMMUNICATION_COMPONENT The course communication component. */
public const COURSE_COMMUNICATION_COMPONENT = 'core_course';
/** @var string COMMUNICATION_STATUS_PENDING The communication status pending. */
public const COMMUNICATION_STATUS_PENDING = 'pending';
/** @var string COMMUNICATION_STATUS_READY The communication status sent. */
public const COMMUNICATION_STATUS_READY = 'ready';
}
+547
View File
@@ -0,0 +1,547 @@
<?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 core_communication;
use context;
use stdClass;
/**
* Helper method for communication.
*
* @package core_communication
* @copyright 2023 Safat Shahin <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class helper {
/**
* Load the communication instance for group id.
*
* @param int $groupid The group id
* @param context $context The context, to make sure any instance using group can load the communication instance
* @return api The communication instance.
*/
public static function load_by_group(int $groupid, context $context): api {
return \core_communication\api::load_by_instance(
context: $context,
component: constants::GROUP_COMMUNICATION_COMPONENT,
instancetype: constants::GROUP_COMMUNICATION_INSTANCETYPE,
instanceid: $groupid,
);
}
/**
* Load the communication instance for course id.
*
* @param int $courseid The course id
* @param \context $context The context
* @param string|null $provider The provider name
* @return api The communication instance
*/
public static function load_by_course(
int $courseid,
\context $context,
?string $provider = null,
): api {
return \core_communication\api::load_by_instance(
context: $context,
component: constants::COURSE_COMMUNICATION_COMPONENT,
instancetype: constants::COURSE_COMMUNICATION_INSTANCETYPE,
instanceid: $courseid,
provider: $provider,
);
}
/**
* Communication api call to create room for a group if course has group mode enabled.
*
* @param int $courseid The course id.
* @return stdClass
*/
public static function get_course(int $courseid): stdClass {
global $DB;
return $DB->get_record(
table: 'course',
conditions: ['id' => $courseid],
strictness: MUST_EXIST,
);
}
/**
* Is group mode enabled for the course.
*
* @param stdClass $course The course object
*/
public static function is_group_mode_enabled_for_course(stdClass $course): bool {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return false;
}
$groupmode = $course->groupmode ?? get_course(courseid: $course->id)->groupmode;
return (int)$groupmode !== NOGROUPS;
}
/**
* Helper to update room membership according to action passed.
* This method will help reduce a large amount of duplications of code in different places in core.
*
* @param \stdClass $course The course object.
* @param array $userids The user ids to add to the communication room.
* @param string $memberaction The action to perform on the communication room.
*/
public static function update_course_communication_room_membership(
\stdClass $course,
array $userids,
string $memberaction,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
// Validate communication api action.
$roomuserprovider = new \ReflectionClass(room_user_provider::class);
if (!$roomuserprovider->hasMethod($memberaction)) {
throw new \coding_exception('Invalid action provided.');
}
// Get the group mode for this course.
$groupmode = $course->groupmode ?? get_course(courseid: $course->id)->groupmode;
$coursecontext = \context_course::instance(courseid: $course->id);
// If group mode is not set, then just handle the update normally for these users.
if ((int)$groupmode === NOGROUPS) {
$communication = self::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
$communication->$memberaction($userids);
} else {
// If group mode is set, then handle the update for these users with repect to the group they are in.
$coursegroups = groups_get_all_groups(courseid: $course->id);
$usershandled = [];
// Filter all the users that have the capability to access all groups.
$allaccessgroupusers = self::get_users_has_access_to_all_groups(
userids: $userids,
courseid: $course->id,
);
foreach ($coursegroups as $coursegroup) {
// Get all group members.
$groupmembers = groups_get_members(groupid: $coursegroup->id, fields: 'u.id');
$groupmembers = array_column($groupmembers, 'id');
// Find the common user ids between the group members and incoming userids.
$groupuserstohandle = array_intersect(
$groupmembers,
$userids,
);
// Add users who have the capability to access this group (and haven't been added already).
foreach ($allaccessgroupusers as $allaccessgroupuser) {
if (!in_array($allaccessgroupuser, $groupuserstohandle, true)) {
$groupuserstohandle[] = $allaccessgroupuser;
}
}
// Keep track of the users we have handled already.
$usershandled = array_merge($usershandled, $groupuserstohandle);
// Let's check if we need to add/remove members from room because of a role change.
// First, get all the instance users for this group.
$communication = self::load_by_group(
groupid: $coursegroup->id,
context: $coursecontext,
);
$instanceusers = $communication->get_processor()->get_all_userids_for_instance();
// The difference between the instance users and the group members are the ones we want to check.
$roomuserstocheck = array_diff(
$instanceusers,
$groupmembers
);
if (!empty($roomuserstocheck)) {
// Check if they still have the capability to keep their access in the room.
$userslostcaps = array_diff(
$roomuserstocheck,
self::get_users_has_access_to_all_groups(
userids: $roomuserstocheck,
courseid: $course->id,
),
);
// Remove users who no longer have the capability.
if (!empty($userslostcaps)) {
$communication->remove_members_from_room(userids: $userslostcaps);
}
}
// Check if we have to add any room members who have gained the capability.
$usersgainedcaps = array_diff(
$allaccessgroupusers,
$instanceusers,
);
// If we have users, add them to the room.
if (!empty($usersgainedcaps)) {
$communication->add_members_to_room(userids: $usersgainedcaps);
}
// Finally, trigger the update task for the users who need to be handled.
$communication->$memberaction($groupuserstohandle);
}
// If the user was not in any group, but an update/remove action was requested for the user,
// then the user must have had a role with the capablity, but made a regular user.
$usersnothandled = array_diff($userids, $usershandled);
// These users are not handled and not in any group, so logically these users lost their permission to stay in the room.
foreach ($coursegroups as $coursegroup) {
$communication = self::load_by_group(
groupid: $coursegroup->id,
context: $coursecontext,
);
$communication->remove_members_from_room(userids: $usersnothandled);
}
}
}
/**
* Get users with the capability to access all groups.
*
* @param array $userids user ids to check the permission
* @param int $courseid course id
* @return array of userids
*/
public static function get_users_has_access_to_all_groups(
array $userids,
int $courseid
): array {
$allgroupsusers = [];
$context = \context_course::instance(courseid: $courseid);
foreach ($userids as $userid) {
if (
has_capability(
capability: 'moodle/site:accessallgroups',
context: $context,
user: $userid,
)
) {
$allgroupsusers[] = $userid;
}
}
return $allgroupsusers;
}
/**
* Get the course communication url according to course setup.
*
* @param stdClass $course The course object.
* @return string The communication room url.
*/
public static function get_course_communication_url(stdClass $course): string {
// If it's called from site context, then just return.
if ($course->id === SITEID) {
return '';
}
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return '';
}
$url = '';
// Get the group mode for this course.
$groupmode = $course->groupmode ?? get_course(courseid: $course->id)->groupmode;
$coursecontext = \context_course::instance(courseid: $course->id);
// If group mode is not set then just handle the course communication for these users.
if ((int)$groupmode === NOGROUPS) {
$communication = self::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
$url = $communication->get_communication_room_url();
} else {
// If group mode is set then handle the group communication rooms for these users.
$coursegroups = groups_get_all_groups(courseid: $course->id);
$numberofgroups = count($coursegroups);
// If no groups available, nothing to show.
if ($numberofgroups === 0) {
return '';
}
$readygroups = [];
foreach ($coursegroups as $coursegroup) {
$communication = self::load_by_group(
groupid: $coursegroup->id,
context: $coursecontext,
);
$roomstatus = $communication->get_communication_room_url() ? 'ready' : 'pending';
if ($roomstatus === 'ready') {
$readygroups[$communication->get_processor()->get_id()] = $communication->get_communication_room_url();
}
}
if (!empty($readygroups)) {
$highestkey = max(array_keys($readygroups));
$url = $readygroups[$highestkey];
}
}
return empty($url) ? '' : $url;
}
/**
* Get the enrolled users for course.
*
* @param stdClass $course The course object.
* @return array
*/
public static function get_enrolled_users_for_course(stdClass $course): array {
global $CFG;
require_once($CFG->libdir . '/enrollib.php');
return array_column(
enrol_get_course_users(courseid: $course->id),
'id',
);
}
/**
* Get the course communication status notification for course.
*
* @param \stdClass $course The course object.
*/
public static function get_course_communication_status_notification(\stdClass $course): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
// Get the group mode for this course.
$groupmode = $course->groupmode ?? get_course(courseid: $course->id)->groupmode;
$coursecontext = \context_course::instance(courseid: $course->id);
// If group mode is not set then just handle the course communication for these users.
if ((int)$groupmode === NOGROUPS) {
$communication = self::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
$communication->show_communication_room_status_notification();
} else {
// If group mode is set then handle the group communication rooms for these users.
$coursegroups = groups_get_all_groups(courseid: $course->id);
$numberofgroups = count($coursegroups);
// If no groups available, nothing to show.
if ($numberofgroups === 0) {
return;
}
$numberofreadygroups = 0;
foreach ($coursegroups as $coursegroup) {
$communication = self::load_by_group(
groupid: $coursegroup->id,
context: $coursecontext,
);
$roomstatus = $communication->get_communication_room_url() ? 'ready' : 'pending';
switch ($roomstatus) {
case 'ready':
$numberofreadygroups ++;
break;
case 'pending':
$pendincommunicationobject = $communication;
break;
}
}
if ($numberofgroups === $numberofreadygroups) {
$communication->show_communication_room_status_notification();
} else {
$pendincommunicationobject->show_communication_room_status_notification();
}
}
}
/**
* Update course communication according to course data.
* Course can have course or group rooms. Group mode enabling will create rooms for groups.
*
* @param stdClass $course The course data
* @param bool $changesincoursecat Whether the course moved to a different category
*/
public static function update_course_communication_instance(
stdClass $course,
bool $changesincoursecat
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
// Check if provider is selected.
$provider = $course->selectedcommunication ?? null;
// If the course moved to hidden category, set provider to none.
if ($changesincoursecat && empty($course->visible)) {
$provider = processor::PROVIDER_NONE;
}
// Get the course context.
$coursecontext = \context_course::instance(courseid: $course->id);
// Get the course image.
$courseimage = course_get_courseimage(course: $course);
// Get the course communication instance.
$coursecommunication = self::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
// Attempt to get the communication provider if it wasn't provided in the data.
if (empty($provider)) {
$provider = $coursecommunication->get_provider();
}
// This nasty logic is here because of hide course doesn't pass anything in the data object.
if (!empty($course->communicationroomname)) {
$coursecommunicationroomname = $course->communicationroomname;
} else {
$coursecommunicationroomname = $course->fullname ?? get_course($course->id)->fullname;
}
// List of enrolled users for course communication.
$enrolledusers = self::get_enrolled_users_for_course(course: $course);
// Check for group mode, we will have to get the course data again as the group info is not always in the object.
$groupmode = $course->groupmode ?? get_course(courseid: $course->id)->groupmode;
// If group mode is disabled, get the communication information for creating room for a course.
if ((int)$groupmode === NOGROUPS) {
// Remove all the members from active group rooms if there is any.
$coursegroups = groups_get_all_groups(courseid: $course->id);
foreach ($coursegroups as $coursegroup) {
$communication = self::load_by_group(
groupid: $coursegroup->id,
context: $coursecontext,
);
// Remove the members from the group room.
$communication->remove_all_members_from_room();
// Now delete the group room.
$communication->update_room(active: processor::PROVIDER_INACTIVE);
}
// Now create/update the course room.
$communication = self::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
$communication->configure_room_and_membership_by_provider(
provider: $provider,
instance: $course,
communicationroomname: $coursecommunicationroomname,
users: $enrolledusers,
instanceimage: $courseimage,
);
} else {
// Update the group communication instances.
self::update_group_communication_instances_for_course(
course: $course,
provider: $provider,
);
// Remove all the members for the course room if instance available.
$communication = self::load_by_course(
courseid: $course->id,
context: $coursecontext,
provider: $provider === processor::PROVIDER_NONE ? null : $provider,
);
if ($communication->get_processor() === null) {
// If a course communication instance is not created, create one.
$communication->create_and_configure_room(
communicationroomname: $coursecommunicationroomname,
avatar: $courseimage,
instance: $course,
queue: false,
);
} else {
$communication->remove_all_members_from_room();
// Now update the course communication instance with the latest changes.
// We are not making room for this instance as it is a group mode enabled course.
// If provider is none, then we will make the room inactive, otherwise always active in group mode.
$communication->update_room(
active: $provider === processor::PROVIDER_NONE ? processor::PROVIDER_INACTIVE : processor::PROVIDER_ACTIVE,
communicationroomname: $coursecommunicationroomname,
avatar: $courseimage,
instance: $course,
queue: false,
);
}
}
}
/**
* Update the group communication instances.
*
* @param stdClass $course The course object.
* @param string $provider The provider name.
*/
public static function update_group_communication_instances_for_course(
stdClass $course,
string $provider,
): void {
$coursegroups = groups_get_all_groups(courseid: $course->id);
$coursecontext = \context_course::instance(courseid: $course->id);
$allaccessgroupusers = self::get_users_has_access_to_all_groups(
userids: self::get_enrolled_users_for_course(course: $course),
courseid: $course->id,
);
foreach ($coursegroups as $coursegroup) {
$groupuserstoadd = array_column(
groups_get_members(groupid: $coursegroup->id),
'id',
);
foreach ($allaccessgroupusers as $allaccessgroupuser) {
if (!in_array($allaccessgroupuser, $groupuserstoadd, true)) {
$groupuserstoadd[] = $allaccessgroupuser;
}
}
// Now create/update the group room.
$communication = self::load_by_group(
groupid: $coursegroup->id,
context: $coursecontext,
);
$communication->configure_room_and_membership_by_provider(
provider: $provider,
instance: $course,
communicationroomname: $coursegroup->name,
users: $groupuserstoadd,
);
}
}
}
+634
View File
@@ -0,0 +1,634 @@
<?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 core_communication;
use context_course;
use core\hook\access\after_role_assigned;
use core\hook\access\after_role_unassigned;
use core_enrol\hook\before_enrol_instance_delete;
use core_enrol\hook\after_enrol_instance_status_updated;
use core_enrol\hook\after_user_enrolled;
use core_enrol\hook\before_user_enrolment_update;
use core_enrol\hook\before_user_enrolment_remove;
use core_course\hook\after_course_created;
use core_course\hook\before_course_delete;
use core_course\hook\after_course_updated;
use core_group\hook\after_group_created;
use core_group\hook\after_group_deleted;
use core_group\hook\after_group_membership_added;
use core_group\hook\after_group_membership_removed;
use core_group\hook\after_group_updated;
use core_user\hook\before_user_deleted;
use core_user\hook\before_user_update;
/**
* Hook listener for communication api.
*
* @package core_communication
* @copyright 2023 Safat Shahin <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class hook_listener {
/**
* Get the course and group object for the group hook.
*
* @param mixed $hook The hook object.
* @return array
*/
protected static function get_group_and_course_data_for_group_hook(mixed $hook): array {
$group = $hook->groupinstance;
$course = helper::get_course(
courseid: $group->courseid,
);
return [
$group,
$course,
];
}
/**
* Communication api call to create room for a group if course has group mode enabled.
*
* @param after_group_created $hook The group created hook.
*/
public static function create_group_communication(
after_group_created $hook,
): void {
[$group, $course] = self::get_group_and_course_data_for_group_hook(
hook: $hook,
);
// Check if group mode enabled before handling the communication.
if (!helper::is_group_mode_enabled_for_course(course: $course)) {
return;
}
$coursecontext = \context_course::instance(courseid: $course->id);
// Get the course communication instance to set the provider.
$coursecommunication = helper::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
$communication = api::load_by_instance(
context: $coursecontext,
component: constants::GROUP_COMMUNICATION_COMPONENT,
instancetype: constants::GROUP_COMMUNICATION_INSTANCETYPE,
instanceid: $group->id,
provider: $coursecommunication->get_provider(),
);
$communication->create_and_configure_room(
communicationroomname: $group->name,
instance: $course,
);
// As it's a new group, we need to add the users with all access group role to the room.
$enrolledusers = helper::get_enrolled_users_for_course(course: $course);
$userstoadd = helper::get_users_has_access_to_all_groups(
userids: $enrolledusers,
courseid: $course->id,
);
$communication->add_members_to_room(
userids: $userstoadd,
queue: false,
);
}
/**
* Communication api call to update room for a group if course has group mode enabled.
*
* @param after_group_updated $hook The group updated hook.
*/
public static function update_group_communication(
after_group_updated $hook,
): void {
[$group, $course] = self::get_group_and_course_data_for_group_hook(
hook: $hook,
);
// Check if group mode enabled before handling the communication.
if (!helper::is_group_mode_enabled_for_course(course: $course)) {
return;
}
$coursecontext = \context_course::instance(courseid: $course->id);
$communication = helper::load_by_group(
groupid: $group->id,
context: $coursecontext,
);
// If the name didn't change, then we don't need to update the room.
if ($group->name === $communication->get_room_name()) {
return;
}
$communication->update_room(
active: processor::PROVIDER_ACTIVE,
communicationroomname: $group->name,
instance: $course,
);
}
/**
* Delete the communication room for a group if course has group mode enabled.
*
* @param after_group_deleted $hook The group deleted hook.
*/
public static function delete_group_communication(
after_group_deleted $hook
): void {
[$group, $course] = self::get_group_and_course_data_for_group_hook(
hook: $hook,
);
// Check if group mode enabled before handling the communication.
if (!helper::is_group_mode_enabled_for_course(course: $course)) {
return;
}
$context = context_course::instance($course->id);
$communication = helper::load_by_group(
groupid: $group->id,
context: $context,
);
$communication->delete_room();
}
/**
* Add members to group room when a new member is added to the group.
*
* @param after_group_membership_added $hook The group membership added hook.
*/
public static function add_members_to_group_room(
after_group_membership_added $hook,
): void {
[$group, $course] = self::get_group_and_course_data_for_group_hook(
hook: $hook,
);
// Check if group mode enabled before handling the communication.
if (!helper::is_group_mode_enabled_for_course(course: $course)) {
return;
}
$context = context_course::instance($course->id);
$communication = helper::load_by_group(
groupid: $group->id,
context: $context,
);
$communication->add_members_to_room(
userids: $hook->userids,
);
}
/**
* Remove members from the room when a member is removed from group room.
*
* @param after_group_membership_removed $hook The group membership removed hook.
*/
public static function remove_members_from_group_room(
after_group_membership_removed $hook,
): void {
[$group, $course] = self::get_group_and_course_data_for_group_hook(
hook: $hook,
);
// Check if group mode enabled before handling the communication.
if (!helper::is_group_mode_enabled_for_course(course: $course)) {
return;
}
$context = context_course::instance($course->id);
$communication = helper::load_by_group(
groupid: $group->id,
context: $context,
);
$communication->remove_members_from_room(
userids: $hook->userids,
);
}
/**
* Create course communication instance.
*
* @param after_course_created $hook The course created hook.
*/
public static function create_course_communication(
after_course_created $hook,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
$course = $hook->course;
// Check for default provider config setting.
$defaultprovider = get_config(
plugin: 'moodlecourse',
name: 'coursecommunicationprovider',
);
$provider = $course->selectedcommunication ?? $defaultprovider;
if (empty($provider) && $provider === processor::PROVIDER_NONE) {
return;
}
// Check for group mode, we will have to get the course data again as the group info is not always in the object.
$createcourseroom = true;
$creategrouprooms = false;
$coursedata = get_course(courseid: $course->id);
$groupmode = $course->groupmode ?? $coursedata->groupmode;
if ((int)$groupmode !== NOGROUPS) {
$createcourseroom = false;
$creategrouprooms = true;
}
// Prepare the communication api data.
$courseimage = course_get_courseimage(course: $course);
$communicationroomname = !empty($course->communicationroomname) ? $course->communicationroomname : $coursedata->fullname;
$coursecontext = \context_course::instance(courseid: $course->id);
// Communication api call for course communication.
$communication = \core_communication\api::load_by_instance(
context: $coursecontext,
component: constants::COURSE_COMMUNICATION_COMPONENT,
instancetype: constants::COURSE_COMMUNICATION_INSTANCETYPE,
instanceid: $course->id,
provider: $provider,
);
$communication->create_and_configure_room(
communicationroomname: $communicationroomname,
avatar: $courseimage,
instance: $course,
queue: $createcourseroom,
);
// Communication api call for group communication.
if ($creategrouprooms) {
helper::update_group_communication_instances_for_course(
course: $course,
provider: $provider,
);
} else {
$enrolledusers = helper::get_enrolled_users_for_course(course: $course);
$communication->add_members_to_room(
userids: $enrolledusers,
queue: false,
);
}
}
/**
* Update the course communication instance.
*
* @param after_course_updated $hook The course updated hook.
*/
public static function update_course_communication(
after_course_updated $hook,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
$course = $hook->course;
$oldcourse = $hook->oldcourse;
$changeincoursecat = $hook->changeincoursecat;
$groupmode = $course->groupmode ?? get_course($course->id)->groupmode;
if ($changeincoursecat || $groupmode !== $oldcourse->groupmode) {
helper::update_course_communication_instance(
course: $course,
changesincoursecat: $changeincoursecat,
);
}
}
/**
* Delete course communication data and remove members.
* Course can have communication data if it is a group or a course.
* This action is important to perform even if the experimental feature is disabled.
*
* @param before_course_delete $hook The course deleted hook.
*/
public static function delete_course_communication(
before_course_delete $hook,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
$course = $hook->course;
$groupmode = $course->groupmode ?? get_course(courseid: $course->id)->groupmode;
$coursecontext = \context_course::instance(courseid: $course->id);
// If group mode is not set then just handle the course communication room.
if ((int)$groupmode === NOGROUPS) {
$communication = helper::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
$communication->delete_room();
} else {
// If group mode is set then handle the group communication rooms.
$coursegroups = groups_get_all_groups(courseid: $course->id);
foreach ($coursegroups as $coursegroup) {
$communication = helper::load_by_group(
groupid: $coursegroup->id,
context: $coursecontext,
);
$communication->delete_room();
}
}
}
/**
* Update the room membership for the user updates.
*
* @param before_user_update $hook The user updated hook.
*/
public static function update_user_room_memberships(
before_user_update $hook,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
$user = $hook->user;
$currentuserrecord = $hook->currentuserdata;
// Get the user courses.
$usercourses = enrol_get_users_courses(userid: $user->id);
// If the user is suspended then remove the user from all the rooms.
// Otherwise add the user to all the rooms for the courses the user enrolled in.
if (!empty($currentuserrecord) && isset($user->suspended) && $currentuserrecord->suspended !== $user->suspended) {
// Decide the action for the communication api for the user.
$memberaction = ($user->suspended === 0) ? 'add_members_to_room' : 'remove_members_from_room';
foreach ($usercourses as $usercourse) {
helper::update_course_communication_room_membership(
course: $usercourse,
userids: [$user->id],
memberaction: $memberaction,
);
}
}
}
/**
* Delete all room memberships for a user.
*
* @param before_user_deleted $hook The user deleted hook.
*/
public static function delete_user_room_memberships(
before_user_deleted $hook,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
$user = $hook->user;
foreach (enrol_get_users_courses(userid: $user->id) as $course) {
$groupmode = $course->groupmode ?? get_course(courseid: $course->id)->groupmode;
$coursecontext = \context_course::instance(courseid: $course->id);
if ((int)$groupmode === NOGROUPS) {
$communication = helper::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
$communication->get_room_user_provider()->remove_members_from_room(userids: [$user->id]);
$communication->get_processor()->delete_instance_user_mapping(userids: [$user->id]);
} else {
// If group mode is set then handle the group communication rooms.
$coursegroups = groups_get_all_groups(courseid: $course->id);
foreach ($coursegroups as $coursegroup) {
$communication = helper::load_by_group(
groupid: $coursegroup->id,
context: $coursecontext,
);
$communication->get_room_user_provider()->remove_members_from_room(userids: [$user->id]);
$communication->get_processor()->delete_instance_user_mapping(userids: [$user->id]);
}
}
}
}
/**
* Update the room membership of the user for role assigned in a course.
*
* @param after_role_assigned|after_role_unassigned $hook
*/
public static function update_user_membership_for_role_changes(
after_role_assigned|after_role_unassigned $hook,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
$context = $hook->context;
if ($coursecontext = $context->get_course_context(strict: false)) {
helper::update_course_communication_room_membership(
course: get_course(courseid: $coursecontext->instanceid),
userids: [$hook->userid],
memberaction: 'update_room_membership',
);
}
}
/**
* Update the communication memberships for enrol status change.
*
* @param after_enrol_instance_status_updated $hook The enrol status updated hook.
*/
public static function update_communication_memberships_for_enrol_status_change(
after_enrol_instance_status_updated $hook,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
$enrolinstance = $hook->enrolinstance;
// No need to do anything for guest instances.
if ($enrolinstance->enrol === 'guest') {
return;
}
$newstatus = $hook->newstatus;
// Check if a valid status is given.
if (
$newstatus !== ENROL_INSTANCE_ENABLED ||
$newstatus !== ENROL_INSTANCE_DISABLED
) {
return;
}
// Check if the status provided is valid.
switch ($newstatus) {
case ENROL_INSTANCE_ENABLED:
$action = 'add_members_to_room';
break;
case ENROL_INSTANCE_DISABLED:
$action = 'remove_members_from_room';
break;
default:
return;
}
global $DB;
$instanceusers = $DB->get_records(
table: 'user_enrolments',
conditions: ['enrolid' => $enrolinstance->id, 'status' => ENROL_USER_ACTIVE],
);
$enrolledusers = array_column($instanceusers, 'userid');
helper::update_course_communication_room_membership(
course: get_course(courseid: $enrolinstance->courseid),
userids: $enrolledusers,
memberaction: $action,
);
}
/**
* Remove the communication instance memberships when an enrolment instance is deleted.
*
* @param before_enrol_instance_delete $hook The enrol instance deleted hook.
*/
public static function remove_communication_memberships_for_enrol_instance_deletion(
before_enrol_instance_delete $hook,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
$enrolinstance = $hook->enrolinstance;
// No need to do anything for guest instances.
if ($enrolinstance->enrol === 'guest') {
return;
}
global $DB;
$instanceusers = $DB->get_records(
table: 'user_enrolments',
conditions: ['enrolid' => $enrolinstance->id, 'status' => ENROL_USER_ACTIVE],
);
$enrolledusers = array_column($instanceusers, 'userid');
helper::update_course_communication_room_membership(
course: get_course(courseid: $enrolinstance->courseid),
userids: $enrolledusers,
memberaction: 'remove_members_from_room',
);
}
/**
* Add communication instance membership for an enrolled user.
*
* @param after_user_enrolled $hook The user enrolled hook.
*/
public static function add_communication_membership_for_enrolled_user(
after_user_enrolled $hook,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
$enrolinstance = $hook->enrolinstance;
// No need to do anything for guest instances.
if ($enrolinstance->enrol === 'guest') {
return;
}
helper::update_course_communication_room_membership(
course: get_course($enrolinstance->courseid),
userids: [$hook->get_userid()],
memberaction: 'add_members_to_room',
);
}
/**
* Update the communication instance membership for the user enrolment updates.
*
* @param before_user_enrolment_update $hook The user enrolment updated hook.
*/
public static function update_communication_membership_for_updated_user_enrolment(
before_user_enrolment_update $hook,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
$enrolinstance = $hook->enrolinstance;
// No need to do anything for guest instances.
if ($enrolinstance->enrol === 'guest') {
return;
}
$userenrolmentinstance = $hook->userenrolmentinstance;
$statusmodified = $hook->statusmodified;
$timeendmodified = $hook->timeendmodified;
if (
($statusmodified && ((int) $userenrolmentinstance->status === 1)) ||
($timeendmodified && $userenrolmentinstance->timeend !== 0 && (time() > $userenrolmentinstance->timeend))
) {
$action = 'remove_members_from_room';
} else {
$action = 'add_members_to_room';
}
helper::update_course_communication_room_membership(
course: get_course($enrolinstance->courseid),
userids: [$hook->get_userid()],
memberaction: $action,
);
}
/**
* Remove communication instance membership for an enrolled user.
*
* @param before_user_enrolment_remove $hook The user unenrolled hook.
*/
public static function remove_communication_membership_for_unenrolled_user(
before_user_enrolment_remove $hook,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
$enrolinstance = $hook->enrolinstance;
// No need to do anything for guest instances.
if ($enrolinstance->enrol === 'guest') {
return;
}
helper::update_course_communication_room_membership(
course: get_course($enrolinstance->courseid),
userids: [$hook->get_userid()],
memberaction: 'remove_members_from_room',
);
}
}
+10 -1
View File
@@ -111,7 +111,7 @@ class processor {
/**
* Update the communication instance with any changes.
*
* @param null|int $active Active state of the instance (processor::PROVIDER_ACTIVE or processor::PROVIDER_INACTIVE)
* @param null|string $active Active state of the instance (processor::PROVIDER_ACTIVE or processor::PROVIDER_INACTIVE)
* @param null|string $roomname The room name
*/
public function update_instance(
@@ -489,6 +489,15 @@ class processor {
return $this->instancedata->roomname;
}
/**
* Get provider active status.
*
* @return int
*/
public function get_provider_status(): int {
return $this->instancedata->active;
}
/**
* Get communication instance id.
*
+194
View File
@@ -16,9 +16,13 @@
namespace core_communication;
use core_communication\task\add_members_to_room_task;
use core_communication\task\create_and_configure_room_task;
use communication_matrix\matrix_test_helper_trait;
use core_communication\task\synchronise_provider_task;
use core_communication\task\synchronise_providers_task;
use core_communication\task\remove_members_from_room;
use core_communication\task\update_room_task;
defined('MOODLE_INTERNAL') || die();
@@ -351,4 +355,194 @@ class api_test extends \advanced_testcase {
$adhoctask = \core\task\manager::get_adhoc_tasks(synchronise_provider_task::class);
$this->assertCount(2, $adhoctask);
}
/**
* Test the removal of all members from the room.
*
* @covers ::remove_all_members_from_room
*/
public function test_remove_all_members_from_room(): void {
$course = $this->get_course();
$userid = $this->get_user()->id;
$communication = \core_communication\api::load_by_instance(
context: \core\context\course::instance($course->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
$communication->add_members_to_room([$userid]);
// Now test the removing members from a room.
$communication->remove_all_members_from_room();
// Test the remove members tasks added.
$adhoctask = \core\task\manager::get_adhoc_tasks(remove_members_from_room::class);
$this->assertCount(1, $adhoctask);
}
/**
* Test the configuration of room changes as well as the membership with the change of provider.
*
* @covers ::configure_room_and_membership_by_provider
*/
public function test_configure_room_and_membership_by_provider(): void {
global $DB;
$course = $this->get_course('Sampleroom', 'none');
$userid = $this->get_user()->id;
$provider = 'communication_matrix';
$communication = \core_communication\api::load_by_instance(
context: \core\context\course::instance($course->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
$communication->configure_room_and_membership_by_provider(
provider: $provider,
instance: $course,
communicationroomname: $course->fullname,
users: [$userid],
);
$communication->reload();
// Test that the task to create a room is added.
$adhoctask = \core\task\manager::get_adhoc_tasks(create_and_configure_room_task::class);
$this->assertCount(1, $adhoctask);
// Test that no update tasks are added.
$adhoctask = \core\task\manager::get_adhoc_tasks(update_room_task::class);
$this->assertCount(0, $adhoctask);
// Test that the task to add members to room is not added, as we are adding the user mapping not the task.
$adhoctask = \core\task\manager::get_adhoc_tasks(add_members_to_room_task::class);
$this->assertCount(0, $adhoctask);
// Now delete all the ad-hoc tasks.
$DB->delete_records('task_adhoc');
// Now disable the provider by setting none.
$communication->configure_room_and_membership_by_provider(
provider: processor::PROVIDER_NONE,
instance: $course,
communicationroomname: $course->fullname,
users: [$userid],
);
$communication->reload();
// Test that the task to delete a room is added.
$adhoctask = \core\task\manager::get_adhoc_tasks(update_room_task::class);
$this->assertCount(1, $adhoctask);
// Test that the task to remove members from room is added.
$adhoctask = \core\task\manager::get_adhoc_tasks(remove_members_from_room::class);
$this->assertCount(1, $adhoctask);
// Now delete all the ad-hoc tasks.
$DB->delete_records('task_adhoc');
// Now try to set the same none provider again.
$communication->configure_room_and_membership_by_provider(
provider: processor::PROVIDER_NONE,
instance: $course,
communicationroomname: $course->fullname,
users: [$userid],
);
// Test that no communicaiton task is added.
$adhoctask = \core\task\manager::get_adhoc_tasks(create_and_configure_room_task::class);
$this->assertCount(0, $adhoctask);
$adhoctask = \core\task\manager::get_adhoc_tasks(update_room_task::class);
$this->assertCount(0, $adhoctask);
$adhoctask = \core\task\manager::get_adhoc_tasks(add_members_to_room_task::class);
$this->assertCount(0, $adhoctask);
$adhoctask = \core\task\manager::get_adhoc_tasks(remove_members_from_room::class);
$this->assertCount(0, $adhoctask);
// Now let's change it back to matrix and test the update task is added.
$communication->configure_room_and_membership_by_provider(
provider: $provider,
instance: $course,
communicationroomname: $course->fullname,
users: [$userid],
);
$communication->reload();
// Test create task is not added because communication has been created in the past.
$adhoctask = \core\task\manager::get_adhoc_tasks(create_and_configure_room_task::class);
$this->assertCount(0, $adhoctask);
// Test an update task added.
$adhoctask = \core\task\manager::get_adhoc_tasks(update_room_task::class);
$this->assertCount(1, $adhoctask);
// Test add membership task is added.
$adhoctask = \core\task\manager::get_adhoc_tasks(add_members_to_room_task::class);
$this->assertCount(1, $adhoctask);
// Now delete all the ad-hoc tasks.
$DB->delete_records('task_adhoc');
// Now change the provider to another one.
$communication->configure_room_and_membership_by_provider(
provider: 'communication_customlink',
instance: $course,
communicationroomname: $course->fullname,
users: [$userid],
);
$communication->reload();
// Remove membership and update room task for the previous provider.
// Create room task for new one.
$adhoctask = \core\task\manager::get_adhoc_tasks(update_room_task::class);
$this->assertCount(1, $adhoctask);
$adhoctask = \core\task\manager::get_adhoc_tasks(remove_members_from_room::class);
$this->assertCount(1, $adhoctask);
$adhoctask = \core\task\manager::get_adhoc_tasks(create_and_configure_room_task::class);
$this->assertCount(1, $adhoctask);
// Now delete all the ad-hoc tasks.
$DB->delete_records('task_adhoc');
// Now disable the provider.
$communication->configure_room_and_membership_by_provider(
provider: processor::PROVIDER_NONE,
instance: $course,
communicationroomname: $course->fullname,
users: [$userid],
);
$communication->reload();
// Should have one update and one remove task.
$adhoctask = \core\task\manager::get_adhoc_tasks(update_room_task::class);
$this->assertCount(1, $adhoctask);
// This provider doesn't have any membership, so no remove task.
$adhoctask = \core\task\manager::get_adhoc_tasks(remove_members_from_room::class);
$this->assertCount(0, $adhoctask);
// Now delete all the ad-hoc tasks.
$DB->delete_records('task_adhoc');
// Now enable the same provider again.
$communication->configure_room_and_membership_by_provider(
provider: $provider,
instance: $course,
communicationroomname: $course->fullname,
users: [$userid],
);
// Now it should have one update and one add task.
$adhoctask = \core\task\manager::get_adhoc_tasks(update_room_task::class);
$this->assertCount(1, $adhoctask);
$adhoctask = \core\task\manager::get_adhoc_tasks(add_members_to_room_task::class);
$this->assertCount(1, $adhoctask);
}
}
@@ -52,7 +52,8 @@ trait communication_test_helper_trait {
*/
protected function get_course(
string $roomname = 'Sampleroom',
string $provider = 'communication_matrix'
string $provider = 'communication_matrix',
array $extrafields = [],
): \stdClass {
$this->setup_communication_configs();
@@ -61,7 +62,7 @@ trait communication_test_helper_trait {
'communicationroomname' => $roomname,
];
return $this->getDataGenerator()->create_course($records);
return $this->getDataGenerator()->create_course(array_merge($records, $extrafields));
}
/**
+223
View File
@@ -0,0 +1,223 @@
<?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 core_communication;
use communication_matrix\matrix_test_helper_trait;
use core_communication\processor as communication_processor;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../provider/matrix/tests/matrix_test_helper_trait.php');
require_once(__DIR__ . '/communication_test_helper_trait.php');
/**
* Test communication helper methods.
*
* @package core_communication
* @copyright 2023 Safat Shahin <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \core_communication\helper
*/
class helper_test extends \advanced_testcase {
use communication_test_helper_trait;
use matrix_test_helper_trait;
public function setUp(): void {
parent::setUp();
$this->resetAfterTest();
$this->setup_communication_configs();
$this->initialise_mock_server();
}
/**
* Test load_by_group.
*/
public function test_load_by_group(): void {
// As communication is created by default.
$course = $this->get_course(
extrafields: ['groupmode' => SEPARATEGROUPS],
);
$group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$context = \context_course::instance(courseid: $course->id);
$groupcommunication = helper::load_by_group(
groupid: $group->id,
context: $context,
);
$this->assertInstanceOf(
expected: communication_processor::class,
actual: $groupcommunication->get_processor(),
);
}
/**
* Test load_by_course.
*/
public function test_load_by_course(): void {
// As communication is created by default.
$course = $this->get_course();
$coursecontext = \context_course::instance(courseid: $course->id);
$coursecommunication = helper::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
$this->assertInstanceOf(
expected: communication_processor::class,
actual: $coursecommunication->get_processor(),
);
}
/**
* Test get_access_to_all_group_cap_users.
*/
public function test_get_users_has_access_to_all_groups(): void {
global $DB;
// Set up the data with course, group, user etc.
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$course = $this->get_course();
$coursecontext = \context_course::instance(courseid: $course->id);
// Enrol user1 as teacher.
$teacherrole = $DB->get_record(
table: 'role',
conditions: ['shortname' => 'manager'],
);
$this->getDataGenerator()->enrol_user(
userid: $user1->id,
courseid: $course->id,
);
role_assign(
roleid: $teacherrole->id,
userid: $user1->id,
contextid: $coursecontext->id,
);
// Enrol user2 as student.
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
$this->getDataGenerator()->enrol_user(
userid: $user2->id,
courseid: $course->id,
);
role_assign(
roleid: $studentrole->id,
userid: $user2->id,
contextid: $coursecontext->id,
);
$allgroupaccessusers = helper::get_users_has_access_to_all_groups(
userids: [$user1->id, $user2->id],
courseid: $course->id,
);
$this->assertContains(
needle: $user1->id,
haystack: $allgroupaccessusers,
);
$this->assertNotContains(
needle: $user2->id,
haystack: $allgroupaccessusers,
);
}
/**
* Test update_communication_room_membership.
*/
public function test_update_communication_room_membership(): void {
global $DB;
// Set up the data with course, group, user etc.
$user = $this->getDataGenerator()->create_user();
$course = $this->get_course();
$coursecontext = \context_course::instance(courseid: $course->id);
$teacherrole = $DB->get_record(
table: 'role',
conditions: ['shortname' => 'manager'],
);
$this->getDataGenerator()->enrol_user(
userid: $user->id,
courseid: $course->id,
);
role_assign(
roleid: $teacherrole->id,
userid:$user->id,
contextid: $coursecontext->id,
);
// Now remove members from room.
helper::update_course_communication_room_membership(
course: $course,
userids: [$user->id],
memberaction: 'remove_members_from_room',
);
// Now test that there is communication instances for the course and the user removed from that instance.
$coursecommunication = helper::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
// Check the user is added for course communication instance.
$courseusers = $coursecommunication->get_processor()->get_all_delete_flagged_userids();
$courseusers = reset($courseusers);
$this->assertEquals(
expected: $user->id,
actual: $courseusers,
);
// Now add members to room.
helper::update_course_communication_room_membership(
course: $course,
userids: [$user->id],
memberaction: 'add_members_to_room',
);
$coursecommunication->reload();
// Check the user is added for course communication instance.
$courseusers = $coursecommunication->get_processor()->get_instance_userids();
$courseusers = reset($courseusers);
$this->assertEquals(
expected: $user->id,
actual: $courseusers,
);
// Now update membership.
helper::update_course_communication_room_membership(
course: $course,
userids: [$user->id],
memberaction: 'update_room_membership',
);
$coursecommunication->reload();
// Check the user is added for course communication instance.
$courseusers = $coursecommunication->get_processor()->get_instance_userids();
$courseusers = reset($courseusers);
$this->assertEquals(
expected: $user->id,
actual: $courseusers,
);
// Now try using invalid action.
$this->expectException('coding_exception');
$this->expectExceptionMessage('Invalid action provided.');
helper::update_course_communication_room_membership(
course: $course,
userids: [$user->id],
memberaction: 'a_funny_action',
);
}
}
+461
View File
@@ -0,0 +1,461 @@
<?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 core_communication;
use communication_matrix\matrix_test_helper_trait;
use core_communication\task\add_members_to_room_task;
use core_communication\task\create_and_configure_room_task;
use core_communication\task\delete_room_task;
use core_communication\task\update_room_membership_task;
use core_communication\task\update_room_task;
use core_communication\processor as communication_processor;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../provider/matrix/tests/matrix_test_helper_trait.php');
require_once(__DIR__ . '/communication_test_helper_trait.php');
/**
* Test communication hook listeners.
*
* @package core_communication
* @copyright 2023 Safat Shahin <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \core_communication\hook_listener
*/
class hook_listener_test extends \advanced_testcase {
use communication_test_helper_trait;
use matrix_test_helper_trait;
public function setUp(): void {
parent::setUp();
$this->resetAfterTest();
$this->setup_communication_configs();
$this->initialise_mock_server();
}
/**
* Test create_group_communication.
*/
public function test_create_update_delete_group_communication(): void {
global $DB;
$course = $this->get_course(
extrafields: ['groupmode' => SEPARATEGROUPS],
);
$coursecontext = \context_course::instance(courseid: $course->id);
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
// Enrol user1 as teacher.
$teacherrole = $DB->get_record(
table: 'role',
conditions: ['shortname' => 'manager'],
);
$this->getDataGenerator()->enrol_user(
userid: $user1->id,
courseid: $course->id,
);
role_assign(
roleid: $teacherrole->id,
userid: $user1->id,
contextid: $coursecontext->id,
);
// Enrol user2 as student.
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
$this->getDataGenerator()->enrol_user(
userid: $user2->id,
courseid: $course->id,
);
role_assign(
roleid: $studentrole->id,
userid: $user2->id,
contextid: $coursecontext->id,
);
$group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$context = \context_course::instance($course->id);
$groupcommunication = helper::load_by_group(
groupid: $group->id,
context: $context,
);
$this->assertInstanceOf(
expected: communication_processor::class,
actual: $groupcommunication->get_processor(),
);
$this->assertEquals(
expected: $group->id,
actual: $groupcommunication->get_processor()->get_instance_id(),
);
// Task to create room should be added.
$adhoctask = \core\task\manager::get_adhoc_tasks(create_and_configure_room_task::class);
$this->assertCount(1, $adhoctask);
// Task to add members to room should not be there as the room is yet to be created.
$adhoctask = \core\task\manager::get_adhoc_tasks(add_members_to_room_task::class);
$this->assertCount(0, $adhoctask);
// Only users with access to all groups should be added to the room at this point.
$groupcommunicationusers = $groupcommunication->get_processor()->get_all_userids_for_instance();
$this->assertEquals(
expected: [$user1->id],
actual: $groupcommunicationusers,
);
// Now delete all the ad-hoc tasks.
$DB->delete_records('task_adhoc');
// Now cann the update group but don't change the group name.
groups_update_group($group);
// No task should be added as nothing changed.
$adhoctask = \core\task\manager::get_adhoc_tasks(update_room_task::class);
$this->assertCount(0, $adhoctask);
// Now change the group name.
$changedgroupname = 'Changedgroupname';
$group->name = $changedgroupname;
groups_update_group($group);
// Now one task should be there to update the group room name.
$adhoctask = \core\task\manager::get_adhoc_tasks(update_room_task::class);
$this->assertCount(1, $adhoctask);
$groupcommunication->reload();
$this->assertEquals(
expected: $changedgroupname,
actual: $groupcommunication->get_processor()->get_room_name(),
);
// Now delete the group.
groups_delete_group($group->id);
$adhoctask = \core\task\manager::get_adhoc_tasks(delete_room_task::class);
$this->assertCount(1, $adhoctask);
}
/**
* Test add_members_to_group_room.
*/
public function test_add_members_to_group_room(): void {
global $DB;
$course = $this->get_course(
extrafields: ['groupmode' => SEPARATEGROUPS],
);
$coursecontext = \context_course::instance(courseid: $course->id);
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
// Enrol user1 as teacher.
$teacherrole = $DB->get_record(
table: 'role',
conditions: ['shortname' => 'manager'],
);
$this->getDataGenerator()->enrol_user(
userid: $user1->id,
courseid: $course->id,
);
role_assign(
roleid: $teacherrole->id,
userid: $user1->id,
contextid: $coursecontext->id,
);
// Enrol user2 as student.
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
$this->getDataGenerator()->enrol_user(
userid: $user2->id,
courseid: $course->id,
);
role_assign(
roleid: $studentrole->id,
userid: $user2->id,
contextid: $coursecontext->id,
);
$group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
// Now check if the teacher is added to the group room as the teacher has access to all groups.
$context = \context_course::instance($course->id);
$groupcommunication = helper::load_by_group(
groupid: $group->id,
context: $context,
);
// Now the communication instance should not have the student added yet.
$this->assertNotContains(
needle: $user2->id,
haystack: $groupcommunication->get_processor()->get_all_userids_for_instance(),
);
groups_add_member(
grouporid: $group,
userorid: $user2,
);
// Now it should have the student.
$this->assertContains(
needle: $user2->id,
haystack: $groupcommunication->get_processor()->get_all_userids_for_instance(),
);
}
/**
* Test if the course instances are created properly for course default provider.
*/
public function test_course_default_provider(): void {
$defaultprovider = 'communication_matrix';
// Set the default communication for course.
set_config(
name: 'coursecommunicationprovider',
value: $defaultprovider,
plugin: 'moodlecourse',
);
// Test that the default communication is created for course mode.
$course = $this->get_course();
$coursecontext = \context_course::instance(courseid: $course->id);
$coursecommunication = helper::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
$this->assertEquals(
expected: $defaultprovider,
actual: $coursecommunication->get_provider(),
);
$this->assertEquals(
expected: 'core_course',
actual: $coursecommunication->get_processor()->get_component(),
);
$this->assertEquals(
expected: $course->id,
actual: $coursecommunication->get_processor()->get_instance_id(),
);
}
/**
* Test update_course_communication.
*/
public function test_update_course_communication(): void {
global $DB;
// Set up the data with course, group, user etc.
$user = $this->getDataGenerator()->create_user();
$course = $this->get_course();
$group = $this->getDataGenerator()->create_group(record: ['courseid' => $course->id]);
$coursecontext = \context_course::instance(courseid: $course->id);
$teacherrole = $DB->get_record(
table: 'role',
conditions: ['shortname' => 'teacher'],
);
$this->getDataGenerator()->enrol_user(
userid: $user->id,
courseid: $course->id,
);
role_assign(
roleid: $teacherrole->id,
userid: $user->id,
contextid: $coursecontext->id,
);
groups_add_member(
grouporid: $group->id,
userorid: $user->id,
);
// Now test that there is communication instances for the course and the user added for that instance.
$coursecommunication = helper::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
$this->assertInstanceOf(
expected: communication_processor::class,
actual: $coursecommunication->get_processor(),
);
// Check the user is added for course communication instance.
$courseusers = $coursecommunication->get_processor()->get_all_userids_for_instance();
$courseusers = reset($courseusers);
$this->assertEquals(
expected: $user->id,
actual: $courseusers,
);
// Group should not have any instance yet.
$groupcommunication = helper::load_by_group(
groupid: $group->id,
context: $coursecontext,
);
$this->assertNull(actual: $groupcommunication->get_processor());
// Now update the course.
$course->groupmode = SEPARATEGROUPS;
$course->selectedcommunication = 'communication_matrix';
update_course(data: $course);
// Now there should be a group communication instance.
$groupcommunication->reload();
$this->assertInstanceOf(
expected: communication_processor::class,
actual: $groupcommunication->get_processor(),
);
// The course communication instance must be active.
$coursecommunication->reload();
$this->assertInstanceOf(
expected: communication_processor::class,
actual: $coursecommunication->get_processor(),
);
// All the course instance users must be marked as deleted.
$coursecommunication->reload();
$courseusers = $coursecommunication->get_processor()->get_all_delete_flagged_userids();
$courseusers = reset($courseusers);
$this->assertEquals(
expected: $user->id,
actual: $courseusers,
);
// Group instance should have the user.
$groupusers = $groupcommunication->get_processor()->get_all_userids_for_instance();
$groupusers = reset($groupusers);
$this->assertEquals(
expected: $user->id,
actual: $groupusers,
);
// Now disable the communication instance for the course.
$course->selectedcommunication = communication_processor::PROVIDER_NONE;
update_course(data: $course);
// Now both course and group instance should be disabled.
$coursecommunication->reload();
$this->assertNull(actual: $coursecommunication->get_processor());
$groupcommunication->reload();
$this->assertNull(actual: $groupcommunication->get_processor());
}
/**
* Test create_course_communication_instance.
*/
public function test_create_course_communication_instance(): void {
$course = $this->get_course();
$coursecontext = \context_course::instance(courseid: $course->id);
$coursecommunication = helper::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
$processor = $coursecommunication->get_processor();
$this->assertEquals(
expected: 'communication_matrix',
actual: $processor->get_provider(),
);
$this->assertEquals(
expected: 'Sampleroom',
actual: $processor->get_room_name(),
);
}
/**
* Test delete_course_communication.
*/
public function test_delete_course_communication(): void {
$course = $this->get_course();
delete_course(
courseorid: $course,
showfeedback: false,
);
$adhoctask = \core\task\manager::get_adhoc_tasks(delete_room_task::class);
$this->assertCount(1, $adhoctask);
}
/**
* Test update of room membership when user changes occur.
*/
public function test_update_user_room_memberships(): void {
global $DB;
$user = $this->getDataGenerator()->create_user();
$course = $this->get_course();
$coursecontext = \context_course::instance($course->id);
$teacherrole = $DB->get_record('role', ['shortname' => 'teacher']);
$this->getDataGenerator()->enrol_user($user->id, $course->id);
role_assign($teacherrole->id, $user->id, $coursecontext->id);
$coursecommunication = helper::load_by_course($course->id, $coursecontext);
$courseusers = $coursecommunication->get_processor()->get_all_userids_for_instance();
$courseusers = reset($courseusers);
$this->assertEquals($user->id, $courseusers);
$user->suspended = 1;
user_update_user($user, false);
$coursecommunication->reload();
$courseusers = $coursecommunication->get_processor()->get_all_delete_flagged_userids();
$courseusers = reset($courseusers);
$this->assertEquals($user->id, $courseusers);
}
/**
* Test deletion of user room memberships when a user is deleted.
*/
public function test_delete_user_room_memberships(): void {
global $DB;
$user = $this->getDataGenerator()->create_user();
$course = $this->get_course();
$coursecontext = \context_course::instance($course->id);
$teacherrole = $DB->get_record('role', ['shortname' => 'teacher']);
$this->getDataGenerator()->enrol_user($user->id, $course->id);
role_assign($teacherrole->id, $user->id, $coursecontext->id);
delete_user($user);
$coursecommunication = helper::load_by_course($course->id, $coursecontext);
$courseusers = $coursecommunication->get_processor()->get_all_userids_for_instance();
$this->assertEmpty($courseusers);
}
/**
* Test user room membership updates with role changes in a course.
*/
public function test_update_user_membership_for_role_changes(): void {
global $DB;
$user = $this->getDataGenerator()->create_user();
$course = $this->get_course();
$coursecontext = \context_course::instance($course->id);
$teacherrole = $DB->get_record('role', ['shortname' => 'teacher']);
$this->getDataGenerator()->enrol_user($user->id, $course->id);
$adhoctask = \core\task\manager::get_adhoc_tasks(update_room_membership_task::class);
$this->assertCount(1, $adhoctask);
role_assign($teacherrole->id, $user->id, $coursecontext->id);
$adhoctask = \core\task\manager::get_adhoc_tasks(update_room_membership_task::class);
$this->assertCount(2, $adhoctask);
}
}
+1 -12
View File
@@ -4444,18 +4444,7 @@ EOD;
*/
public function communication_url(): string {
global $COURSE;
$url = '';
if ($COURSE->id !== SITEID) {
$comm = \core_communication\api::load_by_instance(
context: \core\context\course::instance($COURSE->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $COURSE->id,
);
$url = $comm->get_communication_room_url();
}
return !empty($url) ? $url : '';
return \core_communication\helper::get_course_communication_url($COURSE);
}
/**