diff --git a/communication/provider/customlink/classes/communication_feature.php b/communication/provider/customlink/classes/communication_feature.php new file mode 100644 index 0000000000000..a85a320a72ad2 --- /dev/null +++ b/communication/provider/customlink/classes/communication_feature.php @@ -0,0 +1,168 @@ +. + +namespace communication_customlink; + +use core_communication\processor; + +/** + * class communication_feature to handle custom link specific actions. + * + * @package communication_customlink + * @copyright 2023 Michael Hawkins + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class communication_feature implements + \core_communication\communication_provider, + \core_communication\room_chat_provider, + \core_communication\form_provider { + + /** @var string The database table storing custom link specific data */ + protected const CUSTOMLINK_TABLE = 'communication_customlink'; + + /** @var \cache_application $cache The application cache for this provider. */ + protected \cache_application $cache; + + /** + * Load the communication provider for the communication API. + * + * @param processor $communication The communication processor object. + * @return communication_feature The communication provider object. + */ + public static function load_for_instance(processor $communication): self { + return new self($communication); + } + + /** + * Constructor for communication provider. + * + * @param processor $communication The communication processor object. + */ + private function __construct( + private \core_communication\processor $communication, + ) { + $this->cache = \cache::make('communication_customlink', 'customlink'); + } + + /** + * Create room - room existence managed externally, always return true. + * + * @return boolean + */ + public function create_chat_room(): bool { + return true; + } + + /** + * Update room - room existence managed externally, always return true. + * + * @return boolean + */ + public function update_chat_room(): bool { + return true; + } + + /** + * Delete room - room existence managed externally, always return true. + * + * @return boolean + */ + public function delete_chat_room(): bool { + return true; + } + + /** + * Fetch the URL for this custom link provider. + * + * @return string|null The custom URL, or null if not found. + */ + public function get_chat_room_url(): ?string { + global $DB; + + $commid = $this->communication->get_id(); + $cachekey = "link_url_{$commid}"; + + // Attempt to fetch the room URL from the cache. + if ($url = $this->cache->get($cachekey)) { + return $url; + } + + // If not found in the cache, fetch the URL from the database. + $url = $DB->get_field( + self::CUSTOMLINK_TABLE, + 'url', + ['commid' => $commid], + ); + + // Cache the URL. + $this->cache->set($cachekey, $url); + + return $url; + } + + public function save_form_data(\stdClass $instance): void { + global $DB; + + $commid = $this->communication->get_id(); + $cachekey = "link_url_{$commid}"; + + $newrecord = new \stdClass(); + $newrecord->url = $instance->customlinkurl ?? null; + + $existingrecord = $DB->get_record( + self::CUSTOMLINK_TABLE, + ['commid' => $commid], + 'id, url' + ); + + if (!$existingrecord) { + // Create the record if it does not exist. + $newrecord->commid = $commid; + $DB->insert_record(self::CUSTOMLINK_TABLE, $newrecord); + + } else if ($newrecord->url !== $existingrecord->url) { + // Update record if the URL has changed. + $newrecord->id = $existingrecord->id; + $DB->update_record(self::CUSTOMLINK_TABLE, $newrecord); + } else { + // No change made. + return; + } + + // Cache the new URL. + $this->cache->set($cachekey, $newrecord->url); + } + + public function set_form_data(\stdClass $instance): void { + if (!empty($instance->id) && !empty($this->communication->get_id())) { + $instance->customlinkurl = $this->get_chat_room_url(); + } + } + + public static function set_form_definition(\MoodleQuickForm $mform): void { + // Custom link description for the communication provider. + $mform->insertElementBefore($mform->createElement('text', 'customlinkurl', + get_string('customlinkurl', 'communication_customlink'), + 'maxlength="255" size="40"'), 'addcommunicationoptionshere'); + $mform->addHelpButton('customlinkurl', 'customlinkurl', 'communication_customlink'); + $mform->setType('customlinkurl', PARAM_URL); + $mform->addRule('customlinkurl', get_string('required'), 'required', null, 'client'); + $mform->addRule('customlinkurl', get_string('maximumchars', '', 255), 'maxlength', 255); + $mform->insertElementBefore($mform->createElement('static', 'customlinkurlinfo', '', + get_string('customlinkurlinfo', 'communication_customlink'), + 'addcommunicationoptionshere'), 'addcommunicationoptionshere'); + } +} diff --git a/communication/provider/customlink/classes/privacy/provider.php b/communication/provider/customlink/classes/privacy/provider.php new file mode 100644 index 0000000000000..898502d85e782 --- /dev/null +++ b/communication/provider/customlink/classes/privacy/provider.php @@ -0,0 +1,39 @@ +. + +namespace communication_customlink\privacy; + +use core_privacy\local\metadata\null_provider; + +/** + * Privacy Subsystem for communication_customlink implementing null_provider. + * + * @package communication_customlink + * @copyright 2023 Michael Hawkins + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason(): string { + return 'privacy:metadata'; + } +} diff --git a/communication/provider/customlink/db/caches.php b/communication/provider/customlink/db/caches.php new file mode 100644 index 0000000000000..e06baed00571a --- /dev/null +++ b/communication/provider/customlink/db/caches.php @@ -0,0 +1,35 @@ +. + +/** + * Defined caches used internally by the provider. + * + * @package communication_customlink + * @copyright 2023 Michael Hawkins + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +declare(strict_types=1); + +defined('MOODLE_INTERNAL') || die(); + +$definitions = [ + 'customlink' => [ + 'mode' => cache_store::MODE_APPLICATION, + 'simplekeys' => true, + 'simpledata' => true, + ], +]; diff --git a/communication/provider/customlink/db/install.xml b/communication/provider/customlink/db/install.xml new file mode 100644 index 0000000000000..bcb64d4a79db4 --- /dev/null +++ b/communication/provider/customlink/db/install.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + +
+
+
diff --git a/communication/provider/customlink/lang/en/communication_customlink.php b/communication/provider/customlink/lang/en/communication_customlink.php new file mode 100644 index 0000000000000..b684484cdb7c5 --- /dev/null +++ b/communication/provider/customlink/lang/en/communication_customlink.php @@ -0,0 +1,30 @@ +. + +/** + * Strings for component communication_customlink, language 'en'. + * + * @package communication_customlink + * @copyright 2023 Michael Hawkins + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['cachedef_customlink'] = 'Custom link data'; +$string['customlinkurl'] = 'Custom link URL'; +$string['customlinkurl_help'] = 'Provide a link to an existing room from any communication service you would like to make available to participants - such as Microsoft Teams, Slack or Matrix.'; +$string['customlinkurlinfo'] = 'The URL of an existing room already set up for this course.'; +$string['pluginname'] = 'Custom link'; +$string['privacy:metadata'] = 'Custom link communication plugin does not store any personal data.'; diff --git a/communication/provider/customlink/tests/behat/custom_link.feature b/communication/provider/customlink/tests/behat/custom_link.feature new file mode 100644 index 0000000000000..066cab93cc952 --- /dev/null +++ b/communication/provider/customlink/tests/behat/custom_link.feature @@ -0,0 +1,86 @@ +@communication @communication_customlink @javascript +Feature: Communication custom link + In order to facilitate easy access to an existing communication platform + As a teacher + I need to be able to make a custom communication link available in my course + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And the following config values are set as admin: + | enablecommunicationsubsystem | 1 | + + Scenario: As a teacher I can configure a custom communication provider for my course + Given I am on the "Course 1" "Course" page logged in as "teacher1" + And "Chat to course participants" "button" should not be visible + When I navigate to "Communication" in current page administration + And the "Communication service" select box should contain "Custom link" + And I should not see "Custom link URL" + And I select "Custom link" from the "Communication service" singleselect + And I should see "Custom link URL" + And I set the following fields to these values: + | communicationroomname | Test URL | + | customlinkurl | #wwwroot#/communication/provider/customlink/tests/behat/fixtures/custom_link_test_page.php | + And I press "Save changes" + Then "Chat to course participants" "button" should be visible + And I click on "Chat to course participants" "button" + # Check the link hits the expected destination. + And I switch to a second window + And I should see "Example messaging service - teacher1" in the "region-main" "region" + And I close all opened windows + # Ensure any communication subsystem tasks have no impact on availability. + And I run all adhoc tasks + And I am on the "Course 1" course page + And "Chat to course participants" "button" should be visible + And I click on "Chat to course participants" "button" + And I switch to a second window + And I should see "Example messaging service - teacher1" in the "region-main" "region" + And I close all opened windows + And I log out + # Confirm student also has access to the custom link. + And I am on the "Course 1" "Course" page logged in as "student1" + And "Chat to course participants" "button" should be visible + And I click on "Chat to course participants" "button" + And I switch to a second window + And I should see "Example messaging service - student1" in the "region-main" "region" + + Scenario: As a teacher I can disable and re-enable a custom communication provider for my course + Given I am on the "Course 1" "Course" page logged in as "teacher1" + And "Chat to course participants" "button" should not be visible + When I navigate to "Communication" in current page administration + And I select "Custom link" from the "Communication service" singleselect + And I set the following fields to these values: + | communicationroomname | Test URL | + | customlinkurl | #wwwroot#/communication/provider/customlink/tests/behat/fixtures/custom_link_test_page.php | + And I press "Save changes" + And "Chat to course participants" "button" should be visible + And I run all adhoc tasks + And I navigate to "Communication" in current page administration + And I select "None" from the "Communication service" singleselect + And I press "Save changes" + And "Chat to course participants" "button" should not be visible + And I run all adhoc tasks + And I am on the "Course 1" course page + And "Chat to course participants" "button" should not be visible + And I navigate to "Communication" in current page administration + And I select "Custom link" from the "Communication service" singleselect + And I set the following fields to these values: + | communicationroomname | Test URL | + | customlinkurl | #wwwroot#/communication/provider/customlink/tests/behat/fixtures/custom_link_test_page.php | + And I press "Save changes" + And "Chat to course participants" "button" should be visible + And I run all adhoc tasks + And I am on the "Course 1" course page + And "Chat to course participants" "button" should be visible + And I click on "Chat to course participants" "button" + And I switch to a second window + And I should see "Example messaging service - teacher1" in the "region-main" "region" diff --git a/communication/provider/customlink/tests/behat/fixtures/custom_link_test_page.php b/communication/provider/customlink/tests/behat/fixtures/custom_link_test_page.php new file mode 100644 index 0000000000000..7bf5a61450fe4 --- /dev/null +++ b/communication/provider/customlink/tests/behat/fixtures/custom_link_test_page.php @@ -0,0 +1,40 @@ +. + +/** + * A page which can be used to represent a messaging service while testing the custom link communication provider. + * + * The current Moodle user's username is listed in the heading to make it easier to confirm the page has been + * opened by the expected user. + * + * @package communication_customlink + * @copyright 2023 Michael Hawkins + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +require_once(__DIR__ . '/../../../../../../config.php'); + +defined('BEHAT_SITE_RUNNING') || die(); + +global $OUTPUT, $PAGE, $USER; + +$PAGE->set_url('/communication/provider/customlink/tests/behat/fixtures/custom_link_test_page.php'); +require_login(); +$PAGE->set_context(core\context\system::instance()); + +echo $OUTPUT->header(); +echo "

Example messaging service - {$USER->username}

"; +echo "

Imagine this is a wonderful messaging service being accessed directly from a link in Moodle!

"; +echo $OUTPUT->footer(); diff --git a/communication/provider/customlink/tests/communication_feature_test.php b/communication/provider/customlink/tests/communication_feature_test.php new file mode 100644 index 0000000000000..60d0cbd21cdf6 --- /dev/null +++ b/communication/provider/customlink/tests/communication_feature_test.php @@ -0,0 +1,117 @@ +. + +namespace communication_customlink; + +use core_communication\processor; +use core_communication\communication_test_helper_trait; + +defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/../../../tests/communication_test_helper_trait.php'); + +/** + * Class communication_feature_test to test the custom link features implemented using the core interfaces. + * + * @package communication_customlink + * @category test + * @copyright 2023 Michael Hawkins + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \communication_customlink\communication_feature + */ +class communication_feature_test extends \advanced_testcase { + + use communication_test_helper_trait; + + public function setUp(): void { + parent::setUp(); + $this->resetAfterTest(); + $this->setup_communication_configs(); + } + + /** + * Test create, update and delete chat room. + * + * @covers ::load_for_instance + */ + public function test_load_for_instance(): void { + $communicationprocessor = $this->get_test_communication_processor(); + + $instance = communication_feature::load_for_instance($communicationprocessor); + $this->assertInstanceOf('communication_customlink\communication_feature', $instance); + } + + /** + * Test create, update and delete chat room. + * + * @covers ::create_chat_room + * @covers ::update_chat_room + * @covers ::delete_chat_room + */ + public function test_create_update_delete_chat_room(): void { + $communicationprocessor = $this->get_test_communication_processor(); + + // Create, update and delete room should always return true because this provider contains + // a link to a room, but does not manage the existence of the room. + $createroomresult = $communicationprocessor->get_room_provider()->create_chat_room(); + $updateroomresult = $communicationprocessor->get_room_provider()->update_chat_room(); + $deleteroomresult = $communicationprocessor->get_room_provider()->delete_chat_room(); + $this->assertTrue($createroomresult); + $this->assertTrue($updateroomresult); + $this->assertTrue($deleteroomresult); + } + + /** + * Test save form data with provider's custom field and fetching with get_chat_room_url(). + * + * @covers ::save_form_data + * @covers ::get_chat_room_url + */ + public function test_save_form_data(): void { + $communicationprocessor = $this->get_test_communication_processor(); + $customlinkurl = 'https://moodle.org/message/index.php'; + $formdatainstance = (object) ['customlinkurl' => $customlinkurl]; + + // Test the custom link URL is saved and can be retrieved as expected. + $communicationprocessor->get_form_provider()->save_form_data($formdatainstance); + $fetchedurl = $communicationprocessor->get_room_provider()->get_chat_room_url(); + $this->assertEquals($customlinkurl, $fetchedurl); + } + + /** + * Create a test custom link communication processor object. + * + * @return processor + */ + protected function get_test_communication_processor(): processor { + $course = $this->getDataGenerator()->create_course(); + $instanceid = $course->id; + $component = 'core_course'; + $instancetype = 'coursecommunication'; + $selectedcommunication = 'communication_customlink'; + $communicationroomname = 'communicationroom'; + + $communicationprocessor = processor::create_instance( + $selectedcommunication, + $instanceid, + $component, + $instancetype, + $communicationroomname, + ); + + return $communicationprocessor; + } +} diff --git a/communication/provider/customlink/version.php b/communication/provider/customlink/version.php new file mode 100644 index 0000000000000..51040bd7eb8e7 --- /dev/null +++ b/communication/provider/customlink/version.php @@ -0,0 +1,30 @@ +. + +/** + * Version information for communication_customlink. + * + * @package communication_customlink + * @copyright 2023 Michael Hawkins + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'communication_customlink'; +$plugin->version = 2023082600; +$plugin->requires = 2023082600; +$plugin->maturity = MATURITY_ALPHA;