diff --git a/communication/classes/api.php b/communication/classes/api.php new file mode 100644 index 00000000000..ab7f74220c5 --- /dev/null +++ b/communication/classes/api.php @@ -0,0 +1,402 @@ +. + +namespace core_communication; + +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\remove_members_from_room; +use core_communication\task\update_room_task; +use stdClass; + +/** + * Class api is the public endpoint of the communication api. This class is the point of contact for api usage. + * + * Communication api allows to add ad-hoc tasks to the queue to perform actions on the communication providers. This api will + * not allow any immediate actions to be performed on the communication providers. It will only add the tasks to the queue. The + * exception has been made for deletion of members in case of deleting the user. This is because the user will not be available. + * The member management api part allows run actions immediately if required. + * + * Communication api does allow to have form elements related to communication api in the required forms. This is done by using + * the form_definition method. This method will add the form elements to the form. + * + * @package core_communication + * @copyright 2023 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class api { + + /** + * @var null|processor $communication The communication settings object + */ + private ?processor $communication; + + /** + * Communication handler constructor to manage and handle all communication related actions. + * + * This class is the entrypoint for all kinda usages. + * It will be used by the other api to manage the communication providers. + * + * @param string $component The component of the item for the instance + * @param string $instancetype The type of the item for the instance + * @param int $instanceid The id of the instance + * + */ + private function __construct( + private string $component, + private string $instancetype, + private int $instanceid + ) { + $this->communication = processor::load_by_instance( + $this->component, + $this->instancetype, + $this->instanceid, + ); + } + + /** + * Get the communication processor object. + * + * @param string $component The component of the item for the instance + * @param string $instancetype The type of the item for the instance + * @param int $instanceid The id of the instance + * @return api + */ + public static function load_by_instance( + string $component, + string $instancetype, + int $instanceid + ): self { + return new self($component, $instancetype, $instanceid); + } + + /** + * Check if the communication api is enabled. + */ + public static function is_available(): bool { + return (bool) get_config('core', 'enablecommunicationsubsystem'); + } + + /** + * Get the communication room url. + * + * @return string|null + */ + public function get_communication_room_url(): ?string { + return $this->communication?->get_room_url(); + } + + /** + * Get the list of plugins for form selection. + * + * @return array + */ + public static function get_communication_plugin_list_for_form(): array { + // Add the option to have communication disabled. + $selection[processor::PROVIDER_NONE] = get_string('nocommunicationselected', 'communication'); + $communicationplugins = \core\plugininfo\communication::get_enabled_plugins(); + foreach ($communicationplugins as $pluginname => $notusing) { + $selection['communication_' . $pluginname] = get_string('pluginname', 'communication_'. $pluginname); + } + return $selection; + } + + /** + * Define the form elements for the communication api. + * This method will be called from the form definition method of the instance. + * + * @param \MoodleQuickForm $mform The form element + */ + public function form_definition(\MoodleQuickForm $mform): void { + $mform->addElement('header', 'communication', get_string('communication', 'communication')); + + // List the communication providers. + $communicationproviders = self::get_communication_plugin_list_for_form(); + $mform->addElement( + 'select', + 'selectedcommunication', + get_string('seleccommunicationprovider', 'communication'), + $communicationproviders); + $mform->addHelpButton('selectedcommunication', 'seleccommunicationprovider', 'communication'); + $mform->setDefault('selectedcommunication', processor::PROVIDER_NONE); + + // Room name for the communication provider. + $mform->addElement('text', + 'communicationroomname', + get_string('communicationroomname', 'communication'), + 'maxlength="100" size="20"'); + $mform->addHelpButton('communicationroomname', 'communicationroomname', 'communication'); + $mform->setType('communicationroomname', PARAM_TEXT); + $mform->hideIf( + 'communicationroomname', + 'selectedcommunication', + 'eq', + processor::PROVIDER_NONE); + } + + /** + * Get the avatar file record for the avatar for filesystem. + * + * @param string $filename The filename of the avatar + * @return stdClass + */ + public function get_avatar_filerecord(string $filename): stdClass { + return (object) [ + 'contextid' => \context_system::instance()->id, + 'component' => 'core_communication', + 'filearea' => 'avatar', + 'filename' => $filename, + 'filepath' => '/', + 'itemid' => $this->communication->get_id(), + ]; + } + + /** + * + * Get the avatar file. + * + * If null is set, then delete the old area file and set the avatarfilename to null. + * This will make sure the plugin api deletes the avatar from the room. + * + * @param string|null $datauri The datauri of the avatar + * @return bool + */ + public function set_avatar_from_datauri_or_filepath(?string $datauri): bool { + global $DB; + + $currentfilename = $DB->get_field('communication', 'avatarfilename', ['id' => $this->communication->get_id()]); + if (empty($datauri) && empty($currentfilename)) { + return false; + } + + $currentfilerecord = $this->communication->get_avatar(); + if (!empty($datauri) && !empty($currentfilerecord)) { + $currentfilehash = $currentfilerecord->get_contenthash(); + $updatedfilehash = \file_storage::hash_from_string(file_get_contents($datauri)); + + // No update required. + if ($currentfilehash === $updatedfilehash) { + return false; + } + } + + $context = \context_system::instance(); + $filename = null; + + $fs = get_file_storage(); + $fs->delete_area_files( + $context->id, + 'core_communication', + 'avatar', + $this->communication->get_id() + ); + + if (!empty($datauri)) { + $filename = "avatar.svg"; + $fs->create_file_from_string($this->get_avatar_filerecord($filename), file_get_contents($datauri)); + } + + $DB->set_field('communication', 'avatarfilename', $filename, ['id' => $this->communication->get_id()]); + return true; + } + + /** + * Set the form data if the data is already available. + * + * @param \stdClass $instance The instance object + */ + public function set_data(\stdClass $instance): void { + if (!empty($instance->id) && $this->communication) { + $instance->selectedcommunication = $this->communication->get_provider(); + $instance->communicationroomname = $this->communication->get_room_name(); + } + } + + /** + * Get the communication provider. + * + * @return string + */ + public function get_provider(): string { + if (!$this->communication) { + return ''; + } + return $this->communication->get_provider(); + } + + /** + * Create a communication ad-hoc task for create operation. + * This method will add a task to the queue to create the room. + * + * @param string $selectedcommunication The selected communication provider + * @param string $communicationroomname The communication room name + * @param string|null $avatarurl The avatar url + */ + public function create_and_configure_room( + string $selectedcommunication, + string $communicationroomname, + ?string $avatarurl = null, + ): void { + + if ($selectedcommunication !== processor::PROVIDER_NONE && $selectedcommunication !== '') { + // Create communication record. + $this->communication = processor::create_instance( + $selectedcommunication, + $this->instanceid, + $this->component, + $this->instancetype, + $communicationroomname, + ); + + // Set the avatar. + if (!empty($avatarurl)) { + $this->set_avatar_from_datauri_or_filepath($avatarurl); + } + + // Add ad-hoc task to create the provider room. + create_and_configure_room_task::queue( + $this->communication, + ); + } + } + + /** + * Create a communication ad-hoc task for update operation. + * This method will add a task to the queue to update the room. + * + * @param string $selectedprovider The selected communication provider + * @param string $communicationroomname The communication room name + * @param string|null $avatarurl The avatar url + */ + public function update_room( + string $selectedprovider, + string $communicationroomname, + ?string $avatarurl = null, + ): void { + + // Existing object found, let's update the communication record and associated actions. + if ($this->communication !== null) { + // Get the previous data to compare for update. + $previousroomname = $this->communication->get_room_name(); + $previousprovider = $this->communication->get_provider(); + + // Update communication record. + $this->communication->update_instance($selectedprovider, $communicationroomname); + + // Update the avatar. + $imageupdaterequired = $this->set_avatar_from_datauri_or_filepath($avatarurl); + + // 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; + } + + // Add ad-hoc task to update the provider room if the room name changed. + if ( + $previousprovider === $selectedprovider && + ($previousroomname !== $communicationroomname || $imageupdaterequired) + ) { + update_room_task::queue( + $this->communication, + ); + } else if ( + $previousprovider !== $selectedprovider + ) { + // Add ad-hoc task to create the provider room. + create_and_configure_room_task::queue( + $this->communication, + ); + } + } else { + // The instance didn't have any communication record, so create one. + $this->create_and_configure_room($selectedprovider, $communicationroomname, $avatarurl); + } + } + + /** + * Create a communication ad-hoc task for delete operation. + * This method will add a task to the queue to delete the room. + */ + public function delete_room(): void { + if ($this->communication !== null) { + // Add the ad-hoc task to remove the room data from the communication table and associated provider actions. + delete_room_task::queue( + $this->communication, + ); + } + } + + /** + * Create a communication ad-hoc task for add members operation and add the user mapping. + * + * This method will add a task to the queue to add the room users. + * + * @param array $userids The user ids to add to the room + * @param bool $queue Whether to queue the task or not + */ + public function add_members_to_room(array $userids, bool $queue = true): void { + // No communication object? something not done right. + if (!$this->communication) { + return; + } + + // No userids? don't bother doing anything. + if (empty($userids)) { + return; + } + + $this->communication->create_instance_user_mapping($userids); + + if ($queue) { + add_members_to_room_task::queue( + $this->communication + ); + } + } + + /** + * Create a communication ad-hoc task for remove members operation or action immediately. + * + * This method will add a task to the queue to remove the room users. + * + * @param array $userids The user ids to remove from the room + * @param bool $queue Whether to queue the task or not + */ + public function remove_members_from_room(array $userids, bool $queue = true): void { + // No communication object? something not done right. + if (!$this->communication) { + return; + } + + if ($this->communication->get_provider() === processor::PROVIDER_NONE) { + return; + } + + // No user ids? don't bother doing anything. + if (empty($userids)) { + return; + } + + $this->communication->add_delete_user_flag($userids); + + if ($queue) { + remove_members_from_room::queue( + $this->communication + ); + } + } +} diff --git a/communication/classes/communication_provider.php b/communication/classes/communication_provider.php new file mode 100644 index 00000000000..3fa7127a18d --- /dev/null +++ b/communication/classes/communication_provider.php @@ -0,0 +1,38 @@ +. + +namespace core_communication; + +/** + * A base communication provider. + * + * This interface should be used to declare support for the instantiation method for communication providers. + * + * Every communication provider must, as a minimum, implement this provider. + * + * @package core_communication + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface communication_provider { + + /** + * A base communication provider. + * + * @param processor $communication The communication object + */ + public static function load_for_instance(processor $communication): self; +} diff --git a/communication/classes/processor.php b/communication/classes/processor.php new file mode 100644 index 00000000000..576717a6722 --- /dev/null +++ b/communication/classes/processor.php @@ -0,0 +1,563 @@ +. + +namespace core_communication; + +use stdClass; +use stored_file; + +/** + * Class processor to manage the base operations of the providers. + * + * This class is responsible for creating, updating, deleting and loading the communication instance, associated actions. + * + * @package core_communication + * @copyright 2023 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class processor { + + /** @var string The magic 'none' provider */ + public const PROVIDER_NONE = 'none'; + + /** @var int The provider active flag */ + public const PROVIDER_ACTIVE = 1; + + /** @var int The provider inactive flag */ + public const PROVIDER_INACTIVE = 0; + + /** @var null|communication_provider|user_provider|room_chat_provider|room_user_provider The provider class */ + private communication_provider|user_provider|room_chat_provider|room_user_provider|null $provider = null; + + /** + * Communication processor constructor. + * + * @param stdClass $instancedata The instance data object + */ + protected function __construct( + private stdClass $instancedata, + ) { + $providercomponent = $this->instancedata->provider; + if (!\core\plugininfo\communication::is_plugin_enabled($providercomponent)) { + throw new \moodle_exception('communicationproviderdisabled', 'core_communication', '', $providercomponent); + } + $providerclass = $this->get_classname_for_provider($providercomponent); + if (!class_exists($providerclass)) { + throw new \moodle_exception('communicationproviderclassnotfound', 'core_communication', '', $providerclass); + } + + if (!is_a($providerclass, communication_provider::class, true)) { + // At the moment we only have one communication provider interface. + // In the future, we may have others, at which point we will support the newest first and + // emit a debugging notice for older ones. + throw new \moodle_exception('communicationproviderclassinvalid', 'core_communication', '', $providerclass); + } + + $this->provider = $providerclass::load_for_instance($this); + } + + /** + * Create communication instance. + * + * @param string $provider The communication provider + * @param int $instanceid The instance id + * @param string $component The component name + * @param string $instancetype The instance type + * @param string $roomname The room name + * @return processor|null + */ + public static function create_instance( + string $provider, + int $instanceid, + string $component, + string $instancetype, + string $roomname, + ): ?self { + global $DB; + + if ($provider === self::PROVIDER_NONE) { + return null; + } + $record = (object) [ + 'provider' => $provider, + 'instanceid' => $instanceid, + 'component' => $component, + 'instancetype' => $instancetype, + 'roomname' => $roomname, + 'avatarfilename' => null, + 'active' => self::PROVIDER_ACTIVE, + ]; + $record->id = $DB->insert_record('communication', $record); + + return new self($record); + } + + /** + * Update communication instance. + * + * @param string $provider The communication provider + * @param string $roomname The room name + */ + public function update_instance( + string $provider, + string $roomname, + ): void { + + global $DB; + if ($provider === self::PROVIDER_NONE) { + $this->instancedata->active = self::PROVIDER_INACTIVE; + } else { + $this->instancedata->provider = $provider; + $this->instancedata->active = self::PROVIDER_ACTIVE; + } + $this->instancedata->roomname = $roomname; + + $DB->update_record('communication', $this->instancedata); + } + + /** + * Delete communication data. + */ + public function delete_instance(): void { + global $DB; + $DB->delete_records('communication', ['id' => $this->instancedata->id]); + } + + /** + * Get non synced instance user ids for the instance. + * + * @param bool $synced The synced status + * @param bool $deleted The deleted status + * @return array + */ + public function get_instance_userids(bool $synced = false, bool $deleted = false): array { + global $DB; + return $DB->get_fieldset_select( + 'communication_user', + 'userid', + 'commid = ? AND synced = ? AND deleted = ?', + [$this->instancedata->id, (int) $synced, (int) $deleted] + ); + } + + /** + * Get existing instance user ids. + * + * @return array + */ + public function get_all_userids_for_instance(): array { + global $DB; + return $DB->get_fieldset_select( + 'communication_user', + 'userid', + 'commid = ?', + [$this->instancedata->id] + ); + } + + /** + * Create communication user record for mapping and sync. + * + * @param array $userids The user ids + */ + public function create_instance_user_mapping(array $userids): void { + global $DB; + + // Check if user ids exits in existing user ids. + $useridstoadd = array_diff($userids, $this->get_all_userids_for_instance()); + + foreach ($useridstoadd as $userid) { + $record = (object) [ + 'commid' => $this->instancedata->id, + 'userid' => $userid, + ]; + $DB->insert_record('communication_user', $record); + } + $this->mark_users_as_not_deleted($userids); + } + + /** + * Mark users as not deleted for the instance. + * + * @param array $userids The user ids + */ + public function mark_users_as_not_deleted(array $userids): void { + global $DB; + + if (empty($userids)) { + return; + } + + $DB->set_field_select( + 'communication_user', + 'deleted', + 0, + 'commid = ? AND userid IN (' . implode(',', $userids) . ')', + [$this->instancedata->id] + ); + } + + /** + * Mark users as synced for the instance. + * + * @param array $userids The user ids + */ + public function mark_users_as_synced(array $userids): void { + global $DB; + + if (empty($userids)) { + return; + } + + $DB->set_field_select( + 'communication_user', + 'synced', + 1, + 'commid = ? AND userid IN (' . implode(',', $userids) . ')', + [$this->instancedata->id] + ); + } + + /** + * Reset users sync flag for the instance. + * + * @param array $userids The user ids + */ + public function reset_users_sync_flag(array $userids): void { + global $DB; + + if (empty($userids)) { + return; + } + + $DB->set_field_select( + 'communication_user', + 'synced', + 0, + 'commid = ? AND userid IN (' . implode(',', $userids) . ')', + [$this->instancedata->id] + ); + } + + /** + * Delete users flag for the instance users. + * + * @param array $userids The user ids + */ + public function add_delete_user_flag(array $userids): void { + global $DB; + + if (empty($userids)) { + return; + } + + $DB->set_field_select( + 'communication_user', + 'deleted', + 1, + 'commid = ? AND userid IN (' . implode(',', $userids) . ')', + [$this->instancedata->id] + ); + } + + /** + * Delete communication user record for userid. + * + * @param array $userids The user ids + */ + public function delete_instance_user_mapping(array $userids): void { + global $DB; + + if (empty($userids)) { + return; + } + + $DB->delete_records_select( + 'communication_user', + 'commid = ? AND userid IN (' . implode(',', $userids) . ')', + [$this->instancedata->id] + ); + } + + /** + * Delete communication user record for userid who are not synced. + * + * @param array $userids The user ids + */ + public function delete_instance_non_synced_user_mapping(array $userids): void { + global $DB; + + if (empty($userids)) { + return; + } + + $DB->delete_records_select( + 'communication_user', + 'commid = ? AND userid IN (' . implode(',', $userids) . ') AND synced = ?' , + [$this->instancedata->id, 0] + ); + } + + /** + * Delete communication user record for instance. + */ + public function delete_user_mappings_for_instance(): void { + global $DB; + $DB->delete_records('communication_user', [ + 'commid' => $this->instancedata->id, + ]); + } + + /** + * Load communication instance by id. + * + * @param int $id The communication instance id + * @return processor|null + */ + public static function load_by_id(int $id): ?self { + global $DB; + + if ($record = $DB->get_record('communication', ['id' => $id])) { + return new self($record); + } + + return null; + } + + /** + * Load communication instance by instance id. + * + * @param string $component The component name + * @param string $instancetype The instance type + * @param int $instanceid The instance id + * @return processor|null + */ + public static function load_by_instance( + string $component, + string $instancetype, + int $instanceid + ): ?self { + + global $DB; + + $record = $DB->get_record('communication', [ + 'instanceid' => $instanceid, + 'component' => $component, + 'instancetype' => $instancetype, + ]); + + if ($record) { + return new self($record); + } + + return null; + } + + /** + * Check if communication instance is active. + * + * @return bool + */ + public function is_instance_active(): bool { + return $this->instancedata->active; + } + + /** + * Get communication provider class name. + * + * @param string $component The component name. + * @return string + */ + private function get_classname_for_provider(string $component): string { + return "{$component}\\communication_feature"; + } + + /** + * Get communication instance id after creating the instance in communication table. + * + * @return int + */ + public function get_id(): int { + return $this->instancedata->id; + } + + /** + * Get communication instance id. + * + * @return string + */ + public function get_component(): string { + return $this->instancedata->component; + } + + /** + * Get communication provider. + * + * @return string|null + */ + public function get_provider(): ?string { + // var_dump($this->instancedata);die; + if ((int)$this->instancedata->active === self::PROVIDER_ACTIVE) { + return $this->instancedata->provider; + } + return self::PROVIDER_NONE; + } + + /** + * Get room name. + * + * @return string|null + */ + public function get_room_name(): ?string { + return $this->instancedata->roomname; + } + + /** + * Get communication instance id. + * + * @return room_chat_provider + */ + public function get_room_provider(): room_chat_provider { + $this->require_api_enabled(); + $this->require_room_features(); + return $this->provider; + } + + /** + * Get communication instance id. + * + * @return user_provider + */ + public function get_user_provider(): user_provider { + $this->require_api_enabled(); + $this->require_user_features(); + return $this->provider; + } + + /** + * Get communication instance id. + * + * @return room_user_provider + */ + public function get_room_user_provider(): room_user_provider { + $this->require_api_enabled(); + $this->require_room_features(); + $this->require_room_user_features(); + return $this->provider; + } + + /** + * Get communication instance id. + * + * @return bool + */ + public function supports_user_features(): bool { + return ($this->provider instanceof user_provider); + } + + /** + * Get communication instance id. + * + * @return bool + */ + public function supports_room_user_features(): bool { + if (!$this->supports_user_features()) { + return false; + } + + if (!$this->supports_room_features()) { + return false; + } + + return ($this->provider instanceof room_user_provider); + } + + /** + * Get communication instance id. + */ + public function require_user_features(): void { + if (!$this->supports_user_features()) { + throw new \coding_exception('User features are not supported by the provider'); + } + } + + /** + * Get communication instance id. + * + * @return bool + */ + public function supports_room_features(): bool { + return ($this->provider instanceof room_chat_provider); + } + + /** + * Check if communication api is enabled. + */ + public function require_api_enabled(): void { + if (!api::is_available()) { + throw new \coding_exception('Communication API is not enabled, please enable it from experimental features'); + } + } + + /** + * Get communication instance id. + */ + public function require_room_features(): void { + if (!$this->supports_room_features()) { + throw new \coding_exception('room features are not supported by the provider'); + } + } + + /** + * Get communication instance id. + */ + public function require_room_user_features(): void { + if (!$this->supports_room_user_features()) { + throw new \coding_exception('room features are not supported by the provider'); + } + } + + /** + * Get communication instance id. + * + * @return bool|\stored_file + */ + public function get_avatar(): ?stored_file { + $fs = get_file_storage(); + $file = $fs->get_file( + (\context_system::instance())->id, + 'core_communication', + 'avatar', + $this->instancedata->id, + '/', + $this->instancedata->avatarfilename, + ); + + return $file ? $file : null; + } + + /** + * Get a room url. + * + * @return string|null + */ + public function get_room_url(): ?string { + if ($this->provider && $this->is_instance_active()) { + return $this->get_room_provider()->get_chat_room_url(); + } + return null; + } +} diff --git a/communication/classes/room_chat_provider.php b/communication/classes/room_chat_provider.php new file mode 100644 index 00000000000..234b1c2fc8b --- /dev/null +++ b/communication/classes/room_chat_provider.php @@ -0,0 +1,51 @@ +. + +namespace core_communication; + +/** + * Class communication_room_base to manage the room operations of communication providers. + * + * Every plugin that supports room operation must implement/extend this class in the plugin. + * + * @package core_communication + * @copyright 2023 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface room_chat_provider { + + /** + * Create a provider room when a instance is created. + */ + public function create_chat_room(): bool; + + /** + * Update a provider room when a instance is updated. + */ + public function update_chat_room(): bool; + + /** + * Delete a provider room when a instance is deleted. + */ + public function delete_chat_room(): bool; + + /** + * Generate a room url if there is a room. + * + * @return string|null + */ + public function get_chat_room_url(): ?string; +} diff --git a/communication/classes/room_user_provider.php b/communication/classes/room_user_provider.php new file mode 100644 index 00000000000..f9a04e48a4b --- /dev/null +++ b/communication/classes/room_user_provider.php @@ -0,0 +1,40 @@ +. + +namespace core_communication; + +/** + * Class communication_user_base to manage communication provider users. + * + * @package core_communication + * @copyright 2023 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface room_user_provider { + /** + * Add members to communication room. + * + * @param array $userids The user ids to be added + */ + public function add_members_to_room(array $userids): void; + + /** + * Remove members from room. + * + * @param array $userids The user ids to be removed + */ + public function remove_members_from_room(array $userids): void; +} diff --git a/communication/classes/task/add_members_to_room_task.php b/communication/classes/task/add_members_to_room_task.php new file mode 100644 index 00000000000..b49931c6b18 --- /dev/null +++ b/communication/classes/task/add_members_to_room_task.php @@ -0,0 +1,64 @@ +. + +namespace core_communication\task; + +use core\task\adhoc_task; +use core_communication\processor; + +/** + * Class add_members_to_room_task to add the task to add members to the room and execute the task to action the addition. + * + * @package core_communication + * @copyright 2023 David Woloszyn + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class add_members_to_room_task extends adhoc_task { + + public function execute() { + // Initialize the custom data operation to be used for the action. + $data = $this->get_custom_data(); + + // Call the communication api to action the operation. + $communication = processor::load_by_id($data->id); + + if ($communication === null) { + mtrace("Skipping room creation because the instance does not exist"); + return; + } + + $communication->get_room_user_provider()->add_members_to_room($communication->get_instance_userids()); + } + + /** + * Queue the task for the next run. + * + * @param processor $communication The communication processor to perform the action on + */ + public static function queue( + processor $communication + ): void { + + // Add ad-hoc task to update the provider room. + $task = new self(); + $task->set_custom_data([ + 'id' => $communication->get_id() + ]); + + // Queue the task for the next run. + \core\task\manager::queue_adhoc_task($task); + } +} diff --git a/communication/classes/task/create_and_configure_room_task.php b/communication/classes/task/create_and_configure_room_task.php new file mode 100644 index 00000000000..1327e4e723d --- /dev/null +++ b/communication/classes/task/create_and_configure_room_task.php @@ -0,0 +1,76 @@ +. + +namespace core_communication\task; + +use core\task\adhoc_task; +use core_communication\processor; + +/** + * Class create_and_configure_room_task to add a task to create a room and execute the task to action the creation. + * + * this task will be queued by the communication api and will use the communication handler api to action the creation. + * + * @package core_communication + * @copyright 2023 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class create_and_configure_room_task extends adhoc_task { + + public function execute() { + $data = $this->get_custom_data(); + + // Call the communication api to action the operation. + $communication = processor::load_by_id($data->id); + + if ($communication === null) { + mtrace("Skipping room creation because the instance does not exist"); + return; + } + + if (!$communication->is_instance_active()) { + mtrace("Skipping room creation because the instance is not active"); + return; + } + + // If the room is created successfully, add members to the room. + if ($communication->get_room_provider()->create_chat_room()) { + add_members_to_room_task::queue( + $communication + ); + } + + } + + /** + * Queue the task for the next run. + * + * @param processor $communication The communication processor to perform the action on + */ + public static function queue( + processor $communication, + ): void { + + // Add ad-hoc task to update the provider room. + $task = new self(); + $task->set_custom_data([ + 'id' => $communication->get_id() + ]); + + // Queue the task for the next run. + \core\task\manager::queue_adhoc_task($task); + } +} diff --git a/communication/classes/task/delete_room_task.php b/communication/classes/task/delete_room_task.php new file mode 100644 index 00000000000..fea70c04a68 --- /dev/null +++ b/communication/classes/task/delete_room_task.php @@ -0,0 +1,72 @@ +. + +namespace core_communication\task; + +use core\task\adhoc_task; +use core_communication\processor; + +/** + * Class delete_room_task to add a task to delete a room and execute the task to action the deletion. + * + * this task will be queued by the communication api and will use the communication handler api to action the deletion. + * + * @package core_communication + * @copyright 2023 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class delete_room_task extends adhoc_task { + + public function execute() { + $data = $this->get_custom_data(); + + $communication = processor::load_by_id($data->id); + + if ($communication === null) { + mtrace("Skipping room creation because the instance does not exist"); + return; + } + + // First remove the members from the room. + $communication->get_room_user_provider()->remove_members_from_room($communication->get_instance_userids(true, true)); + // Now remove any mapping for users who are not in the room. + $communication->delete_instance_non_synced_user_mapping($communication->get_instance_userids(false, true)); + + // Now delete the room. + if ($communication->get_room_provider()->delete_chat_room()) { + $communication->delete_instance(); + } + } + + /** + * Queue the task for the next run. + * + * @param processor $communication The communication processor to perform the action on + */ + public static function queue( + processor $communication, + ): void { + + // Add ad-hoc task to update the provider room. + $task = new self(); + $task->set_custom_data([ + 'id' => $communication->get_id() + ]); + + // Queue the task for the next run. + \core\task\manager::queue_adhoc_task($task); + } +} diff --git a/communication/classes/task/remove_members_from_room.php b/communication/classes/task/remove_members_from_room.php new file mode 100644 index 00000000000..e681f949e01 --- /dev/null +++ b/communication/classes/task/remove_members_from_room.php @@ -0,0 +1,67 @@ +. + +namespace core_communication\task; + +use core\task\adhoc_task; +use core_communication\processor; + +/** + * Class remove_members_from_room to add the task to remove members to the room and execute the task to action the removal. + * + * @package core_communication + * @copyright 2023 David Woloszyn + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class remove_members_from_room extends adhoc_task { + + public function execute() { + // Initialize the custom data operation to be used for the action. + $data = $this->get_custom_data(); + + // Call the communication api to action the operation. + $communication = processor::load_by_id($data->id); + + if ($communication === null) { + mtrace("Skipping room creation because the instance does not exist"); + return; + } + + $communication->get_room_user_provider()->remove_members_from_room($communication->get_instance_userids(true, true)); + + // Now remove any mapping for users who are not in the room. + $communication->delete_instance_non_synced_user_mapping($communication->get_instance_userids(false, true)); + } + + /** + * Queue the task for the next run. + * + * @param processor $communication The communication processor to perform the action on + */ + public static function queue( + processor $communication + ): void { + + // Add ad-hoc task to update the provider room. + $task = new self(); + $task->set_custom_data([ + 'id' => $communication->get_id() + ]); + + // Queue the task for the next run. + \core\task\manager::queue_adhoc_task($task); + } +} diff --git a/communication/classes/task/update_room_task.php b/communication/classes/task/update_room_task.php new file mode 100644 index 00000000000..b487e28e4d9 --- /dev/null +++ b/communication/classes/task/update_room_task.php @@ -0,0 +1,65 @@ +. + +namespace core_communication\task; + +use core\task\adhoc_task; +use core_communication\processor; + +/** + * Class update_room_task to add a task to update a room and execute the task to action the update. + * + * this task will be queued by the communication api and will use the communication handler api to action the updates. + * + * @package core_communication + * @copyright 2023 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class update_room_task extends adhoc_task { + + public function execute() { + $data = $this->get_custom_data(); + + // Call the communication api to action the operation. + $communication = processor::load_by_id($data->id); + + if ($communication === null) { + mtrace("Skipping room creation because the instance does not exist"); + return; + } + + $communication->get_room_provider()->update_chat_room(); + } + + /** + * Queue the task for the next run. + * + * @param processor $communication The communication processor to perform the action on + */ + public static function queue( + processor $communication, + ): void { + + // Add ad-hoc task to update the provider room. + $task = new self(); + $task->set_custom_data([ + 'id' => $communication->get_id() + ]); + + // Queue the task for the next run. + \core\task\manager::queue_adhoc_task($task); + } +} diff --git a/communication/classes/user_provider.php b/communication/classes/user_provider.php new file mode 100644 index 00000000000..d59df679f61 --- /dev/null +++ b/communication/classes/user_provider.php @@ -0,0 +1,34 @@ +. + +namespace core_communication; + +/** + * Class communication_user_base to manage communication provider users. + * + * @package core_communication + * @copyright 2023 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface user_provider { + + /** + * Create members. + * + * @param array $userid The users ids to be created + */ + public function create_members(array $userid): void; +} diff --git a/communication/lib.php b/communication/lib.php new file mode 100644 index 00000000000..84ce24ed542 --- /dev/null +++ b/communication/lib.php @@ -0,0 +1,61 @@ +. + +/** + * Callback and library methods for core communication. + * + * @package core_communication + * @copyright 2023 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Serve the files from the core_communication file areas. + * + * @param stdClass $course the course object + * @param stdClass $cm the course module object + * @param context $context the context + * @param string $filearea the name of the file area + * @param array $args extra arguments (itemid, path) + * @param bool $forcedownload whether force download + * @param array $options additional options affecting the file serving + */ +function core_communication_pluginfile( + $course, + $cm, + $context, + $filearea, + $args, + $forcedownload, + array $options = [] +): void { + + if ($filearea !== 'avatar') { + return; + } + + $itemid = array_shift($args); + $filename = array_pop($args); + + // Retrieve the file from the Files API. + $fs = get_file_storage(); + $file = $fs->get_file($context->id, 'core_communication', $filearea, $itemid, '/', $filename); + if (!$file) { + return; + } + + send_file($file, $filename); +} diff --git a/communication/tests/api_test.php b/communication/tests/api_test.php new file mode 100644 index 00000000000..6dda30881be --- /dev/null +++ b/communication/tests/api_test.php @@ -0,0 +1,315 @@ +. + +namespace core_communication; + +defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/communication_test_helper_trait.php'); + +/** + * Class api_test to test the communication public api and its associated methods. + * + * @package core_communication + * @category test + * @copyright 2023 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \core_communication\api + */ +class api_test extends \advanced_testcase { + + use communication_test_helper_trait; + + public function setUp(): void { + parent::setUp(); + $this->resetAfterTest(); + $this->setup_communication_configs(); + } + + /** + * Test the communication plugin list for the form element returns the correct number of plugins. + * + * @covers ::get_communication_plugin_list_for_form + */ + public function test_get_communication_plugin_list_for_form(): void { + $communicationplugins = \core_communication\api::get_communication_plugin_list_for_form(); + // Get the communication plugins. + $plugins = \core_component::get_plugin_list('communication'); + // Check the number of plugins matches plus 1 as we have none in the selection. + $this->assertCount(count($plugins) + 1, $communicationplugins); + } + + /** + * Test set data to the instance. + * + * @covers ::set_data + */ + public function test_set_data(): void { + $course = $this->get_course(); + + $communication = \core_communication\api::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + // Sample data. + $roomname = 'Sampleroom'; + $provider = 'communication_matrix'; + + // Set the data. + $communication->set_data($course); + + // Test the set data. + $this->assertEquals($roomname, $course->communicationroomname); + $this->assertEquals($provider, $course->selectedcommunication); + } + + /** + * Test get_current_communication_provider method. + * + * @covers ::get_provider + */ + public function test_get_provider(): void { + $course = $this->get_course(); + + $communication = \core_communication\api::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $this->assertEquals('communication_matrix', $communication->get_provider()); + } + + /** + * Test get_avatar_filerecord method. + * + * @covers ::get_avatar_filerecord + */ + public function test_get_avatar_filerecord(): void { + $course = $this->get_course(); + + $communication = \core_communication\api::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + $filerecord = $communication->get_avatar_filerecord('avatar.svg'); + + $this->assertEquals('avatar.svg', $filerecord->filename); + $this->assertEquals('core_communication', $filerecord->component); + $this->assertEquals('avatar', $filerecord->filearea); + } + + /** + * Test set_avatar_from_datauri method. + * + * @covers ::set_avatar_from_datauri_or_filepath + * @covers ::get_avatar_filerecord + */ + public function test_set_avatar_from_datauri_or_filepath(): void { + global $CFG; + $course = $this->get_course('Sampleroom', 'none'); + + // Sample data. + $communicationroomname = 'Sampleroom'; + $selectedcommunication = 'communication_matrix'; + $avatarurl = $CFG->dirroot . '/communication/tests/fixtures/moodle_logo.jpg'; + + $communication = \core_communication\api::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + $communication->create_and_configure_room($selectedcommunication, $communicationroomname, $avatarurl); + + $communicationprocessor = processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $this->assertNotNull($communicationprocessor->get_avatar()); + } + + /** + * Test the create_and_configure_room method to add/create tasks. + * + * @covers ::create_and_configure_room + */ + public function test_create_and_configure_room(): void { + // Get the course by disabling communication so that we can create it manually calling the api. + $course = $this->get_course('Sampleroom', 'none'); + + // Sample data. + $communicationroomname = 'Sampleroom'; + $selectedcommunication = 'communication_matrix'; + + $communication = \core_communication\api::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + $communication->create_and_configure_room($selectedcommunication, $communicationroomname); + + // Test the tasks added. + $adhoctask = \core\task\manager::get_adhoc_tasks('\\core_communication\\task\\create_and_configure_room_task'); + $this->assertCount(1, $adhoctask); + + $adhoctask = reset($adhoctask); + $this->assertInstanceOf('\\core_communication\\task\\create_and_configure_room_task', $adhoctask); + + // Test the communication record exists. + $communicationprocessor = processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $this->assertEquals($communicationroomname, $communicationprocessor->get_room_name()); + $this->assertEquals($selectedcommunication, $communicationprocessor->get_provider()); + } + + /** + * Test the create_and_configure_room method to add/create tasks when no communication provider selected. + * + * @covers ::create_and_configure_room + */ + public function test_create_and_configure_room_without_communication_provider_selected(): void { + // Get the course by disabling communication so that we can create it manually calling the api. + $course = $this->getDataGenerator()->create_course(); + + // Test the tasks added. + $adhoctask = \core\task\manager::get_adhoc_tasks('\\core_communication\\task\\create_and_configure_room_task'); + $this->assertCount(0, $adhoctask); + + // Test the communication record exists. + $communicationprocessor = processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $this->assertNull($communicationprocessor); + } + + /** + * Test update operation. + * + * @covers ::update_room + */ + public function test_update_room(): void { + $course = $this->get_course(); + + // Sample data. + $communicationroomname = 'Sampleroomupdated'; + $selectedcommunication = 'communication_matrix'; + + $communication = \core_communication\api::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + $communication->update_room($selectedcommunication, $communicationroomname); + + // Test the tasks added. + $adhoctask = \core\task\manager::get_adhoc_tasks('\\core_communication\\task\\update_room_task'); + // Should be 2 as one for create, another for update. + $this->assertCount(1, $adhoctask); + + $adhoctask = reset($adhoctask); + $this->assertInstanceOf('\\core_communication\\task\\update_room_task', $adhoctask); + + // Test the communication record exists. + $communicationprocessor = processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $this->assertEquals($communicationroomname, $communicationprocessor->get_room_name()); + $this->assertEquals($selectedcommunication, $communicationprocessor->get_provider()); + } + + /** + * Test delete operation. + * + * @covers ::delete_room + */ + public function test_delete_room(): void { + $course = $this->get_course(); + + // Sample data. + $communicationroomname = 'Sampleroom'; + $selectedcommunication = 'communication_matrix'; + + // Test the communication record exists. + $communicationprocessor = processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $this->assertEquals($communicationroomname, $communicationprocessor->get_room_name()); + $this->assertEquals($selectedcommunication, $communicationprocessor->get_provider()); + + $communication = \core_communication\api::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + $communication->delete_room(); + + // Test the tasks added. + $adhoctask = \core\task\manager::get_adhoc_tasks('\\core_communication\\task\\delete_room_task'); + // Should be 2 as one for create, another for update. + $this->assertCount(1, $adhoctask); + + $adhoctask = reset($adhoctask); + $this->assertInstanceOf('\\core_communication\\task\\delete_room_task', $adhoctask); + } + + /** + * Test the update_room_membership for adding adn removing members. + * + * @covers ::add_members_to_room + * @covers ::remove_members_from_room + */ + public function test_update_room_membership(): void { + $course = $this->get_course(); + $userid = $this->get_user()->id; + + // First test the adding members to a room. + $communication = \core_communication\api::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + $communication->add_members_to_room([$userid]); + + // Test the tasks added. + $adhoctask = \core\task\manager::get_adhoc_tasks('\\core_communication\\task\\add_members_to_room_task'); + $this->assertCount(1, $adhoctask); + + // Now test the removing members from a room. + $communication->remove_members_from_room([$userid]); + + // Test the tasks added. + $adhoctask = \core\task\manager::get_adhoc_tasks('\\core_communication\\task\\remove_members_from_room'); + $this->assertCount(1, $adhoctask); + } +} diff --git a/communication/tests/communication_processor_test.php b/communication/tests/communication_processor_test.php new file mode 100644 index 00000000000..72940d049c7 --- /dev/null +++ b/communication/tests/communication_processor_test.php @@ -0,0 +1,587 @@ +. + +namespace core_communication; + +defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/communication_test_helper_trait.php'); + +/** + * Class communication_processor_test to test the communication internal api and its associated methods. + * + * @package core_communication + * @category test + * @copyright 2023 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \core_communication\communication_processor + */ +class communication_processor_test extends \advanced_testcase { + + use communication_test_helper_trait; + + /** + * Test create instance. + * + * @covers ::create_instance + */ + public function test_create_instance(): void { + global $DB; + $this->resetAfterTest(); + + // Sameple test data. + $instanceid = 10; + $component = 'core_course'; + $instancetype = 'coursecommunication'; + $selectedcommunication = 'communication_matrix'; + $communicationroomname = 'communicationroom'; + + $communicationprocessor = communication_processor::create_instance( + $selectedcommunication, + $instanceid, + $component, + $instancetype, + $communicationroomname, + ); + + // Now test the record against the database. + $communicationrecord = $DB->get_record('communication', + ['instanceid' => $instanceid, 'component' => $component, 'instancetype' => $instancetype]); + + // Test against the set data. + $this->assertNotEmpty($communicationrecord); + $this->assertEquals($instanceid, $communicationrecord->instanceid); + $this->assertEquals($component, $communicationrecord->component); + $this->assertEquals($selectedcommunication, $communicationrecord->provider); + $this->assertEquals($communicationroomname, $communicationrecord->roomname); + $this->assertEquals($instancetype, $communicationrecord->instancetype); + + // Test against the object. + $this->assertEquals($communicationprocessor->get_id(), $communicationrecord->id); + $this->assertEquals($communicationprocessor->get_provider(), $communicationrecord->provider); + $this->assertEquals($communicationprocessor->get_room_name(), $communicationrecord->roomname); + } + + /** + * Test update instance. + * + * @covers ::update_instance + */ + public function test_update_instance(): void { + global $DB; + $this->resetAfterTest(); + + // Sameple test data. + $instanceid = 10; + $component = 'core_course'; + $instancetype = 'coursecommunication'; + $selectedcommunication = 'communication_matrix'; + $communicationroomname = 'communicationroom'; + + $communicationprocessor = communication_processor::create_instance( + $selectedcommunication, + $instanceid, + $component, + $instancetype, + $communicationroomname, + ); + + $selectedcommunication = 'none'; + $communicationroomname = 'communicationroomedited'; + + $communicationprocessor->update_instance($selectedcommunication, $communicationroomname); + + // Now test the record against the database. + $communicationrecord = $DB->get_record('communication', [ + 'instanceid' => $instanceid, + 'component' => $component, + 'instancetype' => $instancetype + ]); + + // Test against the set data. + $this->assertNotEmpty($communicationrecord); + $this->assertEquals($instanceid, $communicationrecord->instanceid); + $this->assertEquals($component, $communicationrecord->component); + $this->assertEquals($selectedcommunication, $communicationrecord->provider); + $this->assertEquals($communicationroomname, $communicationrecord->roomname); + $this->assertEquals($instancetype, $communicationrecord->instancetype); + + // Test against the object. + $this->assertEquals($communicationprocessor->get_id(), $communicationrecord->id); + $this->assertEquals($communicationprocessor->get_provider(), $communicationrecord->provider); + $this->assertEquals($communicationprocessor->get_room_name(), $communicationrecord->roomname); + } + + /** + * Test delete instance. + * + * @covers ::delete_instance + */ + public function test_delete_instance(): void { + global $DB; + $this->resetAfterTest(); + + // Sameple test data. + $instanceid = 10; + $component = 'core_course'; + $instancetype = 'coursecommunication'; + $selectedcommunication = 'communication_matrix'; + $communicationroomname = 'communicationroom'; + + $communicationprocessor = communication_processor::create_instance( + $selectedcommunication, + $instanceid, + $component, + $instancetype, + $communicationroomname, + ); + + $communicationprocessor->delete_instance(); + + // Now test the record against the database. + $communicationrecord = $DB->get_record('communication', [ + 'instanceid' => $instanceid, + 'component' => $component, + 'instancetype' => $instancetype + ]); + + // Test against the set data. + $this->assertEmpty($communicationrecord); + + // Test against the object. + $communicationprocessor = communication_processor::load_by_instance( + $component, + $instancetype, + $instanceid); + $this->assertNull($communicationprocessor); + } + + /** + * Test create instance user mapping. + * + * @covers ::create_instance_user_mapping + * @covers ::get_all_userids_for_instance + */ + public function test_create_instance_user_mapping(): void { + $this->resetAfterTest(); + + global $DB; + $course = $this->get_course('Sampleroom', 'none'); + $userid = $this->get_user()->id; + + // Sample data. + $communicationroomname = 'Sampleroom'; + $selectedcommunication = 'communication_matrix'; + $component = 'core_course'; + $instancetype = 'coursecommunication'; + + // First test the adding members to a room. + $communication = \core_communication\api::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + $communication->create_and_configure_room($selectedcommunication, $communicationroomname); + $communication->add_members_to_room([$userid]); + + // Test against the object. + $communicationprocessor = communication_processor::load_by_instance( + $component, + $instancetype, + $course->id + ); + + $this->assertEquals([$userid], $communicationprocessor->get_all_userids_for_instance()); + + // Test against the database. + $communicationuserrecord = $DB->get_record('communication_user', [ + 'commid' => $communicationprocessor->get_id(), + 'userid' => $userid + ]); + + $this->assertEquals($communicationuserrecord->userid, $userid); + $this->assertEquals($communicationuserrecord->commid, $communicationprocessor->get_id()); + } + + /** + * Test update instance user mapping. + * + * @covers ::get_all_userids_for_instance + */ + public function test_update_instance_user_mapping(): void { + $this->resetAfterTest(); + + global $DB; + $course = $this->get_course(); + $userid = $this->get_user()->id; + + // Sample data. + $communicationroomname = 'Sampleroom'; + $selectedcommunication = 'communication_matrix'; + $component = 'core_course'; + $instancetype = 'coursecommunication'; + + $communication = \core_communication\api::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + $communication->update_room($selectedcommunication, $communicationroomname); + $communication->add_members_to_room([$userid]); + + // Test against the object. + $communicationprocessor = communication_processor::load_by_instance( + $component, + $instancetype, + $course->id + ); + + $this->assertnotEmpty($communicationprocessor->get_instance_userids_by_synced()); + + // Test against the database. + $communicationuserrecord = $DB->get_record('communication_user', [ + 'commid' => $communicationprocessor->get_id(), + 'userid' => $userid + ]); + + $this->assertEquals($communicationuserrecord->userid, $userid); + $this->assertEquals($communicationuserrecord->commid, $communicationprocessor->get_id()); + + // Now add again. + $communicationprocessor->delete_instance_user_mapping([$userid]); + + // Test against the database. + $communicationuserrecord = $DB->get_record('communication_user', [ + 'commid' => $communicationprocessor->get_id(), + 'userid' => $userid + ]); + + $this->assertEmpty($communicationuserrecord); + + + } + + /** + * Test delete instance user mapping. + * + * @covers ::delete_instance_user_mapping + * @covers ::get_all_userids_for_instance + */ + public function test_delete_instance_user_mapping(): void { + $this->resetAfterTest(); + + global $DB; + $course = $this->get_course('Sampleroom', 'none'); + $userid = $this->get_user()->id; + + // Sample data. + $communicationroomname = 'Sampleroom'; + $selectedcommunication = 'communication_matrix'; + $component = 'core_course'; + $instancetype = 'coursecommunication'; + + // First test the adding members to a room. + $communication = \core_communication\api::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + $communication->create_and_configure_room($selectedcommunication, $communicationroomname); + $communication->add_members_to_room([$userid]); + + // Test against the object. + $communicationprocessor = communication_processor::load_by_instance( + $component, + $instancetype, + $course->id + ); + + $this->assertEquals([$userid], $communicationprocessor->get_all_userids_for_instance()); + + // Delete the user mapping. + $communicationprocessor->delete_instance_user_mapping([$userid]); + + $this->assertEmpty($communicationprocessor->get_all_userids_for_instance()); + + // Test against the database. + $communicationuserrecord = $DB->get_record('communication_user', [ + 'commid' => $communicationprocessor->get_id(), + 'userid' => $userid + ]); + + $this->assertEmpty($communicationuserrecord); + } + + /** + * Test delete user mappings for instance. + * + * @covers ::delete_user_mappings_for_instance + * @covers ::get_all_userids_for_instance + */ + public function test_delete_user_mappings_for_instance(): void { + $this->resetAfterTest(); + + global $DB; + $course = $this->get_course('Sampleroom', 'none'); + $userid = $this->get_user()->id; + + // Sample data. + $communicationroomname = 'Sampleroom'; + $selectedcommunication = 'communication_matrix'; + $component = 'core_course'; + $instancetype = 'coursecommunication'; + + // First test the adding members to a room. + $communication = \core_communication\api::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + $communication->create_and_configure_room($selectedcommunication, $communicationroomname); + $communication->add_members_to_room([$userid]); + + // Test against the object. + $communicationprocessor = communication_processor::load_by_instance( + $component, + $instancetype, + $course->id + ); + + $this->assertEquals([$userid], $communicationprocessor->get_all_userids_for_instance()); + + // Delete the user mapping. + $communicationprocessor->delete_user_mappings_for_instance(); + + $this->assertEmpty($communicationprocessor->get_all_userids_for_instance()); + + // Test against the database. + $communicationuserrecord = $DB->get_record('communication_user', [ + 'commid' => $communicationprocessor->get_id(), + 'userid' => $userid + ]); + + $this->assertEmpty($communicationuserrecord); + } + + /** + * Test load by id. + * + * @covers ::load_by_instance + */ + public function test_load_by_instance(): void { + $this->resetAfterTest(); + $course = $this->get_course(); + + // Test the communication record exists. + $communicationprocessor = communication_processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $this->assertNotNull($communicationprocessor); + $this->assertInstanceOf(communication_provider::class, $communicationprocessor->get_room_provider()); + $this->assertInstanceOf(room_chat_provider::class, $communicationprocessor->get_room_provider()); + $this->assertInstanceOf(room_user_provider::class, $communicationprocessor->get_room_provider()); + $this->assertInstanceOf(user_provider::class, $communicationprocessor->get_room_provider()); + } + + /** + * Test load by id. + * + * @covers ::load_by_id + */ + public function test_load_by_id(): void { + $this->resetAfterTest(); + $course = $this->get_course(); + + // Test the communication record exists. + $communicationprocessor = communication_processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $communicationprocessorbyid = communication_processor::load_by_id($communicationprocessor->get_id()); + + $this->assertNotNull($communicationprocessorbyid); + $this->assertInstanceOf(communication_provider::class, $communicationprocessorbyid->get_room_provider()); + $this->assertInstanceOf(room_chat_provider::class, $communicationprocessorbyid->get_room_provider()); + $this->assertInstanceOf(room_user_provider::class, $communicationprocessorbyid->get_room_provider()); + $this->assertInstanceOf(user_provider::class, $communicationprocessorbyid->get_room_provider()); + } + + /** + * Test get component. + * + * @covers ::get_component + */ + public function test_get_component(): void { + $this->resetAfterTest(); + $course = $this->get_course(); + + // Test the communication record exists. + $communicationprocessor = communication_processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $this->assertEquals('core_course', $communicationprocessor->get_component()); + } + + /** + * Test get provider. + * + * @covers ::get_provider + */ + public function test_get_provider(): void { + $this->resetAfterTest(); + $course = $this->get_course(); + + // Test the communication record exists. + $communicationprocessor = communication_processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $this->assertEquals('communication_matrix', $communicationprocessor->get_provider()); + } + + /** + * Test get room name. + * + * @covers ::get_room_name + */ + public function test_get_room_name(): void { + $this->resetAfterTest(); + $course = $this->get_course(); + + // Test the communication record exists. + $communicationprocessor = communication_processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $this->assertEquals('Sampleroom', $communicationprocessor->get_room_name()); + } + + /** + * Test get room provider. + * + * @covers ::get_room_provider + * @covers ::require_room_features + * @covers ::supports_room_features + */ + public function test_get_room_provider(): void { + $this->resetAfterTest(); + $course = $this->get_course(); + + // Test the communication record exists. + $communicationprocessor = communication_processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $this->assertInstanceOf(room_chat_provider::class, $communicationprocessor->get_room_provider()); + } + + /** + * Test get user provider. + * + * @covers ::get_user_provider + * @covers ::require_user_features + * @covers ::supports_user_features + */ + public function test_get_user_provider(): void { + $this->resetAfterTest(); + $course = $this->get_course(); + + // Test the communication record exists. + $communicationprocessor = communication_processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $this->assertInstanceOf(user_provider::class, $communicationprocessor->get_room_provider()); + } + + /** + * Test get room user provider. + * + * @covers ::get_room_user_provider + * @covers ::require_room_features + * @covers ::require_room_user_features + * @covers ::supports_room_user_features + * @covers ::supports_room_features + */ + public function test_get_room_user_provider(): void { + $this->resetAfterTest(); + $course = $this->get_course(); + + // Test the communication record exists. + $communicationprocessor = communication_processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $this->assertInstanceOf(room_user_provider::class, $communicationprocessor->get_room_user_provider()); + } + + /** + * Test get avatar. + * + * @covers ::get_avatar + */ + public function test_get_avatar(): void { + $this->resetAfterTest(); + + global $CFG; + $course = $this->get_course('Sampleroom', 'none'); + + // Sample data. + $communicationroomname = 'Sampleroom'; + $selectedcommunication = 'communication_matrix'; + $avatarurl = $CFG->dirroot . '/communication/tests/fixtures/moodle_logo.jpg'; + + $communication = \core_communication\api::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + $communication->create_and_configure_room($selectedcommunication, $communicationroomname, $avatarurl); + + $communicationprocessor = communication_processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $avatar = $communicationprocessor->get_avatar(); + + $this->assertNotNull($avatar); + $this->assertEquals($avatar->get_component(), 'core_communication'); + $this->assertEquals($avatar->get_filearea(), 'avatar'); + $this->assertEquals($avatar->get_itemid(), $communicationprocessor->get_id()); + $this->assertEquals($avatar->get_filepath(), '/'); + $this->assertEquals($avatar->get_filearea(), 'avatar'); + } +} diff --git a/communication/tests/communication_test_helper_trait.php b/communication/tests/communication_test_helper_trait.php new file mode 100644 index 00000000000..2fb398881fa --- /dev/null +++ b/communication/tests/communication_test_helper_trait.php @@ -0,0 +1,82 @@ +. + +namespace core_communication; + +/** + * Trait communication_test_helper_trait to generate initial setup for communication providers. + * + * @package core_communication + * @category test + * @copyright 2023 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +trait communication_test_helper_trait { + + /** + * Setup necessary configs for communication subsystem. + * + * @return void + */ + protected function setup_communication_configs(): void { + set_config('enablecommunicationsubsystem', 1); + } + + /** + * Get or create course if it does not exist + * + * @param string $roomname The room name for the communication api + * @param string $provider The selected provider + * @return \stdClass + */ + protected function get_course( + string $roomname = 'Sampleroom', + string $provider = 'communication_matrix' + ): \stdClass { + + $this->setup_communication_configs(); + $records = [ + 'selectedcommunication' => $provider, + 'communicationroomname' => $roomname, + ]; + + return $this->getDataGenerator()->create_course($records); + } + + /** + * Get or create user if it does not exist. + * + * @param string $firstname The user's firstname for the communication api + * @param string $lastname The user's lastname for the communication api + * @param string $username The user's username for the communication api + * @return \stdClass + */ + protected function get_user( + string $firstname = 'Samplefn', + string $lastname = 'Sampleln', + string $username = 'sampleun' + ): \stdClass { + + $this->setup_communication_configs(); + $records = [ + 'firstname' => $firstname, + 'lastname' => $lastname, + 'username' => $username + ]; + + return $this->getDataGenerator()->create_user($records); + } +} diff --git a/communication/tests/fixtures/moodle_logo.jpg b/communication/tests/fixtures/moodle_logo.jpg new file mode 100644 index 00000000000..f4516153120 Binary files /dev/null and b/communication/tests/fixtures/moodle_logo.jpg differ diff --git a/communication/tests/processor_test.php b/communication/tests/processor_test.php new file mode 100644 index 00000000000..eb3263fbe65 --- /dev/null +++ b/communication/tests/processor_test.php @@ -0,0 +1,398 @@ +. + +namespace core_communication; + +defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/communication_test_helper_trait.php'); + +/** + * Class processor_test to test the communication internal api and its associated methods. + * + * @package core_communication + * @category test + * @copyright 2023 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \core_communication\processor + */ +class processor_test extends \advanced_testcase { + + use communication_test_helper_trait; + + /** + * Test create instance. + * + * @covers ::create_instance + * @covers ::get_id + * @covers ::get_instance + * @covers ::get_room_name + */ + public function test_create_instance(): void { + global $DB; + $this->resetAfterTest(); + + // Sameple test data. + $instanceid = 10; + $component = 'core_course'; + $instancetype = 'coursecommunication'; + $selectedcommunication = 'communication_matrix'; + $communicationroomname = 'communicationroom'; + + $communicationprocessor = processor::create_instance( + $selectedcommunication, + $instanceid, + $component, + $instancetype, + $communicationroomname, + ); + + // Now test the record against the database. + $communicationrecord = $DB->get_record('communication', + ['instanceid' => $instanceid, 'component' => $component, 'instancetype' => $instancetype]); + + // Test against the set data. + $this->assertNotEmpty($communicationrecord); + $this->assertEquals($instanceid, $communicationrecord->instanceid); + $this->assertEquals($component, $communicationrecord->component); + $this->assertEquals($selectedcommunication, $communicationrecord->provider); + $this->assertEquals($communicationroomname, $communicationrecord->roomname); + $this->assertEquals($instancetype, $communicationrecord->instancetype); + + // Test against the object. + $this->assertEquals($communicationprocessor->get_id(), $communicationrecord->id); + $this->assertEquals($communicationprocessor->get_provider(), $communicationrecord->provider); + $this->assertEquals($communicationprocessor->get_room_name(), $communicationrecord->roomname); + } + + /** + * Test update instance. + * + * @covers ::update_instance + * @covers ::is_instance_active + * @covers ::get_id + * @covers ::get_room_name + */ + public function test_update_instance(): void { + global $DB; + $this->resetAfterTest(); + + // Sameple test data. + $instanceid = 10; + $component = 'core_course'; + $instancetype = 'coursecommunication'; + $selectedcommunication = 'communication_matrix'; + $communicationroomname = 'communicationroom'; + + $communicationprocessor = processor::create_instance( + $selectedcommunication, + $instanceid, + $component, + $instancetype, + $communicationroomname, + ); + + $selectedcommunication = 'none'; + $communicationroomname = 'communicationroomedited'; + + $communicationprocessor->update_instance($selectedcommunication, $communicationroomname); + + // Now test the record against the database. + $communicationrecord = $DB->get_record('communication', [ + 'instanceid' => $instanceid, + 'component' => $component, + 'instancetype' => $instancetype + ]); + + // Test against the set data. + $this->assertNotEmpty($communicationrecord); + $this->assertEquals($instanceid, $communicationrecord->instanceid); + $this->assertEquals($component, $communicationrecord->component); + $this->assertEquals(processor::PROVIDER_INACTIVE, $communicationrecord->active); + $this->assertEquals($communicationroomname, $communicationrecord->roomname); + $this->assertEquals($instancetype, $communicationrecord->instancetype); + + // Test against the object. + $this->assertEquals($communicationprocessor->get_id(), $communicationrecord->id); + $this->assertEquals($communicationprocessor->is_instance_active(), $communicationrecord->active); + $this->assertEquals($communicationprocessor->get_room_name(), $communicationrecord->roomname); + } + + /** + * Test delete instance. + * + * @covers ::delete_instance + * @covers ::create_instance + * @covers ::load_by_instance + */ + public function test_delete_instance(): void { + global $DB; + $this->resetAfterTest(); + + // Sameple test data. + $instanceid = 10; + $component = 'core_course'; + $instancetype = 'coursecommunication'; + $selectedcommunication = 'communication_matrix'; + $communicationroomname = 'communicationroom'; + + $communicationprocessor = processor::create_instance( + $selectedcommunication, + $instanceid, + $component, + $instancetype, + $communicationroomname, + ); + + $communicationprocessor->delete_instance(); + + // Now test the record against the database. + $communicationrecord = $DB->get_record('communication', [ + 'instanceid' => $instanceid, + 'component' => $component, + 'instancetype' => $instancetype + ]); + + // Test against the set data. + $this->assertEmpty($communicationrecord); + + // Test against the object. + $communicationprocessor = processor::load_by_instance( + $component, + $instancetype, + $instanceid); + $this->assertNull($communicationprocessor); + } + + /** + * Test load by id. + * + * @covers ::load_by_instance + * @covers ::get_room_provider + */ + public function test_load_by_instance(): void { + $this->resetAfterTest(); + $course = $this->get_course(); + + // Test the communication record exists. + $communicationprocessor = processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $this->assertNotNull($communicationprocessor); + $this->assertInstanceOf(communication_provider::class, $communicationprocessor->get_room_provider()); + $this->assertInstanceOf(room_chat_provider::class, $communicationprocessor->get_room_provider()); + $this->assertInstanceOf(room_user_provider::class, $communicationprocessor->get_room_provider()); + $this->assertInstanceOf(user_provider::class, $communicationprocessor->get_room_provider()); + } + + /** + * Test load by id. + * + * @covers ::load_by_id + * @covers ::get_room_provider + * @covers ::load_by_instance + */ + public function test_load_by_id(): void { + $this->resetAfterTest(); + $course = $this->get_course(); + + // Test the communication record exists. + $communicationprocessor = processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $communicationprocessorbyid = processor::load_by_id($communicationprocessor->get_id()); + + $this->assertNotNull($communicationprocessorbyid); + $this->assertInstanceOf(communication_provider::class, $communicationprocessorbyid->get_room_provider()); + $this->assertInstanceOf(room_chat_provider::class, $communicationprocessorbyid->get_room_provider()); + $this->assertInstanceOf(room_user_provider::class, $communicationprocessorbyid->get_room_provider()); + $this->assertInstanceOf(user_provider::class, $communicationprocessorbyid->get_room_provider()); + } + + /** + * Test get component. + * + * @covers ::get_component + * @covers ::load_by_instance + */ + public function test_get_component(): void { + $this->resetAfterTest(); + $course = $this->get_course(); + + // Test the communication record exists. + $communicationprocessor = processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $this->assertEquals('core_course', $communicationprocessor->get_component()); + } + + /** + * Test get provider. + * + * @covers ::get_provider + * @covers ::load_by_instance + */ + public function test_get_provider(): void { + $this->resetAfterTest(); + $course = $this->get_course(); + + // Test the communication record exists. + $communicationprocessor = processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $this->assertEquals('communication_matrix', $communicationprocessor->get_provider()); + } + + /** + * Test get room name. + * + * @covers ::get_room_name + * @covers ::load_by_instance + */ + public function test_get_room_name(): void { + $this->resetAfterTest(); + $course = $this->get_course(); + + // Test the communication record exists. + $communicationprocessor = processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $this->assertEquals('Sampleroom', $communicationprocessor->get_room_name()); + } + + /** + * Test get room provider. + * + * @covers ::get_room_provider + * @covers ::require_room_features + * @covers ::supports_room_features + * @covers ::load_by_instance + */ + public function test_get_room_provider(): void { + $this->resetAfterTest(); + $course = $this->get_course(); + + // Test the communication record exists. + $communicationprocessor = processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $this->assertInstanceOf(room_chat_provider::class, $communicationprocessor->get_room_provider()); + } + + /** + * Test get user provider. + * + * @covers ::get_user_provider + * @covers ::require_user_features + * @covers ::supports_user_features + * @covers ::load_by_instance + */ + public function test_get_user_provider(): void { + $this->resetAfterTest(); + $course = $this->get_course(); + + // Test the communication record exists. + $communicationprocessor = processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $this->assertInstanceOf(user_provider::class, $communicationprocessor->get_room_provider()); + } + + /** + * Test get room user provider. + * + * @covers ::get_room_user_provider + * @covers ::require_room_features + * @covers ::require_room_user_features + * @covers ::supports_room_user_features + * @covers ::supports_room_features + * @covers ::load_by_instance + */ + public function test_get_room_user_provider(): void { + $this->resetAfterTest(); + $course = $this->get_course(); + + // Test the communication record exists. + $communicationprocessor = processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $this->assertInstanceOf(room_user_provider::class, $communicationprocessor->get_room_user_provider()); + } + + /** + * Test get avatar. + * + * @covers ::get_avatar + * @covers ::load_by_instance + */ + public function test_get_avatar(): void { + $this->resetAfterTest(); + + global $CFG; + $course = $this->get_course('Sampleroom', 'none'); + + // Sample data. + $communicationroomname = 'Sampleroom'; + $selectedcommunication = 'communication_matrix'; + $avatarurl = $CFG->dirroot . '/communication/tests/fixtures/moodle_logo.jpg'; + + $communication = \core_communication\api::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + $communication->create_and_configure_room($selectedcommunication, $communicationroomname, $avatarurl); + + $communicationprocessor = processor::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + + $avatar = $communicationprocessor->get_avatar(); + + $this->assertNotNull($avatar); + $this->assertEquals($avatar->get_component(), 'core_communication'); + $this->assertEquals($avatar->get_filearea(), 'avatar'); + $this->assertEquals($avatar->get_itemid(), $communicationprocessor->get_id()); + $this->assertEquals($avatar->get_filepath(), '/'); + $this->assertEquals($avatar->get_filearea(), 'avatar'); + } +} diff --git a/lang/en/communication.php b/lang/en/communication.php index 92c906dfae4..c04aa2ca66d 100644 --- a/lang/en/communication.php +++ b/lang/en/communication.php @@ -24,6 +24,11 @@ $string['communication'] = 'Communication'; $string['communicationprovidernotfound'] = 'The \'{$a}\' communication provider doesn\'t exist or is not recognised.'; +$string['communicationroomname'] = 'Room name'; +$string['communicationroomname_help'] = 'The name that participants see when they visit the room. If you leave this blank, a default room name will be automatically set.'; $string['managecommunicationproviders'] = 'Manage communication providers'; $string['nocommunicationprovider'] = 'No communication provider found.'; +$string['nocommunicationselected'] = 'None'; $string['privacy:metadata'] = 'The Moodle communication subsystem does not store any personal data.'; +$string['seleccommunicationprovider'] = 'Communication service'; +$string['seleccommunicationprovider_help'] = 'The tool available for chat and related communication methods.'; diff --git a/lib/db/install.xml b/lib/db/install.xml index ac1a966513c..5c0f16874df 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -218,6 +218,35 @@ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 2af3b5374c5..170df67f98b 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -3256,6 +3256,52 @@ privatefiles,moodle|/user/files.php'; upgrade_main_savepoint(true, 2023042000.00); } + if ($oldversion < 2023050400.01) { + // Define communication table. + $table = new xmldb_table('communication'); + + // Adding fields to table communication. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE); + $table->add_field('instanceid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'id'); + $table->add_field('component', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null, 'instanceid'); + $table->add_field('instancetype', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null, 'component'); + $table->add_field('provider', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null, 'instancerype'); + $table->add_field('roomname', XMLDB_TYPE_CHAR, '255', null, null, null, null, 'provider'); + $table->add_field('avatarfilename', XMLDB_TYPE_CHAR, '100', null, null, null, null, 'roomname'); + $table->add_field('active', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, 1, 'avatarfilename'); + + // Add key. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + + // Conditionally launch create table for communication. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Define communication user table. + $table = new xmldb_table('communication_user'); + + // Adding fields to table communication. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE); + $table->add_field('commid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'id'); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'commid'); + $table->add_field('synced', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, 0, 'userid'); + $table->add_field('deleted', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, 0, 'synced'); + + // Add keys. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + $table->add_key('commid', XMLDB_KEY_FOREIGN, ['commid'], 'communication', ['id']); + $table->add_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']); + + // Conditionally launch create table for communication. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2023050400.01); + } + // Automatically generated Moodle v4.2.0 release upgrade line. // Put any upgrade step following this. diff --git a/version.php b/version.php index aa0c583e0a1..1263fce8eab 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2023050400.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2023050400.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '4.3dev (Build: 20230504)'; // Human-friendly version name