diff --git a/communication/classes/api.php b/communication/classes/api.php index b9840cd1992..db3f454bec7 100644 --- a/communication/classes/api.php +++ b/communication/classes/api.php @@ -451,6 +451,9 @@ class api { ); } + // Reload so the currently selected provider is used. + $this->reload(); + // Update provider record from form data. if ($instance !== null) { $this->communication->get_form_provider()->save_form_data($instance); @@ -514,8 +517,8 @@ class api { return; } - // No userids? don't bother doing anything. - if (empty($userids)) { + // No user IDs or this provider does not manage users? No action required. + if (empty($userids) || !$this->communication->supports_user_features()) { return; } @@ -542,12 +545,14 @@ class api { return; } - if ($this->communication->get_provider() === processor::PROVIDER_NONE) { + $provider = $this->communication->get_provider(); + + if ($provider === processor::PROVIDER_NONE) { return; } - // No user ids? don't bother doing anything. - if (empty($userids)) { + // No user IDs or this provider does not manage users? No action required. + if (empty($userids) || !$this->communication->supports_user_features()) { return; } diff --git a/communication/classes/task/create_and_configure_room_task.php b/communication/classes/task/create_and_configure_room_task.php index 1327e4e723d..a65dfee5d46 100644 --- a/communication/classes/task/create_and_configure_room_task.php +++ b/communication/classes/task/create_and_configure_room_task.php @@ -46,13 +46,12 @@ class create_and_configure_room_task extends adhoc_task { return; } - // If the room is created successfully, add members to the room. - if ($communication->get_room_provider()->create_chat_room()) { + // If the room is created successfully, add members to the room if supported by the provider. + if ($communication->get_room_provider()->create_chat_room() && $communication->supports_user_features()) { add_members_to_room_task::queue( $communication ); } - } /** diff --git a/communication/provider/customlink/classes/communication_feature.php b/communication/provider/customlink/classes/communication_feature.php new file mode 100644 index 00000000000..a85a320a72a --- /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 00000000000..898502d85e7 --- /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 00000000000..e06baed0057 --- /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 00000000000..bcb64d4a79d --- /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 00000000000..b684484cdb7 --- /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 00000000000..066cab93cc9 --- /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 00000000000..7bf5a61450f --- /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 00000000000..60d0cbd21cd --- /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 00000000000..51040bd7eb8 --- /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; diff --git a/communication/provider/matrix/tests/communication_feature_test.php b/communication/provider/matrix/tests/communication_feature_test.php index 7b81f24775f..d90fc3483c3 100644 --- a/communication/provider/matrix/tests/communication_feature_test.php +++ b/communication/provider/matrix/tests/communication_feature_test.php @@ -167,6 +167,8 @@ class communication_feature_test extends \advanced_testcase { ], ); + $provider->reload(); + // Then call the provider's update method to actually perform the change. $provider->update_chat_room(); diff --git a/lib/tests/behat/behat_transformations.php b/lib/tests/behat/behat_transformations.php index cd5c4a41221..bbd80ad44d3 100644 --- a/lib/tests/behat/behat_transformations.php +++ b/lib/tests/behat/behat_transformations.php @@ -107,6 +107,7 @@ class behat_transformations extends behat_base { * @return TableNode The transformed table */ public function tablenode_transformations(TableNode $tablenode) { + global $CFG; // Walk through all values including the optional headers. $rows = $tablenode->getRows(); foreach ($rows as $rowkey => $row) { @@ -123,6 +124,11 @@ class behat_transformations extends behat_base { $rows[$rowkey][$colkey] = $this->get_transformed_timestamp($match[1]); } } + + // Transform wwwroot. + if (preg_match('/#wwwroot#/', $rows[$rowkey][$colkey])) { + $rows[$rowkey][$colkey] = $this->replace_wwwroot($rows[$rowkey][$colkey]); + } } } @@ -133,6 +139,18 @@ class behat_transformations extends behat_base { return $tablenode; } + /** + * Convert #wwwroot# to the wwwroot config value, so it is + * possible to reference fully qualified URLs within the site. + * + * @Transform /^((.*)#wwwroot#(.*))$/ + * @param string $string + * @return string + */ + public function arg_insert_wwwroot(string $string): string { + return $this->replace_wwwroot($string); + } + /** * Replaces $NASTYSTRING vars for a nasty string. * @@ -176,4 +194,15 @@ class behat_transformations extends behat_base { return $time; } } + + /** + * Replace #wwwroot# with the actual wwwroot config value. + * + * @param string $string String to attempt the replacement in. + * @return string + */ + protected function replace_wwwroot(string $string): string { + global $CFG; + return str_replace('#wwwroot#', $CFG->wwwroot, $string); + } } diff --git a/theme/boost/scss/moodle/drawer.scss b/theme/boost/scss/moodle/drawer.scss index cc1606bda7b..b66185540ee 100644 --- a/theme/boost/scss/moodle/drawer.scss +++ b/theme/boost/scss/moodle/drawer.scss @@ -47,7 +47,8 @@ $drawer-bg: darken($body-bg, 5%) !default; [data-region="drawer"] { padding: $drawer-padding-x $drawer-padding-y; } - .jsenabled .btn-footer-popover { + .jsenabled .btn-footer-popover, + .jsenabled .btn-footer-communication { @include transition(0.2s); } } diff --git a/theme/boost/scss/moodle/layout.scss b/theme/boost/scss/moodle/layout.scss index 13435722a4c..3be9b6b4bff 100644 --- a/theme/boost/scss/moodle/layout.scss +++ b/theme/boost/scss/moodle/layout.scss @@ -228,7 +228,8 @@ margin-left: 0; margin-right: $drawer-right-width; padding-right: 1rem; - .jsenabled & .btn-footer-popover { + .jsenabled & .btn-footer-popover, + .jsenabled & .btn-footer-communication { right: calc(#{$drawer-right-width} + 2rem); } } diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index 0e8207710c3..c97a46055f2 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -29586,12 +29586,14 @@ span.editinstructions .alert-link { [data-region=drawer] { padding: 20px 20px; } - .jsenabled .btn-footer-popover { + .jsenabled .btn-footer-popover, + .jsenabled .btn-footer-communication { transition: 0.2s; } } @media (min-width: 576px) and (prefers-reduced-motion: reduce) { - .jsenabled .btn-footer-popover { + .jsenabled .btn-footer-popover, + .jsenabled .btn-footer-communication { transition: none; } } @@ -36653,7 +36655,7 @@ span[data-flexitour=container][x-placement=right] div[data-role=arrow]:after, sp margin-right: 315px; padding-right: 1rem; } - .jsenabled #page.drawers.show-drawer-right .btn-footer-popover { + .jsenabled #page.drawers.show-drawer-right .btn-footer-popover, .jsenabled #page.drawers.show-drawer-right .btn-footer-communication { right: calc(315px + 2rem); } } diff --git a/theme/boost/templates/footer.mustache b/theme/boost/templates/footer.mustache index 1bd8d6af3d4..e5f15bfa9b1 100644 --- a/theme/boost/templates/footer.mustache +++ b/theme/boost/templates/footer.mustache @@ -38,7 +38,7 @@