diff --git a/admin/environment.xml b/admin/environment.xml index 2fd7533f84a21..37916496422b0 100644 --- a/admin/environment.xml +++ b/admin/environment.xml @@ -4703,4 +4703,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/admin/presets/tests/generator_test.php b/admin/presets/tests/generator_test.php index 52ef904e99662..a8bef76ceed48 100644 --- a/admin/presets/tests/generator_test.php +++ b/admin/presets/tests/generator_test.php @@ -180,7 +180,7 @@ public function test_create_preset(?string $name = null, ?string $comments = nul * * @return array */ - public function create_preset_provider(): array { + public static function create_preset_provider(): array { return [ 'Default values' => [ ], diff --git a/admin/presets/tests/helper_test.php b/admin/presets/tests/helper_test.php index f0e39c1ed9782..b9e97707be511 100644 --- a/admin/presets/tests/helper_test.php +++ b/admin/presets/tests/helper_test.php @@ -84,7 +84,7 @@ public function test_create_preset(?string $name = null, ?string $comments = nul * * @return array */ - public function create_preset_provider(): array { + public static function create_preset_provider(): array { return [ 'Default values' => [ ], @@ -177,7 +177,7 @@ public function test_add_item(string $name, string $value, ?string $plugin = 'no * * @return array */ - public function add_item_provider(): array { + public static function add_item_provider(): array { return [ 'Setting without plugin' => [ 'name' => 'settingname', @@ -239,7 +239,7 @@ public function test_add_plugin(string $type, string $name, $enabled = 0): void * * @return array */ - public function add_plugin_provider(): array { + public static function add_plugin_provider(): array { return [ 'Plugin: enabled (using int)' => [ 'type' => 'plugintype', @@ -321,7 +321,7 @@ public function test_change_default_preset(string $preset, ?array $settings = nu * * @return array */ - public function change_default_preset_provider(): array { + public static function change_default_preset_provider(): array { return [ 'Starter preset' => [ 'preset' => 'starter', @@ -364,7 +364,7 @@ public function change_default_preset_provider(): array { 'preset' => 'unexisting', ], 'Valid XML file' => [ - 'preset' => __DIR__ . '/fixtures/import_settings_plugins.xml', + 'preset' => self::get_fixture_path(__NAMESPACE__, 'import_settings_plugins.xml'), 'settings' => [ 'allowemojipicker' => 1, 'enableportfolios' => 1, @@ -377,7 +377,7 @@ public function change_default_preset_provider(): array { ], ], 'Invalid XML file' => [ - 'preset' => __DIR__ . '/fixtures/invalid_xml_file.xml', + 'preset' => self::get_fixture_path(__NAMESPACE__, 'invalid_xml_file.xml'), ], 'Unexisting XML file' => [ 'preset' => __DIR__ . '/fixtures/unexisting.xml', diff --git a/admin/presets/tests/local/setting/adminpresets_admin_setting_bloglevel_test.php b/admin/presets/tests/local/setting/adminpresets_admin_setting_bloglevel_test.php index d59108abbc54e..4f6a59a5a65e3 100644 --- a/admin/presets/tests/local/setting/adminpresets_admin_setting_bloglevel_test.php +++ b/admin/presets/tests/local/setting/adminpresets_admin_setting_bloglevel_test.php @@ -72,7 +72,7 @@ public function test_save_value(int $settingvalue, bool $expectedsaved): void { * * @return array */ - public function save_value_provider(): array { + public static function save_value_provider(): array { return [ 'Save the bloglevel and set blog_menu block visibility to true' => [ 'setttingvalue' => BLOG_USER_LEVEL, diff --git a/admin/presets/tests/local/setting/adminpresets_admin_setting_sitesettext_test.php b/admin/presets/tests/local/setting/adminpresets_admin_setting_sitesettext_test.php index 3ae332154faa2..de2f2159800c1 100644 --- a/admin/presets/tests/local/setting/adminpresets_admin_setting_sitesettext_test.php +++ b/admin/presets/tests/local/setting/adminpresets_admin_setting_sitesettext_test.php @@ -66,7 +66,7 @@ public function test_save_value(string $settingname, string $settingvalue, bool * * @return array */ - public function save_value_provider(): array { + public static function save_value_provider(): array { return [ 'Fullname: different value' => [ 'settingname' => 'fullname', diff --git a/admin/presets/tests/local/setting/adminpresets_setting_test.php b/admin/presets/tests/local/setting/adminpresets_setting_test.php index 3cd81ed5d549f..e57dbb9ed0e32 100644 --- a/admin/presets/tests/local/setting/adminpresets_setting_test.php +++ b/admin/presets/tests/local/setting/adminpresets_setting_test.php @@ -77,7 +77,7 @@ public function test_save_value(string $category, string $settingplugin, string * * @return array */ - public function save_value_provider(): array { + public static function save_value_provider(): array { return [ 'Core setting with the same value is not saved' => [ 'category' => 'optionalsubsystems', @@ -167,7 +167,7 @@ public function test_save_attributes_values(string $category, string $settingplu * * @return array */ - public function save_attributes_values_provider(): array { + public static function save_attributes_values_provider(): array { return [ 'Plugin setting with the same value is not saved' => [ 'category' => 'modsettinglesson', diff --git a/admin/presets/tests/manager_test.php b/admin/presets/tests/manager_test.php index 2ab19814ebc3f..bb2381524b7e4 100644 --- a/admin/presets/tests/manager_test.php +++ b/admin/presets/tests/manager_test.php @@ -367,7 +367,7 @@ public function test_export_preset(bool $includesensible = false, string $preset * * @return array */ - public function export_preset_provider(): array { + public static function export_preset_provider(): array { return [ 'Export settings and plugins, excluding sensible' => [ 'includesensible' => false, @@ -530,26 +530,32 @@ public function test_import_preset(string $filecontents, bool $expectedpreset, b * * @return array */ - public function import_preset_provider(): array { + public static function import_preset_provider(): array { return [ 'Import settings from an empty file' => [ 'filecontents' => '', 'expectedpreset' => false, ], 'Import settings and plugins from a valid XML file' => [ - 'filecontents' => file_get_contents(__DIR__ . '/fixtures/import_settings_plugins.xml'), + 'filecontents' => file_get_contents( + filename: self::get_fixture_path(__NAMESPACE__, 'import_settings_plugins.xml') + ), 'expectedpreset' => true, 'expectedsettings' => true, 'expectedplugins' => true, ], 'Import only settings from a valid XML file' => [ - 'filecontents' => file_get_contents(__DIR__ . '/fixtures/import_settings.xml'), + 'filecontents' => file_get_contents( + filename: self::get_fixture_path(__NAMESPACE__, 'import_settings.xml') + ), 'expectedpreset' => true, 'expectedsettings' => true, 'expectedplugins' => false, ], 'Import settings and plugins from a valid XML file with Starter name, which will be marked as non-core' => [ - 'filecontents' => file_get_contents(__DIR__ . '/fixtures/import_starter_name.xml'), + 'filecontents' => file_get_contents( + filename: self::get_fixture_path(__NAMESPACE__, 'import_starter_name.xml') + ), 'expectedpreset' => true, 'expectedsettings' => true, 'expectedplugins' => true, @@ -558,7 +564,9 @@ public function import_preset_provider(): array { 'expectedpresetname' => 'Starter', ], 'Import settings from an invalid XML file' => [ - 'filecontents' => file_get_contents(__DIR__ . '/fixtures/invalid_xml_file.xml'), + 'filecontents' => file_get_contents( + filename: self::get_fixture_path(__NAMESPACE__, 'invalid_xml_file.xml') + ), 'expectedpreset' => false, 'expectedsettings' => false, 'expectedplugins' => false, @@ -566,20 +574,26 @@ public function import_preset_provider(): array { 'expectedexception' => \Exception::class, ], 'Import unexisting settings category' => [ - 'filecontents' => file_get_contents(__DIR__ . '/fixtures/unexisting_category.xml'), + 'filecontents' => file_get_contents( + filename: self::get_fixture_path(__NAMESPACE__, 'unexisting_category.xml') + ), 'expectedpreset' => false, 'expectedsettings' => false, 'expectedplugins' => false, ], 'Import unexisting setting' => [ - 'filecontents' => file_get_contents(__DIR__ . '/fixtures/unexisting_setting.xml'), + 'filecontents' => file_get_contents( + filename: self::get_fixture_path(__NAMESPACE__, 'unexisting_setting.xml') + ), 'expectedpreset' => false, 'expectedsettings' => false, 'expectedplugins' => false, 'expecteddebugging' => true, ], 'Import valid settings with one unexisting setting too' => [ - 'filecontents' => file_get_contents(__DIR__ . '/fixtures/import_settings_with_unexisting_setting.xml'), + 'filecontents' => file_get_contents( + filename: self::get_fixture_path(__NAMESPACE__, 'import_settings_with_unexisting_setting.xml'), + ), 'expectedpreset' => true, 'expectedsettings' => false, 'expectedplugins' => false, diff --git a/admin/renderer.php b/admin/renderer.php index 0f9333f78e589..177a19a29c168 100644 --- a/admin/renderer.php +++ b/admin/renderer.php @@ -874,8 +874,8 @@ public function warn_if_not_registered() { protected function mobile_configuration_warning($mobileconfigured) { $output = ''; if (!$mobileconfigured) { - $settingslink = new moodle_url('/admin/settings.php', ['section' => 'mobilesettings']); - $configurebutton = $this->single_button($settingslink, get_string('enablemobilewebservice', 'admin')); + $settingslink = new moodle_url('/admin/search.php', ['query' => 'enablemobilewebservice']); + $configurebutton = $this->single_button($settingslink, get_string('enablemobilewebservice', 'admin'), 'get'); $output .= $this->warning(get_string('mobilenotconfiguredwarning', 'admin') . ' ' . $configurebutton); } diff --git a/admin/roles/tests/preset_test.php b/admin/roles/tests/preset_test.php index bc18626b12f85..bc7b32c15fe7c 100644 --- a/admin/roles/tests/preset_test.php +++ b/admin/roles/tests/preset_test.php @@ -84,7 +84,7 @@ public function test_xml(): void { public function test_mixed_levels(): void { // The problem here is that we cannot guarantee plugin contexts // have unique short names, so we have to also support level numbers. - $xml = file_get_contents(__DIR__ . '/fixtures/mixed_levels.xml'); + $xml = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'mixed_levels.xml')); $this->assertTrue(\core_role_preset::is_valid_preset($xml)); $preset = \core_role_preset::parse_preset($xml); diff --git a/admin/settings/courses.php b/admin/settings/courses.php index d1776633a5f9d..c45cda39ff5fe 100644 --- a/admin/settings/courses.php +++ b/admin/settings/courses.php @@ -420,6 +420,12 @@ new lang_string('generalgroups', 'backup'), new lang_string('configgeneralgroups', 'backup'), array('value' => 1, 'locked' => 0))); $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_competencies', new lang_string('generalcompetencies','backup'), new lang_string('configgeneralcompetencies','backup'), array('value'=>1, 'locked'=>0))); + $temp->add(new admin_setting_configcheckbox_with_lock( + 'backup/backup_general_customfield', + new lang_string('generalcustomfield', 'backup'), + new lang_string('configgeneralcustomfield', 'backup'), + ['value' => 1, 'locked' => 0], + )); $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_contentbankcontent', new lang_string('generalcontentbankcontent', 'backup'), new lang_string('configgeneralcontentbankcontent', 'backup'), @@ -431,7 +437,6 @@ ['value' => 1, 'locked' => 0]) ); - $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_legacyfiles', new lang_string('generallegacyfiles', 'backup'), new lang_string('configlegacyfiles', 'backup'), array('value' => 1, 'locked' => 0))); @@ -460,6 +465,12 @@ new lang_string('generalgroups', 'backup'), new lang_string('configgeneralgroups', 'backup'), array('value' => 1, 'locked' => 0))); $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_import_competencies', new lang_string('generalcompetencies','backup'), new lang_string('configgeneralcompetencies','backup'), array('value'=>1, 'locked'=>0))); + $temp->add(new admin_setting_configcheckbox_with_lock( + 'backup/backup_import_customfield', + new lang_string('generalcustomfield', 'backup'), + new lang_string('configgeneralcustomfield', 'backup'), + ['value' => 1, 'locked' => 0], + )); $temp->add(new admin_setting_configcheckbox_with_lock( 'backup/backup_import_contentbankcontent', new lang_string('generalcontentbankcontent', 'backup'), @@ -589,6 +600,12 @@ $temp->add(new admin_setting_configcheckbox('backup/backup_auto_groups', new lang_string('generalgroups', 'backup'), new lang_string('configgeneralgroups', 'backup'), 1)); $temp->add(new admin_setting_configcheckbox('backup/backup_auto_competencies', new lang_string('generalcompetencies','backup'), new lang_string('configgeneralcompetencies','backup'), 1)); + $temp->add(new admin_setting_configcheckbox( + 'backup/backup_auto_customfield', + new lang_string('generalcustomfield', 'backup'), + new lang_string('configgeneralcustomfield', 'backup'), + 1, + )); $temp->add(new admin_setting_configcheckbox( 'backup/backup_auto_contentbankcontent', new lang_string('generalcontentbankcontent', 'backup'), @@ -668,6 +685,12 @@ $temp->add(new admin_setting_configcheckbox_with_lock('restore/restore_general_competencies', new lang_string('generalcompetencies', 'backup'), new lang_string('configrestorecompetencies', 'backup'), array('value' => 1, 'locked' => 0))); + $temp->add(new admin_setting_configcheckbox_with_lock( + 'restore/restore_general_customfield', + new lang_string('generalcustomfield', 'backup'), + new lang_string('configrestorecustomfield', 'backup'), + ['value' => 1, 'locked' => 0], + )); $temp->add(new admin_setting_configcheckbox_with_lock('restore/restore_general_contentbankcontent', new lang_string('generalcontentbankcontent', 'backup'), new lang_string('configrestorecontentbankcontent', 'backup'), array('value' => 1, 'locked' => 0))); diff --git a/admin/tests/external/set_block_protection_test.php b/admin/tests/external/set_block_protection_test.php index 60cbd4071fba9..82f9ece14e499 100644 --- a/admin/tests/external/set_block_protection_test.php +++ b/admin/tests/external/set_block_protection_test.php @@ -85,7 +85,7 @@ public function test_execute( * * @return array */ - public function execute_provider(): array { + public static function execute_provider(): array { return [ [ 'block_login', diff --git a/admin/tests/external/set_plugin_order_test.php b/admin/tests/external/set_plugin_order_test.php index 8673e3c4ae163..33482cb4e11c5 100644 --- a/admin/tests/external/set_plugin_order_test.php +++ b/admin/tests/external/set_plugin_order_test.php @@ -72,7 +72,7 @@ public function test_execute_editors( * * @return array */ - public function execute_editor_provider(): array { + public static function execute_editor_provider(): array { $pluginmanager = \core_plugin_manager::instance(); $allplugins = array_keys($pluginmanager->get_plugins_of_type('editor')); @@ -142,7 +142,7 @@ public function test_execute_editors_non_orderable(string $plugin): void { $this->assertIsArray(set_plugin_order::execute($plugin, 1)); } - public function execute_non_orderable_provider(): array { + public static function execute_non_orderable_provider(): array { return [ // Activities do not support ordering. ['mod_assign'], diff --git a/admin/tests/external/set_plugin_state_test.php b/admin/tests/external/set_plugin_state_test.php index febea13989884..9bdff10ba4766 100644 --- a/admin/tests/external/set_plugin_state_test.php +++ b/admin/tests/external/set_plugin_state_test.php @@ -66,7 +66,7 @@ public function test_execute( * * @return array */ - public function execute_standard_provider(): array { + public static function execute_standard_provider(): array { $generatetestsfor = function (string $plugin): array { return [ [ diff --git a/admin/tests/reportbuilder/datasource/task_logs_test.php b/admin/tests/reportbuilder/datasource/task_logs_test.php index 4acd7ba845b44..511cd3328361f 100644 --- a/admin/tests/reportbuilder/datasource/task_logs_test.php +++ b/admin/tests/reportbuilder/datasource/task_logs_test.php @@ -110,7 +110,7 @@ public function test_datasource_non_default_columns(): void { * * @return array[] */ - public function datasource_filters_provider(): array { + public static function datasource_filters_provider(): array { return [ 'Filter name' => ['task_log:name', [ 'task_log:name_values' => [send_schedules::class], diff --git a/admin/tool/admin_presets/tests/behat/download.feature b/admin/tool/admin_presets/tests/behat/download.feature index 2866028325f8d..f2ba8b714aa15 100644 --- a/admin/tool/admin_presets/tests/behat/download.feature +++ b/admin/tool/admin_presets/tests/behat/download.feature @@ -5,16 +5,18 @@ Feature: I can download a preset | name | | Custom preset | - @javascript Scenario: Custom preset settings can be downloaded Given I log in as "admin" And I navigate to "Site admin presets" in site administration When I open the action menu in "Custom preset" "table_row" - Then following "Download" "link" in the "Custom preset" "table_row" should download between "0" and "5000" bytes + Then following "Download" in the "Custom preset" "table_row" should download a file that: + | Has mimetype | text/xml | + | Contains text in xml element | Custom preset | - @javascript Scenario: Core preset settings can be downloaded Given I log in as "admin" And I navigate to "Site admin presets" in site administration When I open the action menu in "Starter" "table_row" - Then following "Download" "link" in the "Starter" "table_row" should download between "0" and "5000" bytes + Then following "Download" in the "Starter" "table_row" should download a file that: + | Has mimetype | text/xml | + | Contains text in xml element | Starter | diff --git a/admin/tool/admin_presets/tests/local/action/base_test.php b/admin/tool/admin_presets/tests/local/action/base_test.php index 2cf069cc36158..de2b8354057d3 100644 --- a/admin/tool/admin_presets/tests/local/action/base_test.php +++ b/admin/tool/admin_presets/tests/local/action/base_test.php @@ -70,7 +70,7 @@ public function test_base_log(string $action, string $mode, ?string $expectedcla * * @return array */ - public function log_provider(): array { + public static function log_provider(): array { return [ // Action = base. 'action=base and mode = show' => [ diff --git a/admin/tool/admin_presets/tests/local/action/export_test.php b/admin/tool/admin_presets/tests/local/action/export_test.php index 4e858f400b971..0a7660b7939c5 100644 --- a/admin/tool/admin_presets/tests/local/action/export_test.php +++ b/admin/tool/admin_presets/tests/local/action/export_test.php @@ -156,7 +156,7 @@ public function test_export_execute(bool $includesensible = false, string $prese * * @return array */ - public function export_execute_provider(): array { + public static function export_execute_provider(): array { return [ 'Export settings and plugins, excluding sensible' => [ 'includesensible' => false, diff --git a/admin/tool/admin_presets/tests/local/action/import_test.php b/admin/tool/admin_presets/tests/local/action/import_test.php index db31668f2c9d1..9369100a93b27 100644 --- a/admin/tool/admin_presets/tests/local/action/import_test.php +++ b/admin/tool/admin_presets/tests/local/action/import_test.php @@ -190,7 +190,7 @@ public function test_import_execute(string $filecontents, bool $expectedpreset, * * @return array */ - public function import_execute_provider(): array { + public static function import_execute_provider(): array { $fixturesfolder = __DIR__ . '/../../../../../presets/tests/fixtures/'; return [ diff --git a/admin/tool/behat/tests/behat/frozen_clock.feature b/admin/tool/behat/tests/behat/frozen_clock.feature new file mode 100644 index 0000000000000..91b11bcfdf11d --- /dev/null +++ b/admin/tool/behat/tests/behat/frozen_clock.feature @@ -0,0 +1,61 @@ +@tool @tool_behat +Feature: Frozen clock in Behat + In order to write tests that depend on the current system time + As a test writer + I need to set the time using a Behat step + + Background: + Given the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + And the following "activities" exist: + | activity | course | name | idnumber | externalurl | + | url | C1 | Fixture | url1 | #wwwroot#/admin/tool/behat/tests/fixtures/core/showtime.php | + | forum | C1 | TestForum | forum1 | | + + Scenario: Time has been frozen + # Set up 2 forum discussions at different times. This tests the clock in the Behat CLI process. + Given the time is frozen at "2024-03-01 12:34:56" + And the following "mod_forum > discussions" exist: + | user | forum | name | message | + | admin | forum1 | Subject1 | Message1 | + And the time is frozen at "2024-08-01 12:34:56" + And the following "mod_forum > discussions" exist: + | user | forum | name | message | + | admin | forum1 | Subject2 | Message2 | + When I am on the "TestForum" "forum activity" page logged in as admin + Then I should see "1 Mar 2024" in the "Subject1" "table_row" + And I should see "1 Aug 2024" in the "Subject2" "table_row" + # Also view time on the fixture page. This tests the clock for Behat web server requests. + And I am on the "Fixture" "url activity" page + And I should see "Behat time is not the same as real time" + # This Unix time corresponds to 12:34:56 in Perth time zone. + And I should see "Unix time 1722486896" + And I should see "Date-time 2024-08-01 12:34:56" + + # This scenario is second, to verify that the clock automatically goes back to normal after test. + Scenario: Time is normal + Given the following "mod_forum > discussions" exist: + | user | forum | name | message | + | admin | forum1 | Subject1 | Message1 | + When I am on the "TestForum" "forum activity" page logged in as admin + # The time should be the real current time, not the frozen time. + Then I should see "## today ##%d %b %Y##" in the "Subject1" "table_row" + And I am on the "Fixture" "url activity" page + And I should see "Behat time is the same as real time" + + Scenario: Time is frozen and then unfrozen + Given the time is frozen at "2024-03-01 12:34:56" + And the following "mod_forum > discussions" exist: + | user | forum | name | message | + | admin | forum1 | Subject1 | Message1 | + And the time is no longer frozen + And the following "mod_forum > discussions" exist: + | user | forum | name | message | + | admin | forum1 | Subject2 | Message2 | + When I am on the "TestForum" "forum activity" page logged in as admin + Then I should see "1 Mar 2024" in the "Subject1" "table_row" + # The time should be the real current time, not the frozen time for this entry. + And I should see "## today ##%d %b %Y##" in the "Subject2" "table_row" + And I am on the "Fixture" "url activity" page + And I should see "Behat time is the same as real time" diff --git a/admin/tool/behat/tests/behat/tabs.feature b/admin/tool/behat/tests/behat/tabs.feature index 308af83cb2251..210b916e5266c 100644 --- a/admin/tool/behat/tests/behat/tabs.feature +++ b/admin/tool/behat/tests/behat/tabs.feature @@ -25,7 +25,7 @@ Feature: Confirm that we can open multiple browser tabs And I open a tab named "CourseViewer4" on the "My courses" page # Switch between all the tabs and confirm their different contents. - Then I should see "You're not enrolled in any course" + Then I should see "You're not enrolled in any courses." And I switch to "CourseViewer2" tab And "Course 3" "heading" should exist And I switch to "CourseViewer1" tab diff --git a/admin/tool/behat/tests/behat_form_text_test.php b/admin/tool/behat/tests/behat_form_text_test.php index a57906959aeca..105ab13f5c397 100644 --- a/admin/tool/behat/tests/behat_form_text_test.php +++ b/admin/tool/behat/tests/behat_form_text_test.php @@ -53,7 +53,7 @@ class behat_form_text_test extends \basic_testcase { * * @return array of value and expectation pairs to be tested. */ - public function provider_test_set_get_value() { + public static function provider_test_set_get_value(): array { return [ 'null' => [null, null], 'int' => [3, 3], @@ -68,7 +68,7 @@ public function provider_test_set_get_value() { * * @param mixed $value value to be set. * @param mixed $expectation value to be checked. - * @dataProvider provider_test_set_get_value() + * @dataProvider provider_test_set_get_value */ public function test_set_get_value($value, $expectation): void { $session = $this->createMock(Session::class); @@ -85,7 +85,7 @@ public function test_set_get_value($value, $expectation): void { * * @return array of decsep, value, match and result pairs to be tested. */ - public function provider_test_matches() { + public static function provider_test_matches(): array { return [ 'lazy true' => ['.', 'hello', 'hello', true], 'lazy false' => ['.', 'hello', 'bye', false], @@ -113,7 +113,7 @@ public function provider_test_matches() { * @param mixed $value value to be set. * @param mixed $match value to be matched. * @param bool $result expected return status of the function. - * @dataProvider provider_test_matches() + * @dataProvider provider_test_matches */ public function test_matches($decsep, $value, $match, $result): void { global $CFG; diff --git a/admin/tool/behat/tests/fixtures/core/showtime.php b/admin/tool/behat/tests/fixtures/core/showtime.php new file mode 100644 index 0000000000000..6cc27b74eb3b8 --- /dev/null +++ b/admin/tool/behat/tests/fixtures/core/showtime.php @@ -0,0 +1,52 @@ +. + +/** + * Fixture to show the current server time using \core\clock. + * + * @package tool_behat + * @copyright 2024 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// phpcs:disable moodle.Files.RequireLogin.Missing +require(__DIR__ . '/../../../../../../config.php'); + +defined('BEHAT_SITE_RUNNING') || die('Behat fixture'); + +$PAGE->set_context(\context_system::instance()); +$PAGE->set_url(new \moodle_url('/admin/tool/behat/tests/fixtures/core/showtime.php')); + +echo $OUTPUT->header(); + +$clock = \core\di::get(\core\clock::class); +$dt = $clock->now(); +$realbefore = time(); +$time = $clock->time(); +$realafter = time(); + +echo html_writer::div('Unix time ' . $time); +echo html_writer::div('Date-time ' . $dt->format('Y-m-d H:i:s')); + +echo html_writer::div('TZ ' . $dt->getTimezone()->getName()); + +if ($time >= $realbefore && $time <= $realafter) { + echo html_writer::div('Behat time is the same as real time'); +} else { + echo html_writer::div('Behat time is not the same as real time'); +} + +echo $OUTPUT->footer(); diff --git a/admin/tool/behat/tests/manager_util_test.php b/admin/tool/behat/tests/manager_util_test.php index 5bb6627231c8f..35ff061ffc4ba 100644 --- a/admin/tool/behat/tests/manager_util_test.php +++ b/admin/tool/behat/tests/manager_util_test.php @@ -107,11 +107,11 @@ public function setUp(): void { */ private function get_behat_config_util($behatconfigutil, $notheme = false) { // Create a map of arguments to return values. - $map = array( - array('withfeatures', __DIR__.'/fixtures/theme/withfeatures'), - array('nofeatures', __DIR__.'/fixtures/theme/nofeatures'), - array('defaulttheme', __DIR__.'/fixtures/theme/defaulttheme'), - ); + $map = [ + ['withfeatures', self::get_fixture_path(__NAMESPACE__, 'theme/withfeatures')], + ['nofeatures', self::get_fixture_path(__NAMESPACE__, 'theme/nofeatures')], + ['defaulttheme', self::get_fixture_path(__NAMESPACE__, 'theme/defaulttheme')], + ]; // List of themes is const for test. if ($notheme) { $themelist = array('defaulttheme'); diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_color_test.php b/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_color_test.php index f93e43e2abc86..17a55cd65aef6 100644 --- a/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_color_test.php +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_color_test.php @@ -410,13 +410,13 @@ public function get_fontsize(string $fontsize): int { $pos3 = stripos($fontsize, 'px'); if ($pos1 !== false) { $rem = substr($fontsize, 0, -3); - $newfontsize = $newfontsize * $rem; + $newfontsize = is_numeric($rem) ? $newfontsize * $rem : $newfontsize; } else if ($pos2 !== false) { $em = substr($fontsize, 0, -2); - $newfontsize = $newfontsize * $em; + $newfontsize = is_numeric($em) ? $newfontsize * $em : $newfontsize; } else if ($pos3 !== false) { $px = substr($fontsize, 0, -2); - $newfontsize = 0.75 * $px; + $newfontsize = is_numeric($px) ? 0.75 * $px : $newfontsize; } else if (in_array($fontsize, array_keys($this->fontsizenames))) { $newfontsize = $this->fontsizenames[$fontsize]; } else { diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/i_is_not_used.php b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/i_is_not_used.php index a9a7799b32a06..656474b9f527a 100644 --- a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/i_is_not_used.php +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/i_is_not_used.php @@ -16,7 +16,7 @@ namespace tool_brickfield\local\htmlchecker\common\checks; -use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_tag_test; +use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_test; /** * Brickfield accessibility HTML checker library. @@ -28,11 +28,25 @@ * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class i_is_not_used extends brickfield_accessibility_tag_test { +class i_is_not_used extends brickfield_accessibility_test { /** @var int The default severity code for this test. */ public $defaultseverity = \tool_brickfield\local\htmlchecker\brickfield_accessibility::BA_TEST_SEVERE; /** @var string The tag this test will fire on. */ public $tag = 'i'; + + /** + * Check for any i elements and flag them as errors + * while allowing font awesome icons to be used. + */ + public function check(): void { + foreach ($this->get_all_elements('i') as $element) { + // Ensure this is not a font awesome icon with aria-hidden. + if (str_contains($element->getAttribute('class'), 'fa-') && $element->getAttribute('aria-hidden') === 'true') { + continue; + } + $this->add_report($element); + } + } } diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/table_data_should_have_th.php b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/table_data_should_have_th.php index 4d77cb3d42391..cddef948e483f 100644 --- a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/table_data_should_have_th.php +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/table_data_should_have_th.php @@ -46,7 +46,7 @@ public function check(): void { foreach ($tr->childNodes as $th) { if ($this->property_is_equal($th, 'tagName', 'th')) { break 3; - } else { + } else if ($th->nodeName != '#text') { $this->add_report($table); break 3; } @@ -57,7 +57,7 @@ public function check(): void { foreach ($child->childNodes as $th) { if ($this->property_is_equal($th, 'tagName', 'th')) { break 2; - } else { + } else if ($th->nodeName != '#text') { $this->add_report($table); break 2; } diff --git a/admin/tool/brickfield/tests/local/htmlchecker/common/checks/css_text_has_contrast_test.php b/admin/tool/brickfield/tests/local/htmlchecker/common/checks/css_text_has_contrast_test.php index 4f739f0b6f62e..d98d6b54e795d 100644 --- a/admin/tool/brickfield/tests/local/htmlchecker/common/checks/css_text_has_contrast_test.php +++ b/admin/tool/brickfield/tests/local/htmlchecker/common/checks/css_text_has_contrast_test.php @@ -32,7 +32,7 @@ * Class test_css_text_has_contrast_test * @covers \tool_brickfield\local\htmlchecker\brickfield_accessibility */ -class css_text_has_contrast_test extends all_checks { +final class css_text_has_contrast_test extends all_checks { /** @var string The check type. */ protected $checktype = 'css_text_has_contrast'; @@ -227,6 +227,18 @@ class css_text_has_contrast_test extends all_checks { This is contrasty enough.

EOD; + /** @var string HTML with calculated size colour values. */ + private $calculatedfail = <<

+ This is not contrasty enough.

+EOD; + + /** @var string HTML with calculated size colour values. */ + private $calculatedpass = <<

+ This is contrasty enough.

+EOD; + /** * Test for the area assign intro */ @@ -458,4 +470,20 @@ public function test_good_backgroundcssrgba(): void { $results = $this->get_checker_results($html); $this->assertEmpty($results); } + + /** + * Test for calculated (12pt) text with insufficient contrast of 4.49. + */ + public function test_check_for_calculated_fail(): void { + $results = $this->get_checker_results($this->calculatedfail); + $this->assertTrue($results[0]->element->tagName == 'p'); + } + + /** + * Test for calculated (12pt) text with sufficient contrast of 4.81. + */ + public function test_check_for_calculated_pass(): void { + $results = $this->get_checker_results($this->calculatedpass); + $this->assertEmpty($results); + } } diff --git a/admin/tool/brickfield/tests/local/htmlchecker/common/checks/i_is_not_used_test.php b/admin/tool/brickfield/tests/local/htmlchecker/common/checks/i_is_not_used_test.php index a5a6cebb5b190..c32705f5bb00b 100644 --- a/admin/tool/brickfield/tests/local/htmlchecker/common/checks/i_is_not_used_test.php +++ b/admin/tool/brickfield/tests/local/htmlchecker/common/checks/i_is_not_used_test.php @@ -30,6 +30,8 @@ /** * Class i_is_not_used_testcase + * + * @covers \tool_brickfield\local\htmlchecker\common\checks\i_is_not_used */ class i_is_not_used_test extends all_checks { /** @var string Check type */ @@ -71,4 +73,13 @@ public function test_check(): void { $results = $this->get_checker_results($this->htmlpass); $this->assertEmpty($results); } + + /** + * Test for font awesome icon. + */ + public function test_fa_icon(): void { + $html = '
Hello there
'; + $results = $this->get_checker_results($html); + $this->assertCount(2, $results); + } } diff --git a/admin/tool/brickfield/tests/local/htmlchecker/common/checks/table_data_should_have_th_test.php b/admin/tool/brickfield/tests/local/htmlchecker/common/checks/table_data_should_have_th_test.php index e682efbcb95ac..82f12509de951 100644 --- a/admin/tool/brickfield/tests/local/htmlchecker/common/checks/table_data_should_have_th_test.php +++ b/admin/tool/brickfield/tests/local/htmlchecker/common/checks/table_data_should_have_th_test.php @@ -30,6 +30,8 @@ /** * Class table_data_should_have_th_test + * + * @covers \tool_brickfield\local\htmlchecker\common\checks\table_data_should_have_th */ class table_data_should_have_th_test extends all_checks { /** @var string Check type */ @@ -113,6 +115,117 @@ class table_data_should_have_th_test extends all_checks { +EOD; + + /** @var string HTML that should not get flagged. */ + private $htmlpass3 = << + + + Table should have at least one th - pass + + + + + + + + + + + + + +
+ This is table heading +
+ This is a tables data +
+ + +EOD; + + /** @var string HTML that should not get flagged. */ + private $htmlpass4 = << + + + Table should have at least one th - pass + + + + + + + + + +
+ This is table heading +
+ This is a tables data +
+ + +EOD; + + /** @var string HTML that should not get flagged. */ + private $htmlpass5 = << + + + Table should have at least one th - pass + + + + + + + + + + + + + + + + +
+ This is table heading +
+ This is a table heading in table data +
+ This is a tables data +
+ + +EOD; + + /** @var string HTML that should not get flagged. */ + private $htmlpass6 = << + + + Table should have at least one th - pass + + + + + + + + + + + +
+ This is a table heading in table data +
+ This is a tables data +
+ + EOD; /** * Test that th does not exist @@ -134,5 +247,17 @@ public function test_check_pass(): void { $results = $this->get_checker_results($this->htmlpass2); $this->assertEmpty($results); + + $results = $this->get_checker_results($this->htmlpass3); + $this->assertEmpty($results); + + $results = $this->get_checker_results($this->htmlpass4); + $this->assertEmpty($results); + + $results = $this->get_checker_results($this->htmlpass5); + $this->assertEmpty($results); + + $results = $this->get_checker_results($this->htmlpass6); + $this->assertEmpty($results); } } diff --git a/admin/tool/brickfield/tests/tool_test.php b/admin/tool/brickfield/tests/tool_test.php index ed3e64ca033be..75823f00b838e 100644 --- a/admin/tool/brickfield/tests/tool_test.php +++ b/admin/tool/brickfield/tests/tool_test.php @@ -30,7 +30,7 @@ class tool_test extends \advanced_testcase { /** @var string base 64 image */ - protected $base64img = <<'; return [ 'Image tag alone (base64)' => [ - $this->base64img, + self::$base64img, true, ], 'Image tag alone (link)' => [ @@ -130,7 +130,7 @@ public function base64_img_provider(): array { false, ], 'Image tag in string (base64)' => [ - "This is my image {$this->base64img}.", + "This is my image " . self::$base64img, true, ], 'Image tag in string (link)' => [ @@ -162,7 +162,7 @@ public function test_base64_img_detected(string $content, bool $expectation): vo } public function test_truncate_base64(): void { - $truncated = tool::truncate_base64($this->base64img); + $truncated = tool::truncate_base64(self::$base64img); $this->assertStringContainsString('addElement('select', 'roleid', get_string('selectrole', 'tool_cohortroles'), $options); $mform->addRule('roleid', null, 'required'); - $context = context_system::instance(); $options = array( 'multiple' => true, - 'data-contextid' => $context->id, - 'data-includes' => 'all' + 'includes' => 'all', ); $mform->addElement('cohort', 'cohortids', get_string('selectcohorts', 'tool_cohortroles'), $options); $mform->addRule('cohortids', null, 'required'); diff --git a/admin/tool/customlang/tests/local/mlang/langstring_test.php b/admin/tool/customlang/tests/local/mlang/langstring_test.php index f3fca1072ce6f..17f72da2b1c74 100644 --- a/admin/tool/customlang/tests/local/mlang/langstring_test.php +++ b/admin/tool/customlang/tests/local/mlang/langstring_test.php @@ -60,7 +60,7 @@ public function test_fix_syntax(string $text, int $version, ?int $fromversion, s * * @return array */ - public function fix_syntax_data(): array { + public static function fix_syntax_data(): array { return [ // Syntax sanity v1 strings. [ diff --git a/admin/tool/customlang/tests/local/mlang/phpparser_test.php b/admin/tool/customlang/tests/local/mlang/phpparser_test.php index aca53c1b3af76..fe6e6bce28d6d 100644 --- a/admin/tool/customlang/tests/local/mlang/phpparser_test.php +++ b/admin/tool/customlang/tests/local/mlang/phpparser_test.php @@ -79,7 +79,7 @@ public function test_parse(string $phpcode, array $expected, bool $exception): v * * @return array */ - public function parse_provider(): array { + public static function parse_provider(): array { return [ 'Invalid PHP code' => [ 'No PHP code', [], false diff --git a/admin/tool/dataprivacy/tests/api_test.php b/admin/tool/dataprivacy/tests/api_test.php index ee44389e96744..d8e3ee7e77419 100644 --- a/admin/tool/dataprivacy/tests/api_test.php +++ b/admin/tool/dataprivacy/tests/api_test.php @@ -633,7 +633,7 @@ public function test_can_download_data_request_for_user(): void { * * @return array */ - public function data_request_creation_provider() { + public static function data_request_creation_provider(): array { return [ 'Export request by user, automatic approval off' => [ false, api::DATAREQUEST_TYPE_EXPORT, 'automaticdataexportapproval', false, 0, @@ -815,7 +815,7 @@ public function test_deny_data_request(): void { * * @return array */ - public function get_data_requests_provider() { + public static function get_data_requests_provider(): array { $completeonly = [api::DATAREQUEST_STATUS_COMPLETE, api::DATAREQUEST_STATUS_DOWNLOAD_READY, api::DATAREQUEST_STATUS_DELETED]; $completeandcancelled = array_merge($completeonly, [api::DATAREQUEST_STATUS_CANCELLED]); @@ -1000,7 +1000,7 @@ public function test_get_approved_contextlist_collection_for_request(): void { /** * Data provider for test_has_ongoing_request. */ - public function status_provider() { + public static function status_provider(): array { return [ [api::DATAREQUEST_STATUS_AWAITING_APPROVAL, true], [api::DATAREQUEST_STATUS_APPROVED, true], @@ -1090,7 +1090,7 @@ public function test_is_site_dpo(): void { * * @return array */ - public function notify_dpo_provider() { + public static function notify_dpo_provider(): array { return [ [false, api::DATAREQUEST_TYPE_EXPORT, 'requesttypeexport', 'Export my user data'], [false, api::DATAREQUEST_TYPE_DELETE, 'requesttypedelete', 'Delete my user data'], @@ -1442,7 +1442,7 @@ public function test_effective_contextlevel_invalid_contextlevels($contextlevel) /** * Data provider for invalid contextlevel fetchers. */ - public function invalid_effective_contextlevel_provider() { + public static function invalid_effective_contextlevel_provider(): array { return [ [CONTEXT_COURSECAT], [CONTEXT_COURSE], @@ -2002,7 +2002,7 @@ public function test_get_approved_contextlist_collection_for_collection_delete_c /** * Data provider for \tool_dataprivacy_api_testcase::test_set_context_defaults */ - public function set_context_defaults_provider() { + public static function set_context_defaults_provider(): array { $contextlevels = [ [CONTEXT_COURSECAT], [CONTEXT_COURSE], @@ -2477,7 +2477,7 @@ public function test_can_create_data_deletion_request_for_children(): void { * * @return array */ - public function queue_data_request_task_provider() { + public static function queue_data_request_task_provider(): array { return [ 'With user ID provided' => [true], 'Without user ID provided' => [false], @@ -2514,7 +2514,7 @@ public function test_queue_data_request_task(bool $withuserid): void { /** * Data provider for test_is_automatic_request_approval_on(). */ - public function automatic_request_approval_setting_provider() { + public static function automatic_request_approval_setting_provider(): array { return [ 'Data export, not set' => [ 'automaticdataexportapproval', api::DATAREQUEST_TYPE_EXPORT, null, false diff --git a/admin/tool/dataprivacy/tests/behat/dataexport.feature b/admin/tool/dataprivacy/tests/behat/dataexport.feature index 662ccae191b98..fb068e8a1f2dd 100644 --- a/admin/tool/dataprivacy/tests/behat/dataexport.feature +++ b/admin/tool/dataprivacy/tests/behat/dataexport.feature @@ -55,7 +55,8 @@ Feature: Data export from the privacy API And I reload the page And I should see "Download ready" in the "Victim User 1" "table_row" And I open the action menu in "Victim User 1" "table_row" - And following "Download" should download between "1" and "200000" bytes + And following "Download" should download a file that: + | Contains file in zip | index.html | And the following config values are set as admin: | privacyrequestexpiry | 1 | tool_dataprivacy | And I wait "1" seconds @@ -90,7 +91,8 @@ Feature: Data export from the privacy API And I reload the page And I should see "Download ready" in the "Export all of my personal data" "table_row" And I open the action menu in "Victim User 1" "table_row" - And following "Download" should download between "1" and "200000" bytes + And following "Download" should download a file that: + | Contains file in zip | index.html | And the following config values are set as admin: | privacyrequestexpiry | 1 | tool_dataprivacy | @@ -128,7 +130,8 @@ Feature: Data export from the privacy API And I reload the page And I should see "Download ready" in the "Victim User 1" "table_row" And I open the action menu in "Victim User 1" "table_row" - And following "Download" should download between "1" and "200000" bytes + And following "Download" should download a file that: + | Contains file in zip | index.html | And the following config values are set as admin: | privacyrequestexpiry | 1 | tool_dataprivacy | @@ -187,7 +190,8 @@ Feature: Data export from the privacy API And I reload the page And I should see "Download ready" in the "Victim User 1" "table_row" And I open the action menu in "Victim User 1" "table_row" - And following "Download" should download between "1" and "172000" bytes + And following "Download" should download a file that: + | Contains file in zip | index.html | And the following config values are set as admin: | privacyrequestexpiry | 1 | tool_dataprivacy | And I wait "1" seconds @@ -231,7 +235,8 @@ Feature: Data export from the privacy API And I reload the page And I should see "Download ready" in the "Victim User 1" "table_row" And I open the action menu in "Victim User 1" "table_row" - And following "Download" should download between "1" and "180000" bytes + And following "Download" should download a file that: + | Contains file in zip | index.html | @javascript Scenario: Filter before export data for a user and download it in the view request action @@ -264,4 +269,5 @@ Feature: Data export from the privacy API And I reload the page And I should see "Download ready" in the "Victim User 1" "table_row" And I open the action menu in "Victim User 1" "table_row" - And following "Download" should download between "1" and "180000" bytes + And following "Download" should download a file that: + | Contains file in zip | index.html | diff --git a/admin/tool/dataprivacy/tests/data_request_test.php b/admin/tool/dataprivacy/tests/data_request_test.php index ddd1127ba18fe..25ad30fbf0988 100644 --- a/admin/tool/dataprivacy/tests/data_request_test.php +++ b/admin/tool/dataprivacy/tests/data_request_test.php @@ -35,7 +35,7 @@ class data_request_test extends data_privacy_testcase { * * @return array */ - public function status_state_provider(): array { + public static function status_state_provider(): array { return [ [ 'state' => api::DATAREQUEST_STATUS_PENDING, @@ -138,9 +138,9 @@ public function test_can_reset_others($status): void { * * @return array */ - public function non_resettable_provider(): array { + public static function non_resettable_provider(): array { $states = []; - foreach ($this->status_state_provider() as $thisstatus) { + foreach (self::status_state_provider() as $thisstatus) { if (!$thisstatus['resettable']) { $states[] = $thisstatus; } diff --git a/admin/tool/dataprivacy/tests/expired_contexts_test.php b/admin/tool/dataprivacy/tests/expired_contexts_test.php index d851000ba6a0f..da9ecae7b9fee 100644 --- a/admin/tool/dataprivacy/tests/expired_contexts_test.php +++ b/admin/tool/dataprivacy/tests/expired_contexts_test.php @@ -1370,12 +1370,20 @@ public function test_process_user_context(): void { ]) ->getMock(); $mockprivacymanager->expects($this->atLeastOnce())->method('delete_data_for_user'); - $mockprivacymanager->expects($this->exactly(2)) + $deleteinvocations = $this->exactly(2); + $mockprivacymanager->expects($deleteinvocations) ->method('delete_data_for_all_users_in_context') - ->withConsecutive( - [$blockcontext], - [$usercontext] - ); + ->willReturnCallback(function ($context) use ( + $deleteinvocations, + $blockcontext, + $usercontext, + ): void { + match (self::getInvocationCount($deleteinvocations)) { + 1 => $this->assertEquals($blockcontext, $context), + 2 => $this->assertEquals($usercontext, $context), + default => $this->fail('Unexpected invocation count'), + }; + }); $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class) ->onlyMethods(['get_privacy_manager']) @@ -1588,12 +1596,20 @@ public function test_process_user_historic_block_unapproved(): void { ]) ->getMock(); $mockprivacymanager->expects($this->atLeastOnce())->method('delete_data_for_user'); - $mockprivacymanager->expects($this->exactly(2)) + $deleteinvocations = $this->exactly(2); + $mockprivacymanager->expects($deleteinvocations) ->method('delete_data_for_all_users_in_context') - ->withConsecutive( - [$blockcontext], - [$usercontext] - ); + ->willReturnCallback(function ($context) use ( + $deleteinvocations, + $blockcontext, + $usercontext, + ): void { + match (self::getInvocationCount($deleteinvocations)) { + 1 => $this->assertEquals($blockcontext, $context), + 2 => $this->assertEquals($usercontext, $context), + default => $this->fail('Unexpected invocation count'), + }; + }); $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class) ->onlyMethods(['get_privacy_manager']) @@ -1641,12 +1657,20 @@ public function test_process_user_historic_unexpired_child(): void { ]) ->getMock(); $mockprivacymanager->expects($this->atLeastOnce())->method('delete_data_for_user'); - $mockprivacymanager->expects($this->exactly(2)) + $deleteinvocations = $this->exactly(2); + $mockprivacymanager->expects($deleteinvocations) ->method('delete_data_for_all_users_in_context') - ->withConsecutive( - [$blockcontext], - [$usercontext] - ); + ->willReturnCallback(function ($context) use ( + $deleteinvocations, + $blockcontext, + $usercontext, + ): void { + match (self::getInvocationCount($deleteinvocations)) { + 1 => $this->assertEquals($blockcontext, $context), + 2 => $this->assertEquals($usercontext, $context), + default => $this->fail('Unexpected invocation count'), + }; + }); $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class) ->onlyMethods(['get_privacy_manager']) @@ -1860,12 +1884,20 @@ public function test_process_course_context_approved_children(): void { ]) ->getMock(); $mockprivacymanager->expects($this->never())->method('delete_data_for_user'); - $mockprivacymanager->expects($this->exactly(2)) + $deleteinvocations = $this->exactly(2); + $mockprivacymanager->expects($deleteinvocations) ->method('delete_data_for_all_users_in_context') - ->withConsecutive( - [$forumcontext], - [$coursecontext] - ); + ->willReturnCallback(function ($context) use ( + $deleteinvocations, + $forumcontext, + $coursecontext, + ): void { + match (self::getInvocationCount($deleteinvocations)) { + 1 => $this->assertEquals($forumcontext, $context), + 2 => $this->assertEquals($coursecontext, $context), + default => $this->fail('Unexpected invocation count'), + }; + }); $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class) ->onlyMethods(['get_privacy_manager']) @@ -1918,7 +1950,7 @@ public function test_can_process_deletion($status, $expected): void { * * @return array */ - public function can_process_deletion_provider(): array { + public static function can_process_deletion_provider(): array { return [ 'Pending' => [ expired_context::STATUS_EXPIRED, @@ -1956,7 +1988,7 @@ public function test_is_complete($status, $expected): void { * * @return array */ - public function is_complete_provider(): array { + public static function is_complete_provider(): array { return [ 'Pending' => [ expired_context::STATUS_EXPIRED, @@ -1991,7 +2023,7 @@ public function test_is_fully_expired($record, $expected): void { * * @return array */ - public function is_fully_expired_provider(): array { + public static function is_fully_expired_provider(): array { return [ 'Fully expired' => [ [ diff --git a/admin/tool/dataprivacy/tests/external/external_test.php b/admin/tool/dataprivacy/tests/external/external_test.php index c8be8c63ee5b4..20614042802d5 100644 --- a/admin/tool/dataprivacy/tests/external/external_test.php +++ b/admin/tool/dataprivacy/tests/external/external_test.php @@ -580,7 +580,7 @@ public function test_get_category_options_no_capability(): void { /** * Data provider for \tool_dataprivacy_external_testcase::test_XX_options(). */ - public function get_options_provider() { + public static function get_options_provider(): array { return [ [false, false], [false, true], @@ -705,7 +705,7 @@ public function test_get_purpose_options($includeinherit, $includenotset): void /** * Data provider for \tool_dataprivacy_external_testcase::get_activity_options(). */ - public function get_activity_options_provider() { + public static function get_activity_options_provider(): array { return [ [false, false, true], [false, true, true], diff --git a/admin/tool/dataprivacy/tests/filtered_userlist_test.php b/admin/tool/dataprivacy/tests/filtered_userlist_test.php index f54114cd9d953..67602fbdf7d6f 100644 --- a/admin/tool/dataprivacy/tests/filtered_userlist_test.php +++ b/admin/tool/dataprivacy/tests/filtered_userlist_test.php @@ -57,7 +57,7 @@ public function test_apply_expired_contexts_filters(array $initial, array $expir * * @return array */ - public function apply_expired_contexts_filters_provider(): array { + public static function apply_expired_contexts_filters_provider(): array { return [ // Entire list should be preserved. 'No overrides' => [ diff --git a/admin/tool/httpsreplace/tests/httpsreplace_test.php b/admin/tool/httpsreplace/tests/httpsreplace_test.php index bd4b0e4e72c0b..387aa8ab896c7 100644 --- a/admin/tool/httpsreplace/tests/httpsreplace_test.php +++ b/admin/tool/httpsreplace/tests/httpsreplace_test.php @@ -38,20 +38,20 @@ class httpsreplace_test extends \advanced_testcase { /** * Data provider for test_upgrade_http_links */ - public function upgrade_http_links_provider() { + public static function upgrade_http_links_provider(): array { global $CFG; // Get the http url, since the default test wwwroot is https. $wwwroothttp = preg_replace('/^https:/', 'http:', $CFG->wwwroot); return [ "Test image from another site should be replaced" => [ - "content" => '', + "content" => '', "outputregex" => '/UPDATE/', - "expectedcontent" => '', + "expectedcontent" => '', ], "Test object from another site should be replaced" => [ - "content" => '', + "content" => '', "outputregex" => '/UPDATE/', - "expectedcontent" => '', + "expectedcontent" => '', ], "Test image from a site with international name should be replaced" => [ "content" => '', @@ -79,9 +79,9 @@ public function upgrade_http_links_provider() { "expectedcontent" => '', ], "Search for params should be case insensitive" => [ - "content" => '', + "content" => '', "outputregex" => '/UPDATE/', - "expectedcontent" => '', + "expectedcontent" => '', ], "URL should be case insensitive" => [ "content" => '', @@ -89,30 +89,30 @@ public function upgrade_http_links_provider() { "expectedcontent" => '', ], "More params should not interfere" => [ - "content" => 'A picture 'A picture

', "outputregex" => '/UPDATE/', - "expectedcontent" => 'A picture 'A picture

', ], "Broken URL should not be changed" => [ - "content" => '', + "content" => '', "outputregex" => '/^$/', - "expectedcontent" => '', + "expectedcontent" => '', ], "Link URL should not be changed" => [ - "content" => '' . - $this->getExternalTestFileUrl('/test.png', false) . '', + "content" => '' . + self::getExternalTestFileUrl('/test.png', false) . '', "outputregex" => '/^$/', - "expectedcontent" => '' . - $this->getExternalTestFileUrl('/test.png', false) . '', + "expectedcontent" => '' . + self::getExternalTestFileUrl('/test.png', false) . '', ], "Test image from another site should be replaced but link should not" => [ - "content" => '', + "content" => '', "outputregex" => '/UPDATE/', - "expectedcontent" => '', + "expectedcontent" => '', ], ]; } @@ -127,8 +127,8 @@ public function upgrade_http_links_provider() { * @param string $path Path to be rewritten * @return string */ - protected function get_converted_http_link($path) { - return preg_replace('/^http:/', 'https:', $this->getExternalTestFileUrl($path, false)); + protected static function get_converted_http_link($path) { + return preg_replace('/^http:/', 'https:', self::getExternalTestFileUrl($path, false)); } /** @@ -160,15 +160,15 @@ public function test_upgrade_http_links($content, $ouputregex, $expectedcontent) /** * Data provider for test_http_link_stats */ - public function http_link_stats_provider() { + public static function http_link_stats_provider(): array { global $CFG; // Get the http url, since the default test wwwroot is https. $wwwrootdomain = 'www.example.com'; $wwwroothttp = preg_replace('/^https:/', 'http:', $CFG->wwwroot); - $testdomain = $this->get_converted_http_link(''); + $testdomain = self::get_converted_http_link(''); return [ "Test image from an available site so shouldn't be reported" => [ - "content" => '', + "content" => '', "domain" => $testdomain, "expectedcount" => 0, ], diff --git a/admin/tool/langimport/tests/controller_test.php b/admin/tool/langimport/tests/controller_test.php index 03b0ebb4700bd..008dd470dcaff 100644 --- a/admin/tool/langimport/tests/controller_test.php +++ b/admin/tool/langimport/tests/controller_test.php @@ -48,7 +48,7 @@ public function test_uninstall_lang_invalid(string $lang): void { * * @return array */ - public function uninstall_lang_invalid_provider(): array { + public static function uninstall_lang_invalid_provider(): array { return [ 'Empty string' => [''], 'Meaningless empty string' => [' '], diff --git a/admin/tool/langimport/tests/locale_test.php b/admin/tool/langimport/tests/locale_test.php index aab50f7edc3f3..694794233fa0d 100644 --- a/admin/tool/langimport/tests/locale_test.php +++ b/admin/tool/langimport/tests/locale_test.php @@ -55,8 +55,14 @@ public function test_check_locale_availability(): void { 'set_locale', ]) ->getMock(); - $mock->method('get_locale')->will($this->onConsecutiveCalls('en')); - $mock->method('set_locale')->will($this->onConsecutiveCalls('es', 'en')); + $mock->method('get_locale')->will($this->returnValue('en')); + $setinvocations = $this->exactly(2); + $mock + ->expects($setinvocations) + ->method('set_locale')->willReturnCallback(fn () => match (self::getInvocationCount($setinvocations)) { + 1 => 'es', + 2 => 'en', + }); // Test what happen when locale is available on system. $result = $mock->check_locale_availability('en'); @@ -72,8 +78,14 @@ public function test_check_locale_availability(): void { 'set_locale', ]) ->getMock(); - $mock->method('get_locale')->will($this->onConsecutiveCalls('en')); - $mock->method('set_locale')->will($this->onConsecutiveCalls(false, 'en')); + $mock->expects($this->exactly(1))->method('get_locale')->will($this->returnValue('en')); + $setinvocations = $this->exactly(2); + $mock + ->expects($setinvocations) + ->method('set_locale')->willReturnCallback(fn () => match (self::getInvocationCount($setinvocations)) { + 1 => false, + 2 => 'en', + }); // Test what happen when locale is not available on system. $result = $mock->check_locale_availability('en'); diff --git a/admin/tool/log/store/standard/tests/store_test.php b/admin/tool/log/store/standard/tests/store_test.php index a02e2d9d590a7..06beafb03b772 100644 --- a/admin/tool/log/store/standard/tests/store_test.php +++ b/admin/tool/log/store/standard/tests/store_test.php @@ -389,7 +389,7 @@ public function test_decode_other_with_wrongly_encoded_contents(): void { * * @return array Array of parameters */ - public function decode_other_provider(): array { + public static function decode_other_provider(): array { return [ [['info' => 'd2819896', 'logurl' => 'discuss.php?d=2819896']], [null], diff --git a/admin/tool/lp/classes/external.php b/admin/tool/lp/classes/external.php index f5585d19ee2d9..1fc1c928da2d8 100644 --- a/admin/tool/lp/classes/external.php +++ b/admin/tool/lp/classes/external.php @@ -864,6 +864,8 @@ public static function search_users($query, $capability = '', $limitfrom = 0, $l $context = context_system::instance(); self::validate_context($context); + require_capability('moodle/competency:templatemanage', $context); + $output = $PAGE->get_renderer('tool_lp'); list($filtercapsql, $filtercapparams) = api::filter_users_with_capability_on_user_context_sql($cap, diff --git a/admin/tool/lp/db/services.php b/admin/tool/lp/db/services.php index 7c66a389708c5..8fe082d0791ee 100644 --- a/admin/tool/lp/db/services.php +++ b/admin/tool/lp/db/services.php @@ -128,7 +128,7 @@ 'classpath' => '', 'description' => 'Search for users.', 'type' => 'read', - 'capabilities' => '', + 'capabilities' => 'moodle/competency:templatemanage', 'ajax' => true, ), // This function was originally in this plugin but has been moved to core. diff --git a/admin/tool/lp/tests/externallib_test.php b/admin/tool/lp/tests/externallib_test.php index f2b779a5f0e4c..ddd87ae7985df 100644 --- a/admin/tool/lp/tests/externallib_test.php +++ b/admin/tool/lp/tests/externallib_test.php @@ -141,17 +141,14 @@ public function test_search_users_by_capability(): void { 'email' => 'bobbyyy@dyyylan.com', 'phone1' => '123456', 'phone2' => '78910', 'department' => 'Marketing', 'institution' => 'HQ')); - // First we search with no capability assigned. + // Assign capability required to perform the search. $this->setUser($ux); - $result = external::search_users('yyylan', 'moodle/competency:planmanage'); - $result = external_api::clean_returnvalue(external::search_users_returns(), $result); - $this->assertCount(0, $result['users']); - $this->assertEquals(0, $result['count']); + $systemcontext = \context_system::instance(); + $customrole = $this->assignUserCapability('moodle/competency:templatemanage', $systemcontext->id); // Now we assign a different capability. $usercontext = \context_user::instance($u1->id); - $systemcontext = \context_system::instance(); - $customrole = $this->assignUserCapability('moodle/competency:planview', $usercontext->id); + $this->assignUserCapability('moodle/competency:templatemanage', $usercontext->id, $customrole); $result = external::search_users('yyylan', 'moodle/competency:planmanage'); $result = external_api::clean_returnvalue(external::search_users_returns(), $result); @@ -187,6 +184,8 @@ public function test_search_users_by_capability(): void { $ux3 = $dg->create_user(); role_assign($this->creatorrole, $ux3->id, $usercontext->id); $this->setUser($ux3); + $systemcontext = \context_system::instance(); + $customrole = $this->assignUserCapability('moodle/competency:templatemanage', $systemcontext->id, $customrole); $result = external::search_users('yyylan', 'moodle/competency:planmanage'); $result = external_api::clean_returnvalue(external::search_users_returns(), $result); $this->assertCount(1, $result['users']); @@ -262,6 +261,7 @@ public function test_search_users_by_capability_the_comeback(): void { // Now do the test. $this->setUser($master); + $dummyrole = $this->assignUserCapability('moodle/competency:templatemanage', $syscontext->id); $result = external::search_users('MOODLER', 'moodle/site:config'); $this->assertCount(2, $result['users']); $this->assertEquals(2, $result['count']); @@ -269,6 +269,7 @@ public function test_search_users_by_capability_the_comeback(): void { $this->assertArrayHasKey($slave3->id, $result['users']); $this->setUser($manager); + $this->assignUserCapability('moodle/competency:templatemanage', $syscontext->id, $dummyrole); $result = external::search_users('MOODLER', 'moodle/site:config'); $this->assertCount(1, $result['users']); $this->assertEquals(1, $result['count']); @@ -377,6 +378,8 @@ public function test_search_users(): void { // Switch to a user that cannot view identity fields. $this->setUser($ux); + $systemcontext = \context_system::instance(); + $this->assignUserCapability('moodle/competency:templatemanage', $systemcontext->id, $dummyrole); $CFG->showuseridentity = 'idnumber,email,phone1,phone2,department,institution'; // Only names are included. diff --git a/admin/tool/lp/version.php b/admin/tool/lp/version.php index edc453bad07e2..96c0fa406a917 100644 --- a/admin/tool/lp/version.php +++ b/admin/tool/lp/version.php @@ -25,6 +25,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX). +$plugin->version = 2024042201; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2024041600; // Requires this Moodle version. $plugin->component = 'tool_lp'; // Full name of the plugin (used for diagnostics). diff --git a/admin/tool/lpimportcsv/tests/import_test.php b/admin/tool/lpimportcsv/tests/import_test.php index fafbef13e15c8..cc6ef990a6cfb 100644 --- a/admin/tool/lpimportcsv/tests/import_test.php +++ b/admin/tool/lpimportcsv/tests/import_test.php @@ -31,7 +31,9 @@ public function test_import_framework(): void { $this->resetAfterTest(true); $this->setAdminUser(); - $importer = new framework_importer(file_get_contents(__DIR__ . '/fixtures/example.csv')); + $importer = new framework_importer( + file_get_contents(self::get_fixture_path(__NAMESPACE__, 'example.csv')), + ); $this->assertEquals('', $importer->get_error()); diff --git a/admin/tool/mfa/amd/build/autosubmit_verification_code.min.js b/admin/tool/mfa/amd/build/autosubmit_verification_code.min.js index a52bce52c29d1..578bec532ee75 100644 --- a/admin/tool/mfa/amd/build/autosubmit_verification_code.min.js +++ b/admin/tool/mfa/amd/build/autosubmit_verification_code.min.js @@ -1,3 +1,3 @@ -define("tool_mfa/autosubmit_verification_code",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;_exports.init=()=>{const codeInput=document.querySelector("#id_verificationcode"),codeForm=codeInput.closest("form"),submitButton=codeForm.querySelector("#id_submitbutton");codeInput.addEventListener("keyup",(function(){this.value.length>=6&&codeForm.submit()})),codeInput.disabled&&(submitButton.disabled=!0)}})); +define("tool_mfa/autosubmit_verification_code",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;_exports.init=()=>{const codeInput=document.querySelector("#id_verificationcode"),codeForm=codeInput.closest("form"),submitButton=codeForm.querySelector("#id_submitbutton");codeInput.addEventListener("input",(function(){this.value.length>=6&&(codeForm.submit(),codeInput.readOnly=!0,submitButton.disabled=!0)})),codeInput.disabled&&(submitButton.disabled=!0)}})); //# sourceMappingURL=autosubmit_verification_code.min.js.map \ No newline at end of file diff --git a/admin/tool/mfa/amd/build/autosubmit_verification_code.min.js.map b/admin/tool/mfa/amd/build/autosubmit_verification_code.min.js.map index 6e69dcc631885..720e359f6abc1 100644 --- a/admin/tool/mfa/amd/build/autosubmit_verification_code.min.js.map +++ b/admin/tool/mfa/amd/build/autosubmit_verification_code.min.js.map @@ -1 +1 @@ -{"version":3,"file":"autosubmit_verification_code.min.js","sources":["../src/autosubmit_verification_code.js"],"sourcesContent":["\n// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module to autosubmit the verification code element when it reaches 6 characters.\n *\n * @module tool_mfa/autosubmit_verification_code\n * @copyright 2020 Peter Burnett \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport const init = () => {\n const codeInput = document.querySelector(\"#id_verificationcode\");\n const codeForm = codeInput.closest(\"form\");\n const submitButton = codeForm.querySelector(\"#id_submitbutton\");\n\n // Event listener for code input field.\n codeInput.addEventListener('keyup', function() {\n if (this.value.length >= 6) {\n // Submits the closes form (parent).\n codeForm.submit();\n }\n });\n\n // Disable the submit button if the input field is disabled.\n // This occurs if there are no more attempts left for the factor.\n if (codeInput.disabled) {\n submitButton.disabled = true;\n }\n};\n"],"names":["codeInput","document","querySelector","codeForm","closest","submitButton","addEventListener","this","value","length","submit","disabled"],"mappings":"0KAwBoB,WACVA,UAAYC,SAASC,cAAc,wBACnCC,SAAWH,UAAUI,QAAQ,QAC7BC,aAAeF,SAASD,cAAc,oBAG5CF,UAAUM,iBAAiB,SAAS,WAC5BC,KAAKC,MAAMC,QAAU,GAErBN,SAASO,YAMbV,UAAUW,WACVN,aAAaM,UAAW"} \ No newline at end of file +{"version":3,"file":"autosubmit_verification_code.min.js","sources":["../src/autosubmit_verification_code.js"],"sourcesContent":["\n// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module to autosubmit the verification code element when it reaches 6 characters.\n *\n * @module tool_mfa/autosubmit_verification_code\n * @copyright 2020 Peter Burnett \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport const init = () => {\n const codeInput = document.querySelector(\"#id_verificationcode\");\n const codeForm = codeInput.closest(\"form\");\n const submitButton = codeForm.querySelector(\"#id_submitbutton\");\n\n // Event listener for code input field.\n codeInput.addEventListener('input', function() {\n if (this.value.length >= 6) {\n // Submits the closest form (parent) and prevents accidental resubmission.\n codeForm.submit();\n codeInput.readOnly = true;\n submitButton.disabled = true;\n }\n });\n\n // Disable the submit button if the input field is disabled.\n // This occurs if there are no more attempts left for the factor.\n if (codeInput.disabled) {\n submitButton.disabled = true;\n }\n};\n"],"names":["codeInput","document","querySelector","codeForm","closest","submitButton","addEventListener","this","value","length","submit","readOnly","disabled"],"mappings":"0KAwBoB,WACVA,UAAYC,SAASC,cAAc,wBACnCC,SAAWH,UAAUI,QAAQ,QAC7BC,aAAeF,SAASD,cAAc,oBAG5CF,UAAUM,iBAAiB,SAAS,WAC5BC,KAAKC,MAAMC,QAAU,IAErBN,SAASO,SACTV,UAAUW,UAAW,EACrBN,aAAaO,UAAW,MAM5BZ,UAAUY,WACVP,aAAaO,UAAW"} \ No newline at end of file diff --git a/admin/tool/mfa/amd/src/autosubmit_verification_code.js b/admin/tool/mfa/amd/src/autosubmit_verification_code.js index 6b29304976cde..c0577406751dd 100644 --- a/admin/tool/mfa/amd/src/autosubmit_verification_code.js +++ b/admin/tool/mfa/amd/src/autosubmit_verification_code.js @@ -28,10 +28,12 @@ export const init = () => { const submitButton = codeForm.querySelector("#id_submitbutton"); // Event listener for code input field. - codeInput.addEventListener('keyup', function() { + codeInput.addEventListener('input', function() { if (this.value.length >= 6) { - // Submits the closes form (parent). + // Submits the closest form (parent) and prevents accidental resubmission. codeForm.submit(); + codeInput.readOnly = true; + submitButton.disabled = true; } }); diff --git a/admin/tool/mfa/factor/sms/tests/factor_test.php b/admin/tool/mfa/factor/sms/tests/factor_test.php index 49e3e9a331390..4d98eb90e1ee7 100644 --- a/admin/tool/mfa/factor/sms/tests/factor_test.php +++ b/admin/tool/mfa/factor/sms/tests/factor_test.php @@ -32,7 +32,7 @@ class factor_test extends \advanced_testcase { * * @return array of different country codes and phone numbers. */ - public function format_number_provider(): array { + public static function format_number_provider(): array { return [ 'Phone number with local format' => [ @@ -88,7 +88,7 @@ public function test_format_number(string $phonenumber, string $expected, ?strin * * @return array with different phone numebr tests */ - public function is_valid_phonenumber_provider(): array { + public static function is_valid_phonenumber_provider(): array { return [ ['+919367788755', true], ['8989829304', false], diff --git a/admin/tool/mfa/factor/token/tests/factor_test.php b/admin/tool/mfa/factor/token/tests/factor_test.php index 4b8e3c43afd88..ad394866713a4 100644 --- a/admin/tool/mfa/factor/token/tests/factor_test.php +++ b/admin/tool/mfa/factor/token/tests/factor_test.php @@ -275,7 +275,7 @@ public function test_calculate_expiry_time_for_overnight_expiry_with_an_hour_exp * Increments by 30 minutes to cover half hour and hour cases. * Starting timestamp: 2022-01-15 07:30:00 Australia/Melbourne time. */ - public function timestamp_provider() { + public static function timestamp_provider(): array { $starttimestamp = 1642192200; foreach (range(0, 23) as $i) { $timestamps[] = [$starttimestamp + ($i * HOURSECS)]; diff --git a/admin/tool/mfa/tests/admin_setting_managemfa_test.php b/admin/tool/mfa/tests/admin_setting_managemfa_test.php index fcfd43a2cf034..1a37b8384abb4 100644 --- a/admin/tool/mfa/tests/admin_setting_managemfa_test.php +++ b/admin/tool/mfa/tests/admin_setting_managemfa_test.php @@ -51,7 +51,7 @@ public function test_get_factor_combinations_default(): void { * * @return array */ - public function get_factor_combinations_provider() { + public static function get_factor_combinations_provider(): array { $provider = []; $factors = []; diff --git a/admin/tool/mfa/tests/manager_test.php b/admin/tool/mfa/tests/manager_test.php index 8eace2573b2d4..64d7fcb97d70d 100644 --- a/admin/tool/mfa/tests/manager_test.php +++ b/admin/tool/mfa/tests/manager_test.php @@ -157,7 +157,7 @@ public function test_passed_enough_factors(): void { * * @return array */ - public static function should_redirect_urls_provider() { + public static function should_redirect_urls_provider(): array { $badurl1 = new \moodle_url('/'); $badparam1 = $badurl1->out(); $badurl2 = new \moodle_url('admin/tool/mfa/auth.php'); diff --git a/admin/tool/mobile/classes/hook_callbacks.php b/admin/tool/mobile/classes/hook_callbacks.php index 66b33516040cf..fb73a5ab142cc 100644 --- a/admin/tool/mobile/classes/hook_callbacks.php +++ b/admin/tool/mobile/classes/hook_callbacks.php @@ -76,7 +76,9 @@ public static function before_standard_footer_html_generation( return; } $hook->add_html( - html_writer::link($url, get_string('getmoodleonyourmobile', 'tool_mobile'), ['class' => 'mobilelink']), + html_writer::div( + html_writer::link($url, get_string('getmoodleonyourmobile', 'tool_mobile'), ['class' => 'mobilelink']), + ), ); } diff --git a/admin/tool/moodlenet/tests/lib_test.php b/admin/tool/moodlenet/tests/lib_test.php index 85dd581871466..cd02734fb391d 100644 --- a/admin/tool/moodlenet/tests/lib_test.php +++ b/admin/tool/moodlenet/tests/lib_test.php @@ -52,7 +52,7 @@ public function test_generate_mnet_endpoint($profileurl, $course, $section, $exp * * @return array */ - public function get_endpoints_provider() { + public static function get_endpoints_provider(): array { global $CFG; return [ [ diff --git a/admin/tool/moodlenet/tests/local/import_handler_info_test.php b/admin/tool/moodlenet/tests/local/import_handler_info_test.php index df6d9b59f9e6f..7f1cdbb2ac599 100644 --- a/admin/tool/moodlenet/tests/local/import_handler_info_test.php +++ b/admin/tool/moodlenet/tests/local/import_handler_info_test.php @@ -59,7 +59,7 @@ public function test_initialisation($modname, $description, $expectexception): v * * @return array the data for creation of the info object. */ - public function handler_info_data_provider() { + public static function handler_info_data_provider(): array { return [ 'All data present' => ['label', 'Add a label to the course', false], 'Empty module name' => ['', 'Add a file resource to the course', true], diff --git a/admin/tool/moodlenet/tests/local/remote_resource_test.php b/admin/tool/moodlenet/tests/local/remote_resource_test.php index 53b30bfe0f897..219feed6fecd5 100644 --- a/admin/tool/moodlenet/tests/local/remote_resource_test.php +++ b/admin/tool/moodlenet/tests/local/remote_resource_test.php @@ -53,10 +53,10 @@ public function test_getters($url, $metadata, $expectedextension): void { * * @return array */ - public function remote_resource_data_provider() { + public static function remote_resource_data_provider(): array { return [ 'With filename and extension' => [ - $this->getExternalTestFileUrl('/test.html'), + self::getExternalTestFileUrl('/test.html'), (object) [ 'name' => 'Test html file', 'description' => 'Full description of the html file' @@ -78,8 +78,8 @@ public function remote_resource_data_provider() { * Test confirming the network based operations of a remote_resource. */ public function test_network_features(): void { - $url = $this->getExternalTestFileUrl('/test.html'); - $nonexistenturl = $this->getExternalTestFileUrl('/test.htmlzz'); + $url = self::getExternalTestFileUrl('/test.html'); + $nonexistenturl = self::getExternalTestFileUrl('/test.htmlzz'); $remoteres = new remote_resource( new \curl(), diff --git a/admin/tool/moodlenet/tests/local/url_test.php b/admin/tool/moodlenet/tests/local/url_test.php index a948732c79fda..ade2f0c6e82bb 100644 --- a/admin/tool/moodlenet/tests/local/url_test.php +++ b/admin/tool/moodlenet/tests/local/url_test.php @@ -55,7 +55,7 @@ public function test_parsing($urlstring, $host, $path, $exception): void { * * @return array */ - public function url_provider() { + public static function url_provider(): array { return [ 'No path' => [ 'url' => 'https://example.moodle.net', diff --git a/admin/tool/policy/classes/hook_callbacks.php b/admin/tool/policy/classes/hook_callbacks.php index fa2a95346a04a..71191ca4eaf7b 100644 --- a/admin/tool/policy/classes/hook_callbacks.php +++ b/admin/tool/policy/classes/hook_callbacks.php @@ -79,7 +79,10 @@ public static function before_standard_footer_html_generation(before_standard_fo if (!empty($policies)) { $url = new moodle_url('/admin/tool/policy/viewall.php', ['returnurl' => $PAGE->url]); $hook->add_html( - html_writer::link($url, get_string('userpolicysettings', 'tool_policy'), ['class' => 'policiesfooter']), + html_writer::div( + html_writer::link($url, get_string('userpolicysettings', 'tool_policy')), + 'policiesfooter', + ), ); } } diff --git a/admin/tool/policy/templates/guestconsent.mustache b/admin/tool/policy/templates/guestconsent.mustache index 89640b337f412..1c72c99aa5017 100644 --- a/admin/tool/policy/templates/guestconsent.mustache +++ b/admin/tool/policy/templates/guestconsent.mustache @@ -61,7 +61,7 @@
    {{#policies}}
  • - {{{name}}} diff --git a/admin/tool/recyclebin/classes/category_bin.php b/admin/tool/recyclebin/classes/category_bin.php index 3c18f62d5288b..18b4d8eb2c66a 100644 --- a/admin/tool/recyclebin/classes/category_bin.php +++ b/admin/tool/recyclebin/classes/category_bin.php @@ -117,7 +117,7 @@ public function store_item($course) { // This hack will be removed once recycle bin switches to use its own backup mode, with // own preferences and 100% separate from MOODLE_AUTOMATED. // TODO: Remove this as part of MDL-65228. - $forcedbackupsettings = $CFG->forced_plugin_settings['backup'] ?? null; + $forcedbackupsettings = $CFG->forced_plugin_settings['backup'] ?? []; $CFG->forced_plugin_settings['backup']['backup_auto_storage'] = 0; $CFG->forced_plugin_settings['backup']['backup_auto_files'] = 1; @@ -253,7 +253,7 @@ public function restore_item($item) { // This hack will be removed once recycle bin switches to use its own backup mode, with // own preferences and 100% separate from MOODLE_AUTOMATED. // TODO: Remove this as part of MDL-65228. - $forcedrestoresettings = $CFG->forced_plugin_settings['restore'] ?? null; + $forcedrestoresettings = $CFG->forced_plugin_settings['restore'] ?? []; $CFG->forced_plugin_settings['restore']['restore_general_users'] = 1; $CFG->forced_plugin_settings['restore']['restore_general_groups'] = 1; diff --git a/admin/tool/recyclebin/classes/course_bin.php b/admin/tool/recyclebin/classes/course_bin.php index bfb19b4790cc0..069fef2f396d4 100644 --- a/admin/tool/recyclebin/classes/course_bin.php +++ b/admin/tool/recyclebin/classes/course_bin.php @@ -121,7 +121,7 @@ public function store_item($cm) { // This hack will be removed once recycle bin switches to use its own backup mode, with // own preferences and 100% separate from MOODLE_AUTOMATED. // TODO: Remove this as part of MDL-65228. - $forcedbackupsettings = $CFG->forced_plugin_settings['backup'] ?? null; + $forcedbackupsettings = $CFG->forced_plugin_settings['backup'] ?? []; $CFG->forced_plugin_settings['backup']['backup_auto_storage'] = 0; $CFG->forced_plugin_settings['backup']['backup_auto_files'] = 1; diff --git a/admin/tool/recyclebin/tests/category_bin_test.php b/admin/tool/recyclebin/tests/category_bin_test.php index 953a7019912d3..c4e2cd001cd1e 100644 --- a/admin/tool/recyclebin/tests/category_bin_test.php +++ b/admin/tool/recyclebin/tests/category_bin_test.php @@ -175,7 +175,7 @@ public function test_cleanup_task(): void { * Used to verify that recycle bin is immune to various settings. Provides plugin, name, value for * direct usage with set_config() */ - public function recycle_bin_settings_provider() { + public static function recycle_bin_settings_provider(): array { return [ 'backup/backup_auto_storage moodle' => [[ (object)['plugin' => 'backup', 'name' => 'backup_auto_storage', 'value' => 0], diff --git a/admin/tool/recyclebin/tests/course_bin_test.php b/admin/tool/recyclebin/tests/course_bin_test.php index c37a619ac590d..f594aa78151ee 100644 --- a/admin/tool/recyclebin/tests/course_bin_test.php +++ b/admin/tool/recyclebin/tests/course_bin_test.php @@ -175,7 +175,7 @@ public function test_cleanup_task(): void { * Used to verify that recycle bin is immune to various settings. Provides plugin, name, value for * direct usage with set_config() */ - public function recycle_bin_settings_provider() { + public static function recycle_bin_settings_provider(): array { return [ 'backup/backup_auto_storage moodle' => [[ (object)['plugin' => 'backup', 'name' => 'backup_auto_storage', 'value' => 0], diff --git a/admin/tool/uploadcourse/tests/course_test.php b/admin/tool/uploadcourse/tests/course_test.php index 70cdc183edfc0..e5fc520504ed4 100644 --- a/admin/tool/uploadcourse/tests/course_test.php +++ b/admin/tool/uploadcourse/tests/course_test.php @@ -967,8 +967,14 @@ public function test_restore_file(): void { // Restore from a file, checking that the file takes priority over the templatecourse. $mode = tool_uploadcourse_processor::MODE_CREATE_NEW; $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; - $data = array('shortname' => 'A1', 'backupfile' => __DIR__ . '/fixtures/backup.mbz', - 'summary' => 'A', 'category' => 1, 'fullname' => 'A1', 'templatecourse' => $c1->shortname); + $data = [ + 'shortname' => 'A1', + 'backupfile' => self::get_fixture_path(__NAMESPACE__, 'backup.mbz'), + 'summary' => 'A', + 'category' => 1, + 'fullname' => 'A1', + 'templatecourse' => $c1->shortname, + ]; $co = new tool_uploadcourse_course($mode, $updatemode, $data); $this->assertTrue($co->prepare()); $co->proceed(); @@ -986,8 +992,13 @@ public function test_restore_file(): void { $this->assertTrue($found); // Restoring twice from the same file should work. - $data = array('shortname' => 'B1', 'backupfile' => __DIR__ . '/fixtures/backup.mbz', - 'summary' => 'B', 'category' => 1, 'fullname' => 'B1'); + $data = [ + 'shortname' => 'B1', + 'backupfile' => self::get_fixture_path(__NAMESPACE__, 'backup.mbz'), + 'summary' => 'B', + 'category' => 1, + 'fullname' => 'B1', + ]; $co = new tool_uploadcourse_course($mode, $updatemode, $data); $this->assertTrue($co->prepare()); $co->proceed(); @@ -1020,8 +1031,14 @@ public function test_restore_file_settings(): void { $mode = tool_uploadcourse_processor::MODE_CREATE_NEW; $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; - $data = array('shortname' => 'A1', 'backupfile' => __DIR__ . '/fixtures/backup.mbz', - 'summary' => 'A', 'category' => 1, 'fullname' => 'A1', 'templatecourse' => $c1->shortname); + $data = [ + 'shortname' => 'A1', + 'backupfile' => self::get_fixture_path(__NAMESPACE__, 'backup.mbz'), + 'summary' => 'A', + 'category' => 1, + 'fullname' => 'A1', + 'templatecourse' => $c1->shortname, + ]; $co = new tool_uploadcourse_course($mode, $updatemode, $data); $this->assertTrue($co->prepare()); $co->proceed(); @@ -1259,7 +1276,7 @@ public function test_enrolment_data(): void { * * @return array */ - public function enrolment_uploaddata_error_provider(): array { + public static function enrolment_uploaddata_error_provider(): array { return [ ['errorcannotcreateorupdateenrolment', [ 'shortname' => 'C1', @@ -1487,7 +1504,7 @@ public function test_custom_fields_data_invalid_date(): void { // Create our custom field. $category = $this->get_customfield_generator()->create_category(); $this->create_custom_field($category, 'date', 'mydate', - ['mindate' => strtotime('2020-04-01'), 'maxdate' => '2020-04-30']); + ['mindate' => strtotime('2020-04-01'), 'maxdate' => strtotime('2020-04-30')]); $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY; $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; diff --git a/admin/tool/uploadcourse/tests/processor_test.php b/admin/tool/uploadcourse/tests/processor_test.php index 9ad956d211470..2b4bcb540e571 100644 --- a/admin/tool/uploadcourse/tests/processor_test.php +++ b/admin/tool/uploadcourse/tests/processor_test.php @@ -110,11 +110,11 @@ public function test_restore_restore_file(): void { $cir->load_csv_content($content, 'utf-8', 'comma'); $cir->init(); - $options = array( + $options = [ 'mode' => tool_uploadcourse_processor::MODE_CREATE_NEW, - 'restorefile' => __DIR__ . '/fixtures/backup.mbz', - 'templatecourse' => 'DoesNotExist' // Restorefile takes priority. - ); + 'restorefile' => self::get_fixture_path(__NAMESPACE__, 'backup.mbz'), + 'templatecourse' => 'DoesNotExist', // Restorefile takes priority. + ]; $defaults = array('category' => '1'); $p = new tool_uploadcourse_processor($cir, $options, $defaults); diff --git a/admin/tool/uploaduser/tests/field_value_validators_test.php b/admin/tool/uploaduser/tests/field_value_validators_test.php index 5d4a2f8fb7ab3..995926d97c743 100644 --- a/admin/tool/uploaduser/tests/field_value_validators_test.php +++ b/admin/tool/uploaduser/tests/field_value_validators_test.php @@ -30,7 +30,7 @@ class field_value_validators_test extends \advanced_testcase { /** * Data provider for \field_value_validators_testcase::test_validate_theme(). */ - public function themes_provider() { + public static function themes_provider(): array { return [ 'User themes disabled' => [ false, 'boost', 'warning', get_string('userthemesnotallowed', 'tool_uploaduser') diff --git a/admin/tool/usertours/classes/step.php b/admin/tool/usertours/classes/step.php index 485bc081536e7..b59c8f9384d05 100644 --- a/admin/tool/usertours/classes/step.php +++ b/admin/tool/usertours/classes/step.php @@ -562,7 +562,8 @@ protected function calculate_sortorder() { * @return $this */ public function persist($force = false) { - global $DB; + global $CFG, $DB; + require_once("{$CFG->libdir}/filelib.php"); if (!$this->dirty && !$force) { return $this; @@ -719,6 +720,9 @@ public function add_config_field_to_form(\MoodleQuickForm $mform, $key) { * @return object */ public function prepare_data_for_form() { + global $CFG; + require_once("{$CFG->libdir}/filelib.php"); + $data = $this->to_record(); foreach (self::get_config_keys() as $key) { $data->$key = $this->get_config($key, configuration::get_step_default_value($key)); diff --git a/admin/tool/usertours/tests/helper_test.php b/admin/tool/usertours/tests/helper_test.php index 2b118af6899fd..ddc7db3c78a29 100644 --- a/admin/tool/usertours/tests/helper_test.php +++ b/admin/tool/usertours/tests/helper_test.php @@ -106,7 +106,7 @@ public function test_get_invalid_server_filter(): void { \core\di::set( \core\hook\manager::class, \core\hook\manager::phpunit_get_instance([ - 'test_plugin1' => __DIR__ . '/fixtures/invalid_serverside_hook_fixture.php', + 'test_plugin1' => self::get_fixture_path(__NAMESPACE__, 'invalid_serverside_hook_fixture.php'), ]), ); @@ -118,7 +118,7 @@ public function test_clientside_filter_for_serverside_hook(): void { \core\di::set( \core\hook\manager::class, \core\hook\manager::phpunit_get_instance([ - 'test_plugin1' => __DIR__ . '/fixtures/clientside_filter_for_serverside_hook.php', + 'test_plugin1' => self::get_fixture_path(__NAMESPACE__, 'clientside_filter_for_serverside_hook.php'), ]), ); @@ -130,7 +130,7 @@ public function test_serverside_filter_for_clientside_hook(): void { \core\di::set( \core\hook\manager::class, \core\hook\manager::phpunit_get_instance([ - 'test_plugin1' => __DIR__ . '/fixtures/serverside_filter_for_clientside_hook.php', + 'test_plugin1' => self::get_fixture_path(__NAMESPACE__, 'serverside_filter_for_clientside_hook.php'), ]), ); @@ -142,7 +142,7 @@ public function test_filter_hooks(): void { \core\di::set( \core\hook\manager::class, \core\hook\manager::phpunit_get_instance([ - 'test_plugin1' => __DIR__ . '/fixtures/hook_fixtures.php', + 'test_plugin1' => self::get_fixture_path(__NAMESPACE__, 'hook_fixtures.php'), ]), ); @@ -175,7 +175,7 @@ public function test_get_clientside_filter_module_names(): void { \core\di::set( \core\hook\manager::class, \core\hook\manager::phpunit_get_instance([ - 'test_plugin1' => __DIR__ . '/fixtures/invalid_clientside_hook_fixture.php', + 'test_plugin1' => self::get_fixture_path(__NAMESPACE__, 'invalid_clientside_hook_fixture.php'), ]), ); diff --git a/admin/tool/usertours/tests/tour_test.php b/admin/tool/usertours/tests/tour_test.php index e32df99a1f815..f2ebcb08b92f9 100644 --- a/admin/tool/usertours/tests/tour_test.php +++ b/admin/tool/usertours/tests/tour_test.php @@ -524,14 +524,30 @@ public function test_remove_persisted(): void { // Mock the database. $DB = $this->mock_database(); - $DB->expects($this->exactly(3)) + $deleteinvocations = $this->exactly(3); + $DB->expects($deleteinvocations) ->method('delete_records') - ->withConsecutive( - [$this->equalTo('tool_usertours_tours'), $this->equalTo(['id' => $id])], - [$this->equalTo('user_preferences'), $this->equalTo(['name' => tour::TOUR_LAST_COMPLETED_BY_USER . $id])], - [$this->equalTo('user_preferences'), $this->equalTo(['name' => tour::TOUR_REQUESTED_BY_USER . $id])] - ) - ->willReturn(null); + ->willReturnCallback(function ($table, $conditions) use ($deleteinvocations, $id) { + switch (self::getInvocationCount($deleteinvocations)) { + case 1: + $this->assertEquals('tool_usertours_tours', $table); + $this->assertEquals(['id' => $id], $conditions); + return null; + break; + case 2: + $this->assertEquals('user_preferences', $table); + $this->assertEquals(['name' => tour::TOUR_LAST_COMPLETED_BY_USER . $id], $conditions); + return null; + break; + case 3: + $this->assertEquals('user_preferences', $table); + $this->assertEquals(['name' => tour::TOUR_REQUESTED_BY_USER . $id], $conditions); + return null; + break; + default: + $this->fail('Unexpected call to delete_records'); + } + }); $DB->expects($this->once()) ->method('get_records') @@ -551,7 +567,7 @@ public function test_reset_step_sortorder(): void { for ($i = 4; $i >= 0; $i--) { $id = rand($i * 10, ($i * 10) + 9); $mockdata[] = (object) ['id' => $id]; - $expectations[] = [$this->equalTo('tool_usertours_steps'), $this->equalTo('sortorder'), 4 - $i, ['id' => $id]]; + $expectations[] = [4 - $i, ['id' => $id]]; } // Mock the database. @@ -560,9 +576,19 @@ public function test_reset_step_sortorder(): void { ->method('get_records') ->willReturn($mockdata); - $setfield = $DB->expects($this->exactly(5)) - ->method('set_field'); - call_user_func_array([$setfield, 'withConsecutive'], $expectations); + $setfieldinvocations = $this->exactly(5); + $DB->expects($setfieldinvocations) + ->method('set_field') + ->willReturnCallback(function ($table, $field, $value, $conditions) use ( + $setfieldinvocations, + $expectations, + ): void { + $expectation = $expectations[self::getInvocationCount($setfieldinvocations) - 1]; + $this->assertEquals('tool_usertours_steps', $table); + $this->assertEquals('sortorder', $field); + $this->assertEquals($expectation[0], $value); + $this->assertEquals($expectation[1], $conditions); + }); $tour->reset_step_sortorder(); } @@ -761,9 +787,12 @@ public function test_get_tour_key($id, $getconfig, $setconfig, $willpersist, $us ->getMock(); if ($getconfig) { - $tour->expects($this->exactly(count($getconfig))) + $getinvocations = $this->exactly(count($getconfig)); + $tour->expects($getinvocations) ->method('get_config') - ->will(call_user_func_array([$this, 'onConsecutiveCalls'], $getconfig)); + ->willReturnCallback(function () use ($getinvocations, $getconfig) { + return $getconfig[self::getInvocationCount($getinvocations) - 1]; + }); } if ($setconfig) { diff --git a/analytics/tests/analysis_test.php b/analytics/tests/analysis_test.php index 5d6fa8804fc43..c9e7db37115e2 100644 --- a/analytics/tests/analysis_test.php +++ b/analytics/tests/analysis_test.php @@ -30,7 +30,7 @@ class analysis_test extends \advanced_testcase { * @return null */ public function test_fill_firstanalyses_cache(): void { - require_once(__DIR__ . '/fixtures/test_timesplitting_upcoming_seconds.php'); + require_once(self::get_fixture_path(__NAMESPACE__, 'test_timesplitting_upcoming_seconds.php')); $this->resetAfterTest(); $modelid = 1; diff --git a/analytics/tests/behat/manage_models.feature b/analytics/tests/behat/manage_models.feature index e01619227ee3e..c92ba3c75a60f 100644 --- a/analytics/tests/behat/manage_models.feature +++ b/analytics/tests/behat/manage_models.feature @@ -148,7 +148,8 @@ Feature: Manage analytics models When I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row" And I choose "Export" in the open action menu And I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row" - And following "Export" should download between "100" and "500" bytes + And following "Export" should download a file that: + | Contains file in zip | model-config.json | Scenario: Check invalid site elements When I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row" diff --git a/analytics/tests/calculation_info_test.php b/analytics/tests/calculation_info_test.php index 6a837ee5c19b2..2976bcebf012e 100644 --- a/analytics/tests/calculation_info_test.php +++ b/analytics/tests/calculation_info_test.php @@ -23,8 +23,7 @@ * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class calculation_info_test extends \advanced_testcase { - +final class calculation_info_test extends \advanced_testcase { /** * test_calculation_info description * @@ -88,7 +87,7 @@ public function test_calculation_info_add_pull($info1, $info2, $info3, $info4): * * @return mixed[] */ - public function provider_test_calculation_info_add_pull() { + public static function provider_test_calculation_info_add_pull(): array { return [ 'mixed-types' => ['asd', true, [123, 123, 123], (object)['asd' => 'fgfg']], ]; diff --git a/analytics/tests/community_of_inquiry_activities_completed_by_test.php b/analytics/tests/community_of_inquiry_activities_completed_by_test.php index afa8df8f25824..8d16631f552e4 100644 --- a/analytics/tests/community_of_inquiry_activities_completed_by_test.php +++ b/analytics/tests/community_of_inquiry_activities_completed_by_test.php @@ -50,7 +50,7 @@ public static function availability_levels(): array { * @param string $availabilitylevel * @return void */ - public function test_get_activities_with_availability($availabilitylevel) { + public function test_get_activities_with_availability($availabilitylevel): void { list($course, $stu1) = $this->setup_course(); @@ -113,7 +113,7 @@ public function test_get_activities_with_availability($availabilitylevel) { * * @return void */ - public function test_get_activities_with_weeks() { + public function test_get_activities_with_weeks(): void { $startdate = gmmktime('0', '0', '0', 10, 24, 2015); $record = array( @@ -156,7 +156,7 @@ public function test_get_activities_with_weeks() { * * @return void */ - public function test_get_activities_by_section() { + public function test_get_activities_by_section(): void { // This makes debugging easier, sorry WA's +8 :). $this->setTimezone('UTC'); @@ -226,7 +226,7 @@ public function test_get_activities_by_section() { * * @return void */ - public function test_get_activities_with_specific_restrictions() { + public function test_get_activities_with_specific_restrictions(): void { list($course, $stu1) = $this->setup_course(); diff --git a/analytics/tests/indicator_test.php b/analytics/tests/indicator_test.php index 49c02edcba46f..f724498db5181 100644 --- a/analytics/tests/indicator_test.php +++ b/analytics/tests/indicator_test.php @@ -29,8 +29,7 @@ * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class indicator_test extends \advanced_testcase { - +final class indicator_test extends \advanced_testcase { /** * test_validate_calculated_value * @@ -50,7 +49,7 @@ public function test_validate_calculated_value($indicatorclass, $returnedvalue): * * @return array */ - public function validate_calculated_value() { + public static function validate_calculated_value(): array { return [ 'max' => ['test_indicator_max', [1]], 'min' => ['test_indicator_min', [-1]], @@ -83,7 +82,7 @@ public function test_validate_calculated_value_exceptions($indicatorclass, $will * * @return array */ - public function validate_calculated_value_exceptions() { + public static function validate_calculated_value_exceptions(): array { return [ 'max' => ['test_indicator_max', 2], 'min' => ['test_indicator_min', -2], diff --git a/analytics/tests/manager_test.php b/analytics/tests/manager_test.php index 802ddf52e2439..589f6021e5a3a 100644 --- a/analytics/tests/manager_test.php +++ b/analytics/tests/manager_test.php @@ -31,8 +31,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \core_analytics\manager */ -class manager_test extends \advanced_testcase { - +final class manager_test extends \advanced_testcase { /** * test_deleted_context */ @@ -44,7 +43,7 @@ public function test_deleted_context(): void { set_config('enabled_stores', 'logstore_standard', 'tool_log'); $target = \core_analytics\manager::get_target('test_target_course_level_shortname'); - $indicators = array('test_indicator_max', 'test_indicator_min', 'test_indicator_fullname'); + $indicators = ['test_indicator_max', 'test_indicator_min', 'test_indicator_fullname']; foreach ($indicators as $key => $indicator) { $indicators[$key] = \core_analytics\manager::get_indicator($indicator); } @@ -52,10 +51,10 @@ public function test_deleted_context(): void { $model = \core_analytics\model::create($target, $indicators); $modelobj = $model->get_model_obj(); - $coursepredict1 = $this->getDataGenerator()->create_course(array('visible' => 0)); - $coursepredict2 = $this->getDataGenerator()->create_course(array('visible' => 0)); - $coursetrain1 = $this->getDataGenerator()->create_course(array('visible' => 1)); - $coursetrain2 = $this->getDataGenerator()->create_course(array('visible' => 1)); + $coursepredict1 = $this->getDataGenerator()->create_course(['visible' => 0]); + $coursepredict2 = $this->getDataGenerator()->create_course(['visible' => 0]); + $coursetrain1 = $this->getDataGenerator()->create_course(['visible' => 1]); + $coursetrain2 = $this->getDataGenerator()->create_course(['visible' => 1]); $model->enable('\core\analytics\time_splitting\no_splitting'); @@ -65,25 +64,33 @@ public function test_deleted_context(): void { // Generate a prediction action to confirm that it is deleted when there is an important update. $predictions = $DB->get_records('analytics_predictions'); $prediction = reset($predictions); - $prediction = new \core_analytics\prediction($prediction, array('whatever' => 'not used')); + $prediction = new \core_analytics\prediction($prediction, ['whatever' => 'not used']); $prediction->action_executed(\core_analytics\prediction::ACTION_USEFUL, $model->get_target()); $predictioncontextid = $prediction->get_prediction_data()->contextid; - $npredictions = $DB->count_records('analytics_predictions', array('contextid' => $predictioncontextid)); - $npredictionactions = $DB->count_records('analytics_prediction_actions', - array('predictionid' => $prediction->get_prediction_data()->id)); - $nindicatorcalc = $DB->count_records('analytics_indicator_calc', array('contextid' => $predictioncontextid)); + $npredictions = $DB->count_records('analytics_predictions', ['contextid' => $predictioncontextid]); + $npredictionactions = $DB->count_records( + 'analytics_prediction_actions', + ['predictionid' => $prediction->get_prediction_data()->id] + ); + $nindicatorcalc = $DB->count_records('analytics_indicator_calc', ['contextid' => $predictioncontextid]); \core_analytics\manager::cleanup(); // Nothing is incorrectly deleted. - $this->assertEquals($npredictions, $DB->count_records('analytics_predictions', - array('contextid' => $predictioncontextid))); - $this->assertEquals($npredictionactions, $DB->count_records('analytics_prediction_actions', - array('predictionid' => $prediction->get_prediction_data()->id))); - $this->assertEquals($nindicatorcalc, $DB->count_records('analytics_indicator_calc', - array('contextid' => $predictioncontextid))); + $this->assertEquals($npredictions, $DB->count_records( + 'analytics_predictions', + ['contextid' => $predictioncontextid] + )); + $this->assertEquals($npredictionactions, $DB->count_records( + 'analytics_prediction_actions', + ['predictionid' => $prediction->get_prediction_data()->id] + )); + $this->assertEquals($nindicatorcalc, $DB->count_records( + 'analytics_indicator_calc', + ['contextid' => $predictioncontextid] + )); // Now we delete a context, the course predictions and prediction actions should be deleted. $deletedcontext = \context::instance_by_id($predictioncontextid); @@ -91,10 +98,12 @@ public function test_deleted_context(): void { \core_analytics\manager::cleanup(); - $this->assertEmpty($DB->count_records('analytics_predictions', array('contextid' => $predictioncontextid))); - $this->assertEmpty($DB->count_records('analytics_prediction_actions', - array('predictionid' => $prediction->get_prediction_data()->id))); - $this->assertEmpty($DB->count_records('analytics_indicator_calc', array('contextid' => $predictioncontextid))); + $this->assertEmpty($DB->count_records('analytics_predictions', ['contextid' => $predictioncontextid])); + $this->assertEmpty($DB->count_records( + 'analytics_prediction_actions', + ['predictionid' => $prediction->get_prediction_data()->id] + )); + $this->assertEmpty($DB->count_records('analytics_indicator_calc', ['contextid' => $predictioncontextid])); set_config('enabled_stores', '', 'tool_log'); get_log_manager(true); @@ -111,7 +120,7 @@ public function test_deleted_analysable(): void { set_config('enabled_stores', 'logstore_standard', 'tool_log'); $target = \core_analytics\manager::get_target('test_target_course_level_shortname'); - $indicators = array('test_indicator_max', 'test_indicator_min', 'test_indicator_fullname'); + $indicators = ['test_indicator_max', 'test_indicator_min', 'test_indicator_fullname']; foreach ($indicators as $key => $indicator) { $indicators[$key] = \core_analytics\manager::get_indicator($indicator); } @@ -119,10 +128,10 @@ public function test_deleted_analysable(): void { $model = \core_analytics\model::create($target, $indicators); $modelobj = $model->get_model_obj(); - $coursepredict1 = $this->getDataGenerator()->create_course(array('visible' => 0)); - $coursepredict2 = $this->getDataGenerator()->create_course(array('visible' => 0)); - $coursetrain1 = $this->getDataGenerator()->create_course(array('visible' => 1)); - $coursetrain2 = $this->getDataGenerator()->create_course(array('visible' => 1)); + $coursepredict1 = $this->getDataGenerator()->create_course(['visible' => 0]); + $coursepredict2 = $this->getDataGenerator()->create_course(['visible' => 0]); + $coursetrain1 = $this->getDataGenerator()->create_course(['visible' => 1]); + $coursetrain2 = $this->getDataGenerator()->create_course(['visible' => 1]); $model->enable('\core\analytics\time_splitting\no_splitting'); @@ -139,9 +148,9 @@ public function test_deleted_analysable(): void { \core_analytics\manager::cleanup(); - $this->assertEmpty($DB->count_records('analytics_predict_samples', array('analysableid' => $coursepredict1->id))); - $this->assertEmpty($DB->count_records('analytics_train_samples', array('analysableid' => $coursepredict1->id))); - $this->assertEmpty($DB->count_records('analytics_used_analysables', array('analysableid' => $coursepredict1->id))); + $this->assertEmpty($DB->count_records('analytics_predict_samples', ['analysableid' => $coursepredict1->id])); + $this->assertEmpty($DB->count_records('analytics_train_samples', ['analysableid' => $coursepredict1->id])); + $this->assertEmpty($DB->count_records('analytics_used_analysables', ['analysableid' => $coursepredict1->id])); set_config('enabled_stores', '', 'tool_log'); get_log_manager(true); @@ -190,7 +199,7 @@ public function test_validate_models_declaration(): void { $this->resetAfterTest(); // This is expected to run without an exception. - $models = $this->load_models_from_fixture_file('no_teaching'); + $models = self::load_models_from_fixture_file('no_teaching'); \core_analytics\manager::validate_models_declaration($models); } @@ -214,34 +223,34 @@ public function test_validate_models_declaration_exceptions(array $models, strin * * @return array of (string)testcase => [(array)models, (string)expected exception message] */ - public function validate_models_declaration_exceptions_provider() { + public static function validate_models_declaration_exceptions_provider(): array { return [ 'missing_target' => [ - $this->load_models_from_fixture_file('missing_target'), + self::load_models_from_fixture_file('missing_target'), 'Missing target declaration', ], 'invalid_target' => [ - $this->load_models_from_fixture_file('invalid_target'), + self::load_models_from_fixture_file('invalid_target'), 'Invalid target classname', ], 'missing_indicators' => [ - $this->load_models_from_fixture_file('missing_indicators'), + self::load_models_from_fixture_file('missing_indicators'), 'Missing indicators declaration', ], 'invalid_indicators' => [ - $this->load_models_from_fixture_file('invalid_indicators'), + self::load_models_from_fixture_file('invalid_indicators'), 'Invalid indicator classname', ], 'invalid_time_splitting' => [ - $this->load_models_from_fixture_file('invalid_time_splitting'), + self::load_models_from_fixture_file('invalid_time_splitting'), 'Invalid time splitting classname', ], 'invalid_time_splitting_fq' => [ - $this->load_models_from_fixture_file('invalid_time_splitting_fq'), + self::load_models_from_fixture_file('invalid_time_splitting_fq'), 'Expecting fully qualified time splitting classname', ], 'invalid_enabled' => [ - $this->load_models_from_fixture_file('invalid_enabled'), + self::load_models_from_fixture_file('invalid_enabled'), 'Cannot enable a model without time splitting method specified', ], ]; @@ -253,12 +262,12 @@ public function validate_models_declaration_exceptions_provider() { * @param string $filename * @return array */ - protected function load_models_from_fixture_file(string $filename) { + protected static function load_models_from_fixture_file(string $filename) { global $CFG; $models = null; - require($CFG->dirroot.'/analytics/tests/fixtures/db_analytics_php/'.$filename.'.php'); + require("{$CFG->dirroot}/analytics/tests/fixtures/db_analytics_php/{$filename}.php"); return $models; } @@ -430,9 +439,9 @@ public function test_get_time_splitting_methods(): void { */ public function test_model_declaration_identifier(): void { - $noteaching1 = $this->load_models_from_fixture_file('no_teaching'); - $noteaching2 = $this->load_models_from_fixture_file('no_teaching'); - $noteaching3 = $this->load_models_from_fixture_file('no_teaching'); + $noteaching1 = self::load_models_from_fixture_file('no_teaching'); + $noteaching2 = self::load_models_from_fixture_file('no_teaching'); + $noteaching3 = self::load_models_from_fixture_file('no_teaching'); // Same model declaration should always lead to same identifier. $this->assertEquals( @@ -474,9 +483,9 @@ public function test_model_declaration_identifier(): void { public function test_get_declared_target_and_indicators_instances(): void { $this->resetAfterTest(); - $definition = $this->load_models_from_fixture_file('no_teaching'); + $definition = self::load_models_from_fixture_file('no_teaching'); - list($target, $indicators) = \core_analytics\manager::get_declared_target_and_indicators_instances($definition[0]); + [$target, $indicators] = \core_analytics\manager::get_declared_target_and_indicators_instances($definition[0]); $this->assertTrue($target instanceof \core_analytics\local\target\base); $this->assertNotEmpty($indicators); diff --git a/analytics/tests/prediction_actions_test.php b/analytics/tests/prediction_actions_test.php index ff17cebb6b2c1..ff67f0bd815c3 100644 --- a/analytics/tests/prediction_actions_test.php +++ b/analytics/tests/prediction_actions_test.php @@ -135,7 +135,7 @@ public function test_action_executed(): void { * * @return array */ - public function execute_actions_provider(): array { + public static function execute_actions_provider(): array { return [ 'Empty actions with no filter' => [ [], diff --git a/analytics/tests/prediction_test.php b/analytics/tests/prediction_test.php index 2f0750ddedc65..0b42c371598dd 100644 --- a/analytics/tests/prediction_test.php +++ b/analytics/tests/prediction_test.php @@ -341,14 +341,14 @@ public function test_ml_training_and_prediction($timesplittingid, $predictedrang * * @return array */ - public function provider_ml_training_and_prediction() { + public static function provider_ml_training_and_prediction(): array { $cases = array( 'no_splitting' => array('\core\analytics\time_splitting\no_splitting', 0, 1), 'quarters' => array('\core\analytics\time_splitting\quarters', 3, 4) ); // We need to test all system prediction processors. - return $this->add_prediction_processors($cases); + return static::add_prediction_processors($cases); } /** @@ -417,13 +417,13 @@ public function test_ml_export_import($predictionsprocessorclass, $forcedconfig) * * @return array */ - public function provider_ml_processors() { + public static function provider_ml_processors(): array { $cases = [ 'case' => [], ]; // We need to test all system prediction processors. - return $this->add_prediction_processors($cases); + return static::add_prediction_processors($cases); } /** * Test the system classifiers returns. @@ -507,7 +507,7 @@ public function test_ml_classifiers_return($success, $nsamples, $classes, $predi * * @return array */ - public function provider_ml_classifiers_return() { + public static function provider_ml_classifiers_return(): array { // Using verbose options as the first argument for readability. $cases = array( '1-samples' => array('maybe', 1, [0]), @@ -517,7 +517,7 @@ public function provider_ml_classifiers_return() { ); // We need to test all system prediction processors. - return $this->add_prediction_processors($cases); + return static::add_prediction_processors($cases); } /** @@ -581,13 +581,13 @@ public function test_ml_multi_classifier($timesplittingid, $predictionsprocessor * * @return array */ - public function provider_test_multi_classifier() { + public static function provider_test_multi_classifier(): array { $cases = array( 'notimesplitting' => array('\core\analytics\time_splitting\no_splitting'), ); // Add all system prediction processors. - return $this->add_prediction_processors($cases); + return static::add_prediction_processors($cases); } /** @@ -777,8 +777,7 @@ public function test_not_null_samples(): void { * * @return array */ - public function provider_ml_test_evaluation_configuration() { - + public static function provider_ml_test_evaluation_configuration(): array { $cases = array( 'bad' => array( 'modelquality' => 'random', @@ -797,7 +796,7 @@ public function provider_ml_test_evaluation_configuration() { ) ) ); - return $this->add_prediction_processors($cases); + return static::add_prediction_processors($cases); } /** @@ -955,8 +954,7 @@ protected function is_predictions_processor_ready(string $predictionsprocessorcl * @param array $cases * @return array */ - protected function add_prediction_processors($cases) { - + protected static function add_prediction_processors($cases): array { $return = array(); if (defined('TEST_MLBACKEND_PYTHON_HOST') && defined('TEST_MLBACKEND_PYTHON_PORT') diff --git a/auth/ldap/tests/auth_ldap_test.php b/auth/ldap/tests/auth_ldap_test.php index 48ee545fbf658..b05a2da21faf5 100644 --- a/auth/ldap/tests/auth_ldap_test.php +++ b/auth/ldap/tests/auth_ldap_test.php @@ -66,7 +66,7 @@ public static function setUpBeforeClass(): void { * * @return array[] */ - public function auth_ldap_provider() { + public static function auth_ldap_provider(): array { $pagesizes = [1, 3, 5, 1000]; $subcontexts = [0, 1]; $combinations = []; diff --git a/auth/lti/tests/auth_test.php b/auth/lti/tests/auth_test.php index 4b585ef68a380..695d7d2941253 100644 --- a/auth/lti/tests/auth_test.php +++ b/auth/lti/tests/auth_test.php @@ -27,7 +27,7 @@ class auth_test extends \advanced_testcase { /** @var string issuer URL used for test cases. */ - protected $issuer = 'https://lms.example.org'; + protected static string $issuer = 'https://lms.example.org'; /** @var int const representing cases where no PII is present. */ protected const PII_NONE = 0; @@ -70,10 +70,13 @@ protected function verify_user_profile_image_updated(int $userid): void { * @param bool $includepicture whether to include a profile picture or not (slows tests, so defaults to false). * @return array the users list. */ - protected function get_mock_users_with_ids(array $ids, - string $role = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', bool $includenames = true, - bool $includeemail = true, bool $includepicture = false): array { - + protected static function get_mock_users_with_ids( + array $ids, + string $role = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', + bool $includenames = true, + bool $includeemail = true, + bool $includepicture = false, + ): array { $users = []; foreach ($ids as $id) { $user = [ @@ -91,7 +94,7 @@ protected function get_mock_users_with_ids(array $ids, unset($user['email']); } if ($includepicture) { - $user['picture'] = $this->getExternalTestFileUrl('/test.jpg'); + $user['picture'] = self::getExternalTestFileUrl('/test.jpg'); } $users[] = $user; } @@ -137,7 +140,7 @@ protected function get_mock_member_data_for_user(array $mockuser, string $legacy */ protected function get_mock_launchdata_for_user(array $mockuser, array $mockmigration = []): array { $data = [ - 'iss' => $this->issuer, // Must match registration in create_test_environment. + 'iss' => self::$issuer, // Must match registration in create_test_environment. 'aud' => '123', // Must match registration in create_test_environment. 'sub' => $mockuser['user_id'], // User id on the platform site. 'exp' => time() + 60, @@ -286,12 +289,12 @@ public function test_find_or_create_user_from_launch(?array $legacydata, array $ * * @return array the test case data. */ - public function launch_data_provider(): array { + public static function launch_data_provider(): array { return [ 'New (unlinked) platform learner including PII, no legacy user, no migration claim' => [ 'legacy_data' => null, 'launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], @@ -301,7 +304,7 @@ public function launch_data_provider(): array { 'New (unlinked) platform learner excluding names, no legacy user, no migration claim' => [ 'legacy_data' => null, 'launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false @@ -312,7 +315,7 @@ public function launch_data_provider(): array { 'New (unlinked) platform learner excluding emails, no legacy user, no migration claim' => [ 'legacy_data' => null, 'launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', true, @@ -324,7 +327,7 @@ public function launch_data_provider(): array { 'New (unlinked) platform learner excluding all PII, no legacy user, no migration claim' => [ 'legacy_data' => null, 'launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, @@ -345,7 +348,7 @@ public function launch_data_provider(): array { ] ], 'launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], @@ -374,7 +377,7 @@ public function launch_data_provider(): array { ] ], 'launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], @@ -396,7 +399,7 @@ public function launch_data_provider(): array { ] ], 'launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], @@ -425,7 +428,7 @@ public function launch_data_provider(): array { ] ], 'launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], @@ -454,7 +457,7 @@ public function launch_data_provider(): array { ] ], 'launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], @@ -483,7 +486,7 @@ public function launch_data_provider(): array { ] ], 'launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], @@ -512,7 +515,7 @@ public function launch_data_provider(): array { ] ], 'launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], @@ -540,7 +543,7 @@ public function launch_data_provider(): array { ] ], 'launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], @@ -569,7 +572,7 @@ public function launch_data_provider(): array { ] ], 'launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, @@ -591,7 +594,7 @@ public function launch_data_provider(): array { 'New (unlinked) platform instructor including PII, no legacy user, no migration claim' => [ 'legacy_data' => null, 'launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor' )[0], @@ -601,7 +604,7 @@ public function launch_data_provider(): array { 'New (unlinked) platform instructor excluding PII, no legacy user, no migration claim' => [ 'legacy_data' => null, 'launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', false, @@ -622,7 +625,7 @@ public function launch_data_provider(): array { ] ], 'launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor' )[0], @@ -643,7 +646,7 @@ public function launch_data_provider(): array { 'legacy_data' => null, 'launch_data' => [ 'has_authenticated_before' => true, - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], @@ -654,7 +657,7 @@ public function launch_data_provider(): array { 'legacy_data' => null, 'launch_data' => [ 'has_authenticated_before' => true, - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, @@ -667,7 +670,7 @@ public function launch_data_provider(): array { 'legacy_data' => null, 'launch_data' => [ 'has_authenticated_before' => true, - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor' )[0], @@ -678,7 +681,7 @@ public function launch_data_provider(): array { 'legacy_data' => null, 'launch_data' => [ 'has_authenticated_before' => true, - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', false, @@ -691,7 +694,7 @@ public function launch_data_provider(): array { 'legacy_data' => null, 'launch_data' => [ 'has_authenticated_before' => false, - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', false, @@ -810,17 +813,17 @@ public function test_find_or_create_user_from_membership(?array $legacydata, arr * * @return array the test case data. */ - public function membership_data_provider(): array { + public static function membership_data_provider(): array { return [ 'New (unlinked) platform learner including PII, no legacy data, no consumer key bound, no legacy id' => [ 'legacy_data' => null, 'membership_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], ], - 'iss' => $this->issuer, + 'iss' => self::$issuer, 'legacy_consumer_key' => null, 'expected' => [ 'PII' => self::PII_ALL, @@ -830,14 +833,14 @@ public function membership_data_provider(): array { 'New (unlinked) platform learner excluding PII, no legacy data, no consumer key bound, no legacy id' => [ 'legacy_data' => null, 'membership_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, false )[0], ], - 'iss' => $this->issuer, + 'iss' => self::$issuer, 'legacy_consumer_key' => null, 'expected' => [ 'PII' => self::PII_NONE, @@ -847,13 +850,13 @@ public function membership_data_provider(): array { 'New (unlinked) platform learner excluding names, no legacy data, no consumer key bound, no legacy id' => [ 'legacy_data' => null, 'membership_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, )[0], ], - 'iss' => $this->issuer, + 'iss' => self::$issuer, 'legacy_consumer_key' => null, 'expected' => [ 'PII' => self::PII_EMAILS_ONLY, @@ -863,14 +866,14 @@ public function membership_data_provider(): array { 'New (unlinked) platform learner excluding email, no legacy data, no consumer key bound, no legacy id' => [ 'legacy_data' => null, 'membership_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', true, false )[0], ], - 'iss' => $this->issuer, + 'iss' => self::$issuer, 'legacy_consumer_key' => null, 'expected' => [ 'PII' => self::PII_NAMES_ONLY, @@ -885,13 +888,13 @@ public function membership_data_provider(): array { 'consumer_key' => 'CONSUMER_1', ], 'membership_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'legacy_user_id' => '123-abc' ], - 'iss' => $this->issuer, + 'iss' => self::$issuer, 'legacy_consumer_key' => 'CONSUMER_1', 'expected' => [ 'PII' => self::PII_ALL, @@ -906,12 +909,12 @@ public function membership_data_provider(): array { 'consumer_key' => 'CONSUMER_1', ], 'membership_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], ], - 'iss' => $this->issuer, + 'iss' => self::$issuer, 'legacy_consumer_key' => 'CONSUMER_1', 'expected' => [ 'PII' => self::PII_ALL, @@ -926,12 +929,12 @@ public function membership_data_provider(): array { 'consumer_key' => 'CONSUMER_1', ], 'membership_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['123-abc'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], ], - 'iss' => $this->issuer, + 'iss' => self::$issuer, 'legacy_consumer_key' => 'CONSUMER_1', 'expected' => [ 'PII' => self::PII_ALL, @@ -946,12 +949,12 @@ public function membership_data_provider(): array { 'consumer_key' => 'CONSUMER_1', ], 'membership_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['123-abc'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], ], - 'iss' => $this->issuer, + 'iss' => self::$issuer, 'legacy_consumer_key' => 'CONSUMER_ABCDEF', 'expected' => [ 'PII' => self::PII_ALL, @@ -966,13 +969,13 @@ public function membership_data_provider(): array { 'consumer_key' => 'CONSUMER_1', ], 'membership_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'legacy_user_id' => '123-abc' ], - 'iss' => $this->issuer, + 'iss' => self::$issuer, 'legacy_consumer_key' => null, 'expected' => [ 'PII' => self::PII_ALL, @@ -982,13 +985,13 @@ public function membership_data_provider(): array { 'New (unlinked) platform learner including PII, no legacy data, consumer key bound, legacy user id sent' => [ 'legacy_data' => null, 'membership_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'legacy_user_id' => '123-abc' ], - 'iss' => $this->issuer, + 'iss' => self::$issuer, 'legacy_consumer_key' => 'CONSUMER_1', 'expected' => [ 'PII' => self::PII_ALL, @@ -998,12 +1001,12 @@ public function membership_data_provider(): array { 'New (unlinked) platform instructor including PII, no legacy data, no consumer key bound, no legacy id' => [ 'legacy_data' => null, 'membership_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor' )[0], ], - 'iss' => $this->issuer, + 'iss' => self::$issuer, 'legacy_consumer_key' => null, 'expected' => [ 'PII' => self::PII_ALL, @@ -1013,14 +1016,14 @@ public function membership_data_provider(): array { 'New (unlinked) platform instructor excluding PII, no legacy data, no consumer key bound, no legacy id' => [ 'legacy_data' => null, 'membership_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', false, false )[0], ], - 'iss' => $this->issuer, + 'iss' => self::$issuer, 'legacy_consumer_key' => null, 'expected' => [ 'PII' => self::PII_NONE, @@ -1031,12 +1034,12 @@ public function membership_data_provider(): array { 'legacy_data' => null, 'launch_data' => [ 'has_authenticated_before' => true, - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], ], - 'iss' => $this->issuer, + 'iss' => self::$issuer, 'legacy_consumer_key' => null, 'expected' => [ 'PII' => self::PII_ALL, @@ -1056,7 +1059,7 @@ public function test_create_user_binding(): void { global $DB; $auth = get_auth_plugin('lti'); $user = $this->getDataGenerator()->create_user(); - $mockiss = $this->issuer; + $mockiss = self::$issuer; $mocksub = '1'; // Create a binding and verify it exists. @@ -1068,7 +1071,7 @@ public function test_create_user_binding(): void { $numusersbefore = $DB->count_records('user'); $matcheduser = $auth->find_or_create_user_from_launch( $this->get_mock_launchdata_for_user( - $this->get_mock_users_with_ids([$mocksub])[0] + self::get_mock_users_with_ids([$mocksub])[0] ) ); $numusersafter = $DB->count_records('user'); @@ -1148,17 +1151,17 @@ public function test_update_user_account(array $firstlaunchdata, array $launchda * * @return array the test case data. */ - public function update_user_account_provider(): array { + public static function update_user_account_provider(): array { return [ 'Full PII included in both auths, no picture in either' => [ 'first_launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0] ], 'launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], @@ -1171,7 +1174,7 @@ public function update_user_account_provider(): array { ], 'No PII included in both auths, no picture in either' => [ 'first_launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, @@ -1179,7 +1182,7 @@ public function update_user_account_provider(): array { )[0] ], 'launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, @@ -1194,7 +1197,7 @@ public function update_user_account_provider(): array { ], 'First auth no PII, second auth including PII, no picture in either' => [ 'first_launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, @@ -1202,7 +1205,7 @@ public function update_user_account_provider(): array { )[0] ], 'launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], @@ -1215,13 +1218,13 @@ public function update_user_account_provider(): array { ], 'First auth full PII, second auth no PII, no picture in either' => [ 'first_launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', )[0] ], 'launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, @@ -1236,13 +1239,13 @@ public function update_user_account_provider(): array { ], 'First auth full PII, second auth emails only, no picture in either' => [ 'first_launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', )[0] ], 'launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false @@ -1256,13 +1259,13 @@ public function update_user_account_provider(): array { ], 'First auth full PII, second auth names only, no picture in either' => [ 'first_launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', )[0] ], 'launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', true, @@ -1277,13 +1280,13 @@ public function update_user_account_provider(): array { ], 'Full PII included in both auths, picture included in the second auth' => [ 'first_launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0] ], 'launch_data' => [ - 'user' => $this->get_mock_users_with_ids( + 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', true, diff --git a/availability/condition/completion/tests/condition_test.php b/availability/condition/completion/tests/condition_test.php index f36e36d4e5e99..bfc6a86a0a248 100644 --- a/availability/condition/completion/tests/condition_test.php +++ b/availability/condition/completion/tests/condition_test.php @@ -469,7 +469,7 @@ public function test_previous_activity(int $grade, int $condition, string $mark, $this->assertMatchesRegularExpression($description, $information); } - public function previous_activity_data(): array { + public static function previous_activity_data(): array { // Assign grade, condition, activity to complete, activity to test, result, resultnot, description. return [ 'Missing previous activity complete' => [ @@ -634,7 +634,7 @@ public function test_section_previous_activity(int $condition, bool $mark, strin } - public function section_previous_activity_data(): array { + public static function section_previous_activity_data(): array { return [ // Condition, Activity completion, section to test, result, resultnot, description. 'Completion complete Section with no previous activity' => [ diff --git a/availability/condition/group/classes/frontend.php b/availability/condition/group/classes/frontend.php index 26d7830282c98..1b8c803b5d64c 100644 --- a/availability/condition/group/classes/frontend.php +++ b/availability/condition/group/classes/frontend.php @@ -72,7 +72,7 @@ protected function get_all_groups($courseid) { require_once($CFG->libdir . '/grouplib.php'); if ($courseid != $this->allgroupscourseid) { - $this->allgroups = groups_get_all_groups($courseid, 0, 0, 'g.id, g.name'); + $this->allgroups = groups_get_all_groups($courseid, 0, 0, 'g.id, g.name, g.visibility'); $this->allgroupscourseid = $courseid; } return $this->allgroups; diff --git a/backup/moodle2/backup_stepslib.php b/backup/moodle2/backup_stepslib.php index e5496e408743b..da33c4f98003f 100644 --- a/backup/moodle2/backup_stepslib.php +++ b/backup/moodle2/backup_stepslib.php @@ -557,10 +557,13 @@ protected function define_structure() { FROM {course_format_options} WHERE courseid = ? AND sectionid = 0', [ backup::VAR_PARENTID ]); - $handler = core_course\customfield\course_handler::create(); - $fieldsforbackup = $handler->get_instance_data_for_backup($this->task->get_courseid()); - $handler->backup_define_structure($this->task->get_courseid(), $customfield); - $customfield->set_source_array($fieldsforbackup); + // Custom fields. + if ($this->get_setting_value('customfield')) { + $handler = core_course\customfield\course_handler::create(); + $fieldsforbackup = $handler->get_instance_data_for_backup($this->task->get_courseid()); + $handler->backup_define_structure($this->task->get_courseid(), $customfield); + $customfield->set_source_array($fieldsforbackup); + } // Some annotations @@ -1409,14 +1412,19 @@ protected function define_structure() { FROM {groups} g JOIN {backup_ids_temp} bi ON g.id = bi.itemid WHERE bi.backupid = ? - AND bi.itemname = 'groupfinal'", array(backup::VAR_BACKUPID)); + AND bi.itemname = 'groupfinal'", + [backup_helper::is_sqlparam($this->get_backupid())] + ); $grouping->set_source_sql(" SELECT g.* FROM {groupings} g JOIN {backup_ids_temp} bi ON g.id = bi.itemid WHERE bi.backupid = ? - AND bi.itemname = 'groupingfinal'", array(backup::VAR_BACKUPID)); + AND bi.itemname = 'groupingfinal'", + [backup_helper::is_sqlparam($this->get_backupid())] + ); + $groupinggroup->set_source_table('groupings_groups', array('groupingid' => backup::VAR_PARENTID)); // This only happens if we are including users. @@ -1424,9 +1432,20 @@ protected function define_structure() { $member->set_source_table('groups_members', array('groupid' => backup::VAR_PARENTID)); } - $courseid = $this->task->get_courseid(); - $groupcustomfield->set_source_array($this->get_group_custom_fields_for_backup($courseid)); - $groupingcustomfield->set_source_array($this->get_grouping_custom_fields_for_backup($courseid)); + // Custom fields. + if ($this->get_setting_value('customfield')) { + $groupcustomfieldarray = $this->get_group_custom_fields_for_backup( + $group->get_source_sql(), + [$this->get_backupid()] + ); + $groupcustomfield->set_source_array($groupcustomfieldarray); + + $groupingcustomfieldarray = $this->get_grouping_custom_fields_for_backup( + $grouping->get_source_sql(), + [$this->get_backupid()] + ); + $groupingcustomfield->set_source_array($groupingcustomfieldarray); + } } // Define id annotations (as final) @@ -1445,14 +1464,16 @@ protected function define_structure() { /** * Get custom fields array for group - * @param int $courseid + * + * @param string $groupsourcesql + * @param array $groupsourceparams * @return array */ - protected function get_group_custom_fields_for_backup(int $courseid): array { + protected function get_group_custom_fields_for_backup(string $groupsourcesql, array $groupsourceparams): array { global $DB; $handler = \core_group\customfield\group_handler::create(); $fieldsforbackup = []; - if ($groups = $DB->get_records('groups', ['courseid' => $courseid], '', 'id')) { + if ($groups = $DB->get_records_sql($groupsourcesql, $groupsourceparams)) { foreach ($groups as $group) { $fieldsforbackup = array_merge($fieldsforbackup, $handler->get_instance_data_for_backup($group->id)); } @@ -1462,14 +1483,16 @@ protected function get_group_custom_fields_for_backup(int $courseid): array { /** * Get custom fields array for grouping - * @param int $courseid + * + * @param string $groupingsourcesql + * @param array $groupingsourceparams * @return array */ - protected function get_grouping_custom_fields_for_backup(int $courseid): array { + protected function get_grouping_custom_fields_for_backup(string $groupingsourcesql, array $groupingsourceparams): array { global $DB; $handler = \core_group\customfield\grouping_handler::create(); $fieldsforbackup = []; - if ($groupings = $DB->get_records('groupings', ['courseid' => $courseid], '', 'id')) { + if ($groupings = $DB->get_records_sql($groupingsourcesql, $groupingsourceparams)) { foreach ($groupings as $grouping) { $fieldsforbackup = array_merge($fieldsforbackup, $handler->get_instance_data_for_backup($grouping->id)); } diff --git a/backup/moodle2/restore_qtype_plugin.class.php b/backup/moodle2/restore_qtype_plugin.class.php index 850af2b02b3b6..23d0a3dc076d9 100644 --- a/backup/moodle2/restore_qtype_plugin.class.php +++ b/backup/moodle2/restore_qtype_plugin.class.php @@ -380,7 +380,8 @@ public function recode_legacy_state_answer($state) { * * Only common stuff to all plugins, in this case: * - question: text and feedback - * - question_answers: text and feedbak + * - question_answers: text and feedback + * - question_hints: hint * * Note each qtype will have, if needed, its own define_decode_contents method */ @@ -388,8 +389,9 @@ public static function define_plugin_decode_contents() { $contents = array(); - $contents[] = new restore_decode_content('question', array('questiontext', 'generalfeedback'), 'question_created'); - $contents[] = new restore_decode_content('question_answers', array('answer', 'feedback'), 'question_answer'); + $contents[] = new restore_decode_content('question', ['questiontext', 'generalfeedback'], 'question_created'); + $contents[] = new restore_decode_content('question_answers', ['answer', 'feedback'], 'question_answer'); + $contents[] = new restore_decode_content('question_hints', ['hint'], 'question_hint'); return $contents; } diff --git a/backup/moodle2/restore_root_task.class.php b/backup/moodle2/restore_root_task.class.php index 107463f1c4fea..2a93bc031d6df 100644 --- a/backup/moodle2/restore_root_task.class.php +++ b/backup/moodle2/restore_root_task.class.php @@ -300,8 +300,16 @@ protected function define_settings() { $competencies->set_ui(new backup_setting_ui_checkbox($competencies, get_string('rootsettingcompetencies', 'backup'))); $this->add_setting($competencies); - $customfields = new restore_customfield_setting('customfields', base_setting::IS_BOOLEAN, $defaultvalue); + // Custom fields. + $defaultvalue = false; + $changeable = false; + if (isset($rootsettings['customfield']) && $rootsettings['customfield']) { // Only enabled when available. + $defaultvalue = true; + $changeable = true; + } + $customfields = new restore_customfield_setting('customfield', base_setting::IS_BOOLEAN, $defaultvalue); $customfields->set_ui(new backup_setting_ui_checkbox($customfields, get_string('rootsettingcustomfield', 'backup'))); + $customfields->get_ui()->set_changeable($changeable); $this->add_setting($customfields); // Define Content bank content. diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index 39bdb0030b291..e8f8b7f4da4d8 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -1159,11 +1159,15 @@ protected function define_structure() { $groupinfo = $this->get_setting_value('groups'); if ($groupinfo) { $paths[] = new restore_path_element('group', '/groups/group'); - $paths[] = new restore_path_element('groupcustomfield', '/groups/groupcustomfields/groupcustomfield'); $paths[] = new restore_path_element('grouping', '/groups/groupings/grouping'); - $paths[] = new restore_path_element('groupingcustomfield', - '/groups/groupings/groupingcustomfields/groupingcustomfield'); $paths[] = new restore_path_element('grouping_group', '/groups/groupings/grouping/grouping_groups/grouping_group'); + + // Custom fields. + if ($this->get_setting_value('customfield')) { + $paths[] = new restore_path_element('groupcustomfield', '/groups/groupcustomfields/groupcustomfield'); + $paths[] = new restore_path_element('groupingcustomfield', + '/groups/groupings/groupingcustomfields/groupingcustomfield'); + } } return $paths; } @@ -1235,9 +1239,11 @@ public function process_group($data) { */ public function process_groupcustomfield($data) { $newgroup = $this->get_mapping('group', $data['groupid']); - $data['groupid'] = $newgroup->newitemid ?? $data['groupid']; - $handler = \core_group\customfield\group_handler::create(); - $handler->restore_instance_data_from_backup($this->task, $data); + if ($newgroup && $newgroup->newitemid) { + $data['groupid'] = $newgroup->newitemid; + $handler = \core_group\customfield\group_handler::create(); + $handler->restore_instance_data_from_backup($this->task, $data); + } } public function process_grouping($data) { @@ -1291,10 +1297,12 @@ public function process_grouping($data) { * @return void */ public function process_groupingcustomfield($data) { - $newgroup = $this->get_mapping('grouping', $data['groupingid']); - $data['groupingid'] = $newgroup->newitemid ?? $data['groupingid']; - $handler = \core_group\customfield\grouping_handler::create(); - $handler->restore_instance_data_from_backup($this->task, $data); + $newgrouping = $this->get_mapping('grouping', $data['groupingid']); + if ($newgrouping && $newgrouping->newitemid) { + $data['groupingid'] = $newgrouping->newitemid; + $handler = \core_group\customfield\grouping_handler::create(); + $handler->restore_instance_data_from_backup($this->task, $data); + } } public function process_grouping_group($data) { @@ -1832,12 +1840,19 @@ class restore_course_structure_step extends restore_structure_step { protected function define_structure() { + $paths = []; + $course = new restore_path_element('course', '/course'); - $category = new restore_path_element('category', '/course/category'); - $tag = new restore_path_element('tag', '/course/tags/tag'); - $customfield = new restore_path_element('customfield', '/course/customfields/customfield'); - $courseformatoptions = new restore_path_element('course_format_option', '/course/courseformatoptions/courseformatoption'); - $allowedmodule = new restore_path_element('allowed_module', '/course/allowed_modules/module'); + $paths[] = $course; + $paths[] = new restore_path_element('category', '/course/category'); + $paths[] = new restore_path_element('tag', '/course/tags/tag'); + $paths[] = new restore_path_element('course_format_option', '/course/courseformatoptions/courseformatoption'); + $paths[] = new restore_path_element('allowed_module', '/course/allowed_modules/module'); + + // Custom fields. + if ($this->get_setting_value('customfield')) { + $paths[] = new restore_path_element('customfield', '/course/customfields/customfield'); + } // Apply for 'format' plugins optional paths at course level $this->add_plugin_structure('format', $course); @@ -1860,7 +1875,7 @@ protected function define_structure() { // Apply for admin tool plugins optional paths at course level. $this->add_plugin_structure('tool', $course); - return array($course, $category, $tag, $customfield, $allowedmodule, $courseformatoptions); + return $paths; } /** @@ -4921,6 +4936,12 @@ class restore_create_categories_and_questions extends restore_structure_step { /** @var array $cachedcategory store a question category */ protected $cachedcategory = null; + /** @var stdClass the last question_bank_entry seen during the restore. Processed when we get to a question. */ + protected $latestqbe; + + /** @var stdClass the last question_version seen during the restore. Processed when we get to a question. */ + protected $latestversion; + protected function define_structure() { // Check if the backup is a pre 4.0 one. @@ -5046,45 +5067,25 @@ protected function process_question_category($data) { } /** - * Process pre 4.0 question data where in creates the record for version and entry table. + * Set up date to allow restore of questions from pre-4.0 backups. * - * @param array $data the data from the XML file. + * @param stdClass $data the data from the XML file. */ protected function process_question_legacy_data($data) { - global $DB; - - $oldid = $data->id; - // Process question bank entry. - $entrydata = new stdClass(); - $entrydata->questioncategoryid = $data->category; - $userid = $this->get_mappingid('user', $data->createdby); - if ($userid) { - $entrydata->ownerid = $userid; - } else { - if (!$this->task->is_samesite()) { - $entrydata->ownerid = $this->task->get_userid(); - } - } - // The idnumber if it exists also needs to be unique within a category or reset it to null. - if (isset($data->idnumber) && !$DB->record_exists('question_bank_entries', - ['idnumber' => $data->idnumber, 'questioncategoryid' => $data->category])) { - $entrydata->idnumber = $data->idnumber; - } + $this->latestqbe = (object) [ + 'id' => $data->id, + 'questioncategoryid' => $data->category, + 'ownerid' => $data->createdby, + 'idnumber' => $data->idnumber ?? null, + ]; - $newentryid = $DB->insert_record('question_bank_entries', $entrydata); - // Process question versions. - $versiondata = new stdClass(); - $versiondata->questionbankentryid = $newentryid; - $versiondata->version = 1; - // Question id is updated after inserting the question. - $versiondata->questionid = 0; - $versionstatus = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY; - if ((int)$data->hidden === 1) { - $versionstatus = \core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN; - } - $versiondata->status = $versionstatus; - $newversionid = $DB->insert_record('question_versions', $versiondata); - $this->set_mapping('question_version_created', $oldid, $newversionid); + $this->latestversion = (object) [ + 'id' => $data->id, + 'version' => 1, + 'status' => $data->hidden ? + \core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN : + \core_question\local\bank\question_version_status::QUESTION_STATUS_READY, + ]; } /** @@ -5093,37 +5094,9 @@ protected function process_question_legacy_data($data) { * @param array $data the data from the XML file. */ protected function process_question_bank_entry($data) { - global $DB; - - $data = (object)$data; - $oldid = $data->id; - - $questioncreated = $this->get_mappingid('question_category_created', $data->questioncategoryid) ? true : false; - $recordexist = $DB->record_exists('question_bank_entries', ['id' => $data->id, - 'questioncategoryid' => $data->questioncategoryid]); - // Check we have category created. - if (!$questioncreated && $recordexist) { - return self::SKIP_ALL_CHILDREN; - } - - $data->questioncategoryid = $this->get_new_parentid('question_category'); - $userid = $this->get_mappingid('user', $data->ownerid); - if ($userid) { - $data->ownerid = $userid; - } else { - if (!$this->task->is_samesite()) { - $data->ownerid = $this->task->get_userid(); - } - } - - // The idnumber if it exists also needs to be unique within a category or reset it to null. - if (!empty($data->idnumber) && $DB->record_exists('question_bank_entries', - ['idnumber' => $data->idnumber, 'questioncategoryid' => $data->questioncategoryid])) { - unset($data->idnumber); - } - - $newitemid = $DB->insert_record('question_bank_entries', $data); - $this->set_mapping('question_bank_entry', $oldid, $newitemid); + // We can only determine the right way to process this once we get to + // process_question and have more information, so for now just store. + $this->latestqbe = (object) $data; } /** @@ -5132,16 +5105,9 @@ protected function process_question_bank_entry($data) { * @param array $data the data from the XML file. */ protected function process_question_versions($data) { - global $DB; - - $data = (object)$data; - $oldid = $data->id; - - $data->questionbankentryid = $this->get_new_parentid('question_bank_entry'); - // Question id is updated after inserting the question. - $data->questionid = 0; - $newitemid = $DB->insert_record('question_versions', $data); - $this->set_mapping('question_versions', $oldid, $newitemid); + // We can only determine the right way to process this once we get to + // process_question and have more information, so for now just store. + $this->latestversion = (object) $data; } /** @@ -5152,17 +5118,19 @@ protected function process_question_versions($data) { protected function process_question($data) { global $DB; - $data = (object)$data; + $data = (object) $data; $oldid = $data->id; - // Check if the backup is a pre 4.0 one. + // Check we have one mapping for this question. + if (!$questionmapping = $this->get_mapping('question', $oldid)) { + // No mapping = this question doesn't need to be created/mapped. + return; + } + + // Check if this is a pre 4.0 backup, then there will not be a question bank entry + // or question version in the file. So, we need to set up that data ready to be used below. $restoretask = $this->get_task(); if ($restoretask->backup_release_compare('4.0', '<') || $restoretask->backup_version_compare(20220202, '<')) { - // Check we have one mapping for this question. - if (!$questionmapping = $this->get_mapping('question', $oldid)) { - return; // No mapping = this question doesn't need to be created/mapped. - } - // Get the mapped category (cannot use get_new_parentid() because not // all the categories have been created, so it is not always available // Instead we get the mapping for the question->parentitemid because @@ -5206,28 +5174,66 @@ protected function process_question($data) { } } - $newitemid = $DB->insert_record('question', $data); - $this->set_mapping('question', $oldid, $newitemid); - // Also annotate them as question_created, we need - // that later when remapping parents (keeping the old categoryid as parentid). - $parentcatid = $this->get_old_parentid('question_category'); - $this->set_mapping('question_created', $oldid, $newitemid, false, null, $parentcatid); - // Now update the question_versions table with the new question id. we dont need to do that for random qtypes. - $legacyquestiondata = $this->get_mappingid('question_version_created', $oldid) ? true : false; - if ($legacyquestiondata) { - $parentitemid = $this->get_mappingid('question_version_created', $oldid); + // With newitemid = 0, let's create the question. + if (!$questionmapping->newitemid) { + // Now we know we are inserting a question, we may need to insert the questionbankentry. + if (empty($this->latestqbe->newid)) { + $this->latestqbe->oldid = $this->latestqbe->id; + + $this->latestqbe->questioncategoryid = $this->get_new_parentid('question_category'); + $userid = $this->get_mappingid('user', $this->latestqbe->ownerid); + if ($userid) { + $this->latestqbe->ownerid = $userid; + } else { + if (!$this->task->is_samesite()) { + $this->latestqbe->ownerid = $this->task->get_userid(); + } + } + + // The idnumber if it exists also needs to be unique within a category or reset it to null. + if (!empty($this->latestqbe->idnumber) && $DB->record_exists('question_bank_entries', + ['idnumber' => $this->latestqbe->idnumber, 'questioncategoryid' => $this->latestqbe->questioncategoryid])) { + unset($this->latestqbe->idnumber); + } + + $this->latestqbe->newid = $DB->insert_record('question_bank_entries', $this->latestqbe); + $this->set_mapping('question_bank_entry', $this->latestqbe->oldid, $this->latestqbe->newid); + } + + // Now store the question. + $newitemid = $DB->insert_record('question', $data); + $this->set_mapping('question', $oldid, $newitemid); + // Also annotate them as question_created, we need + // that later when remapping parents (keeping the old categoryid as parentid). + $parentcatid = $this->get_old_parentid('question_category'); + $this->set_mapping('question_created', $oldid, $newitemid, false, null, $parentcatid); + + // Also insert this question_version. + $oldqvid = $this->latestversion->id; + $this->latestversion->questionbankentryid = $this->latestqbe->newid; + $this->latestversion->questionid = $newitemid; + $newqvid = $DB->insert_record('question_versions', $this->latestversion); + $this->set_mapping('question_versions', $oldqvid, $newqvid); + } else { - $parentitemid = $this->get_new_parentid('question_versions'); + // By performing this set_mapping() we make get_old/new_parentid() to work for all the + // children elements of the 'question' one (so qtype plugins will know the question they belong to). + $this->set_mapping('question', $oldid, $questionmapping->newitemid); + + // Also create the question_bank_entry and version mappings, if required. + $newquestionversion = $DB->get_record('question_versions', ['questionid' => $questionmapping->newitemid]); + $this->set_mapping('question_versions', $this->latestversion->id, $newquestionversion->id); + if (empty($this->latestqbe->newid)) { + $this->latestqbe->oldid = $this->latestqbe->id; + $this->latestqbe->newid = $newquestionversion->questionbankentryid; + $this->set_mapping('question_bank_entry', $this->latestqbe->oldid, $this->latestqbe->newid); + } } - $version = new stdClass(); - $version->id = $parentitemid; - $version->questionid = $newitemid; - $DB->update_record('question_versions', $version); // Note, we don't restore any question files yet // as far as the CONTEXT_MODULE categories still // haven't their contexts to be restored to - // The {@link restore_create_question_files}, executed in the final step + // The {@see restore_create_question_files}, executed in the final // step will be in charge of restoring all the question files. } diff --git a/backup/moodle2/tests/backup_xml_transformer_test.php b/backup/moodle2/tests/backup_xml_transformer_test.php index 54c8656d71432..5bd42cf0abfbb 100644 --- a/backup/moodle2/tests/backup_xml_transformer_test.php +++ b/backup/moodle2/tests/backup_xml_transformer_test.php @@ -49,7 +49,7 @@ public function setUp(): void { * * @return array */ - public function filephp_links_replace_data_provider() { + public static function filephp_links_replace_data_provider(): array { return array( array('http://test.test/', 'http://test.test/'), array('http://test.test/file.php/1', 'http://test.test/file.php/1'), diff --git a/backup/moodle2/tests/restore_gradebook_structure_step_test.php b/backup/moodle2/tests/restore_gradebook_structure_step_test.php index aec1a12e5a173..21e2f4aa7428d 100644 --- a/backup/moodle2/tests/restore_gradebook_structure_step_test.php +++ b/backup/moodle2/tests/restore_gradebook_structure_step_test.php @@ -37,12 +37,13 @@ class restore_gradebook_structure_step_test extends \advanced_testcase { * * @return array */ - public function rewrite_step_backup_file_for_legacy_freeze_provider() { + public static function rewrite_step_backup_file_for_legacy_freeze_provider(): array { $fixturesdir = realpath(__DIR__ . '/fixtures/rewrite_step_backup_file_for_legacy_freeze/'); $tests = []; $iterator = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator($fixturesdir), - \RecursiveIteratorIterator::LEAVES_ONLY); + new \RecursiveDirectoryIterator($fixturesdir), + \RecursiveIteratorIterator::LEAVES_ONLY, + ); foreach ($iterator as $sourcefile) { $pattern = '/\.test$/'; diff --git a/backup/util/dbops/backup_controller_dbops.class.php b/backup/util/dbops/backup_controller_dbops.class.php index 2bc367fd47614..fcf2f9d3e592b 100644 --- a/backup/util/dbops/backup_controller_dbops.class.php +++ b/backup/util/dbops/backup_controller_dbops.class.php @@ -556,6 +556,7 @@ public static function apply_config_defaults(backup_controller $controller) { 'backup_general_role_assignments' => 'role_assignments', 'backup_general_activities' => 'activities', 'backup_general_blocks' => 'blocks', + 'backup_general_files' => 'files', 'backup_general_filters' => 'filters', 'backup_general_comments' => 'comments', 'backup_general_badges' => 'badges', @@ -566,6 +567,7 @@ public static function apply_config_defaults(backup_controller $controller) { 'backup_general_questionbank' => 'questionbank', 'backup_general_groups' => 'groups', 'backup_general_competencies' => 'competencies', + 'backup_general_customfield' => 'customfield', 'backup_general_contentbankcontent' => 'contentbankcontent', 'backup_general_xapistate' => 'xapistate', 'backup_general_legacyfiles' => 'legacyfiles' @@ -583,6 +585,7 @@ public static function apply_config_defaults(backup_controller $controller) { 'backup_import_questionbank' => 'questionbank', 'backup_import_groups' => 'groups', 'backup_import_competencies' => 'competencies', + 'backup_import_customfield' => 'customfield', 'backup_import_contentbankcontent' => 'contentbankcontent', 'backup_import_legacyfiles' => 'legacyfiles' ); @@ -606,6 +609,7 @@ public static function apply_config_defaults(backup_controller $controller) { 'backup_auto_role_assignments' => 'role_assignments', 'backup_auto_activities' => 'activities', 'backup_auto_blocks' => 'blocks', + 'backup_auto_files' => 'files', 'backup_auto_filters' => 'filters', 'backup_auto_comments' => 'comments', 'backup_auto_badges' => 'badges', @@ -616,6 +620,7 @@ public static function apply_config_defaults(backup_controller $controller) { 'backup_auto_questionbank' => 'questionbank', 'backup_auto_groups' => 'groups', 'backup_auto_competencies' => 'competencies', + 'backup_auto_customfield' => 'customfield', 'backup_auto_contentbankcontent' => 'contentbankcontent', 'backup_auto_xapistate' => 'xapistate', 'backup_auto_legacyfiles' => 'legacyfiles' diff --git a/backup/util/dbops/restore_controller_dbops.class.php b/backup/util/dbops/restore_controller_dbops.class.php index 9184943448555..093be8d01245d 100644 --- a/backup/util/dbops/restore_controller_dbops.class.php +++ b/backup/util/dbops/restore_controller_dbops.class.php @@ -159,6 +159,7 @@ public static function apply_config_defaults(restore_controller $controller) { 'restore_general_questionbank' => 'questionbank', 'restore_general_groups' => 'groups', 'restore_general_competencies' => 'competencies', + 'restore_general_customfield' => 'customfield', 'restore_general_contentbankcontent' => 'contentbankcontent', 'restore_general_xapistate' => 'xapistate', 'restore_general_legacyfiles' => 'legacyfiles' diff --git a/backup/util/dbops/restore_dbops.class.php b/backup/util/dbops/restore_dbops.class.php index 0633fd8567c83..d587e7748ac70 100644 --- a/backup/util/dbops/restore_dbops.class.php +++ b/backup/util/dbops/restore_dbops.class.php @@ -671,8 +671,7 @@ public static function prechek_precheck_qbanks_by_level($restoreid, $courseid, $ $questions = self::restore_get_questions($restoreid, $category->id); // Collect all the questions for this category into memory so we only talk to the DB once. - $questioncache = $DB->get_records_sql_menu('SELECT q.id, - q.stamp + $questioncache = $DB->get_records_sql_menu('SELECT q.stamp, q.id FROM {question} q JOIN {question_versions} qv ON qv.questionid = q.id @@ -683,8 +682,8 @@ public static function prechek_precheck_qbanks_by_level($restoreid, $courseid, $ WHERE qc.id = ?', array($matchcat->id)); foreach ($questions as $question) { - if (isset($questioncache[$question->stamp." ".$question->version])) { - $matchqid = $questioncache[$question->stamp." ".$question->version]; + if (isset($questioncache[$question->stamp])) { + $matchqid = $questioncache[$question->stamp]; } else { $matchqid = false; } diff --git a/backup/util/dbops/tests/restore_dbops_test.php b/backup/util/dbops/tests/restore_dbops_test.php index 389a981436664..cc9cb010bee85 100644 --- a/backup/util/dbops/tests/restore_dbops_test.php +++ b/backup/util/dbops/tests/restore_dbops_test.php @@ -124,8 +124,7 @@ public function test_backup_ids_cached(): void { /** * Data provider for {@link test_precheck_user()} */ - public function precheck_user_provider() { - + public static function precheck_user_provider(): array { $emailmultiplier = [ 'shortmail' => 'normalusername@example.com', 'longmail' => str_repeat('a', 100) // It's not validated, hence any string is ok. @@ -135,7 +134,7 @@ public function precheck_user_provider() { foreach ($emailmultiplier as $emailk => $email) { // Get the related cases. - $cases = $this->precheck_user_cases($email); + $cases = self::precheck_user_cases($email); // Rename them (keys). foreach ($cases as $key => $case) { $providercases[$key . ' - ' . $emailk] = $case; @@ -150,7 +149,7 @@ public function precheck_user_provider() { * * @param string $email */ - private function precheck_user_cases($email) { + private static function precheck_user_cases($email) { global $CFG; $baseuserarr = [ diff --git a/backup/util/helper/restore_questions_parser_processor.class.php b/backup/util/helper/restore_questions_parser_processor.class.php index 129b2f40ab67a..be3d0c28108b7 100644 --- a/backup/util/helper/restore_questions_parser_processor.class.php +++ b/backup/util/helper/restore_questions_parser_processor.class.php @@ -35,22 +35,35 @@ * TODO: Complete phpdocs */ class restore_questions_parser_processor extends grouped_parser_processor { + /** @var string XML path in the questions.xml backup file to question categories. */ + protected const CATEGORY_PATH = '/question_categories/question_category'; - protected $restoreid; - protected $lastcatid; + /** @var string XML path in the questions.xml to question elements within question_category (Moodle 4.0+). */ + protected const QUESTION_SUBPATH = + '/question_bank_entries/question_bank_entry/question_version/question_versions/questions/question'; + + /** @var string XML path in the questions.xml to question elements within question_category (before Moodle 4.0). */ + protected const LEGACY_QUESTION_SUBPATH = '/questions/question'; + + /** @var string identifies the current restore. */ + protected string $restoreid; + + /** @var int during the restore, this tracks the last category we saw. Any questions we see will be in here. */ + protected int $lastcatid; public function __construct($restoreid) { $this->restoreid = $restoreid; $this->lastcatid = 0; - parent::__construct(array()); + parent::__construct(); // Set the paths we are interested on - $this->add_path('/question_categories/question_category'); - $this->add_path('/question_categories/question_category/questions/question'); + $this->add_path(self::CATEGORY_PATH); + $this->add_path(self::CATEGORY_PATH . self::QUESTION_SUBPATH); + $this->add_path(self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH); } protected function dispatch_chunk($data) { // Prepare question_category record - if ($data['path'] == '/question_categories/question_category') { + if ($data['path'] == self::CATEGORY_PATH) { $info = (object)$data['tags']; $itemname = 'question_category'; $itemid = $info->id; @@ -58,7 +71,8 @@ protected function dispatch_chunk($data) { $this->lastcatid = $itemid; // Prepare question record - } else if ($data['path'] == '/question_categories/question_category/questions/question') { + } else if ($data['path'] == self::CATEGORY_PATH . self::QUESTION_SUBPATH || + $data['path'] == self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH) { $info = (object)$data['tags']; $itemname = 'question'; $itemid = $info->id; diff --git a/backup/util/helper/tests/restore_structure_parser_processor_test.php b/backup/util/helper/tests/restore_structure_parser_processor_test.php index 7f74ec7cef146..6852b0eddcc97 100644 --- a/backup/util/helper/tests/restore_structure_parser_processor_test.php +++ b/backup/util/helper/tests/restore_structure_parser_processor_test.php @@ -53,7 +53,7 @@ public function setUp(): void { * * @return array */ - public function process_cdata_data_provider() { + public static function process_cdata_data_provider(): array { return array( array(null, null, true), array("$@NULL@$", null, true), diff --git a/badges/classes/reportbuilder/local/systemreports/badges.php b/badges/classes/reportbuilder/local/systemreports/badges.php index 33a841718a646..bbcf32fd4a41a 100644 --- a/badges/classes/reportbuilder/local/systemreports/badges.php +++ b/badges/classes/reportbuilder/local/systemreports/badges.php @@ -77,8 +77,8 @@ protected function initialise(): void { $this->add_filters(); $this->add_actions(); - // Set initial sorting by name. $this->set_initial_sort_column('badge:namewithlink', SORT_ASC); + $this->set_default_no_results_notice(new lang_string('nomatchingbadges', 'core_badges')); // Set if report can be downloaded. $this->set_downloadable(false); @@ -157,13 +157,12 @@ public function add_columns(badge $badgeentity): void { * unique identifier */ protected function add_filters(): void { - $filters = [ + $this->add_filters_from_entities([ 'badge:name', 'badge:version', 'badge:status', 'badge:expiry', - ]; - $this->add_filters_from_entities($filters); + ]); } /** diff --git a/badges/classes/reportbuilder/local/systemreports/course_badges.php b/badges/classes/reportbuilder/local/systemreports/course_badges.php index 3bb42aa68f651..6c56c789379d2 100644 --- a/badges/classes/reportbuilder/local/systemreports/course_badges.php +++ b/badges/classes/reportbuilder/local/systemreports/course_badges.php @@ -18,8 +18,10 @@ namespace core_badges\reportbuilder\local\systemreports; +use core\context\system; use core_badges\reportbuilder\local\entities\badge; use core_badges\reportbuilder\local\entities\badge_issued; +use core_reportbuilder\local\helpers\database; use core_reportbuilder\system_report; use lang_string; use moodle_url; @@ -52,11 +54,18 @@ protected function initialise(): void { $this->set_main_table('badge', $entityalias); $this->add_entity($badgeentity); - $type = $this->get_parameter('type', 0, PARAM_INT); - $courseid = $this->get_parameter('courseid', 0, PARAM_INT); + $paramtype = database::generate_param_name(); + $context = $this->get_context(); + if ($context instanceof system) { + $type = BADGE_TYPE_SITE; + $this->add_base_condition_sql("{$entityalias}.type = :$paramtype", [$paramtype => $type]); + } else { + $type = BADGE_TYPE_COURSE; + $paramcourseid = database::generate_param_name(); + $this->add_base_condition_sql("{$entityalias}.type = :$paramtype AND {$entityalias}.courseid = :$paramcourseid", + [$paramtype => $type, $paramcourseid => $context->instanceid]); + } - $this->add_base_condition_simple('type', $type); - $this->add_base_condition_simple('courseid', $courseid); $this->add_base_condition_sql("({$entityalias}.status = " . BADGE_STATUS_ACTIVE . " OR {$entityalias}.status = " . BADGE_STATUS_ACTIVE_LOCKED . ")"); @@ -70,9 +79,12 @@ protected function initialise(): void { $this->add_base_fields("{$badgeissuedalias}.uniquehash"); // Now we can call our helper methods to add the content we want to include in the report. - $this->add_columns($badgeissuedalias); + $this->add_columns(); $this->add_filters(); + $this->set_initial_sort_column('badge:name', SORT_ASC); + $this->set_default_no_results_notice(new lang_string('nomatchingbadges', 'core_badges')); + // Set if report can be downloaded. $this->set_downloadable(false); } @@ -91,18 +103,17 @@ protected function can_view(): bool { * * They are provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier. If custom columns are needed just for this report, they can be defined here. - * - * @param string $badgeissuedalias */ - public function add_columns(string $badgeissuedalias): void { - $columns = [ + protected function add_columns(): void { + $badgeissuedalias = $this->get_entity('badge_issued')->get_table_alias('badge_issued'); + + $this->add_columns_from_entities([ 'badge:image', 'badge:name', 'badge:description', 'badge:criteria', 'badge_issued:issued', - ]; - $this->add_columns_from_entities($columns); + ]); $this->get_column('badge_issued:issued') ->set_title(new lang_string('awardedtoyou', 'core_badges')) @@ -119,8 +130,6 @@ public function add_columns(string $badgeissuedalias): void { $icon = new pix_icon('i/valid', get_string('dateearned', 'badges', $date)); return $OUTPUT->action_icon($badgeurl, $icon, null, null, true); }); - - $this->set_initial_sort_column('badge:name', SORT_ASC); } /** @@ -130,11 +139,9 @@ public function add_columns(string $badgeissuedalias): void { * unique identifier */ protected function add_filters(): void { - $filters = [ + $this->add_filters_from_entities([ 'badge:name', 'badge_issued:issued', - ]; - - $this->add_filters_from_entities($filters); + ]); } } diff --git a/badges/classes/reportbuilder/local/systemreports/recipients.php b/badges/classes/reportbuilder/local/systemreports/recipients.php index b2b2fbb2d0454..22d22659fed27 100644 --- a/badges/classes/reportbuilder/local/systemreports/recipients.php +++ b/badges/classes/reportbuilder/local/systemreports/recipients.php @@ -62,6 +62,9 @@ protected function initialise(): void { $this->add_filters(); $this->add_actions(); + $this->set_initial_sort_column('badge_issued:issued', SORT_DESC); + $this->set_default_no_results_notice(new lang_string('nomatchingawards', 'core_badges')); + // Set if report can be downloaded. $this->set_downloadable(false); } @@ -72,7 +75,9 @@ protected function initialise(): void { * @return bool */ protected function can_view(): bool { - return has_capability('moodle/badges:viewawarded', $this->get_context()); + $badgeid = $this->get_parameter('badgeid', 0, PARAM_INT); + $badge = new \core_badges\badge($badgeid); + return has_capability('moodle/badges:viewawarded', $badge->get_context()); } /** @@ -81,14 +86,11 @@ protected function can_view(): bool { * They are provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier. If custom columns are needed just for this report, they can be defined here. */ - public function add_columns(): void { - $columns = [ + protected function add_columns(): void { + $this->add_columns_from_entities([ 'user:fullnamewithlink', 'badge_issued:issued', - ]; - - $this->add_columns_from_entities($columns); - $this->set_initial_sort_column('badge_issued:issued', SORT_DESC); + ]); } /** @@ -98,12 +100,10 @@ public function add_columns(): void { * unique identifier */ protected function add_filters(): void { - $filters = [ + $this->add_filters_from_entities([ 'user:fullname', 'badge_issued:issued', - ]; - - $this->add_filters_from_entities($filters); + ]); } /** diff --git a/badges/index.php b/badges/index.php index ced1b04e84420..ed77aaa574f7e 100644 --- a/badges/index.php +++ b/badges/index.php @@ -89,6 +89,8 @@ } $PAGE->set_title($hdr); + +/** @var core_badges_renderer $output */ $output = $PAGE->get_renderer('core', 'badges'); if ($delete || $archive) { @@ -159,8 +161,6 @@ } $report = system_report_factory::create(badges::class, $PAGE->context); -$report->set_default_no_results_notice(new lang_string('nobadges', 'badges')); - echo $report->output(); echo $OUTPUT->footer(); diff --git a/badges/recipients.php b/badges/recipients.php index 6a1120a19db97..12cac6eb3db6a 100644 --- a/badges/recipients.php +++ b/badges/recipients.php @@ -24,6 +24,9 @@ * @author Yuliya Bozhko */ +use core_badges\reportbuilder\local\systemreports\recipients; +use core_reportbuilder\system_report_factory; + require_once(__DIR__ . '/../config.php'); require_once($CFG->libdir . '/badgeslib.php'); @@ -63,6 +66,7 @@ $PAGE->set_title($badge->name); $PAGE->navbar->add($badge->name); +/** @var core_badges_renderer $output */ $output = $PAGE->get_renderer('core', 'badges'); echo $output->header(); @@ -73,9 +77,7 @@ echo $OUTPUT->heading(print_badge_image($badge, $context, 'small') . ' ' . $badge->name); echo $output->print_badge_status_box($badge); -$report = \core_reportbuilder\system_report_factory::create(\core_badges\reportbuilder\local\systemreports\recipients::class, - $PAGE->context, '', '', 0, ['badgeid' => $badge->id]); -$report->set_default_no_results_notice(new lang_string('noawards', 'badges')); +$report = system_report_factory::create(recipients::class, $PAGE->context, '', '', 0, ['badgeid' => $badge->id]); echo $report->output(); echo $output->footer(); diff --git a/badges/tests/badgeslib_test.php b/badges/tests/badgeslib_test.php index b5424a07bc4a6..ae404ed1669e9 100644 --- a/badges/tests/badgeslib_test.php +++ b/badges/tests/badgeslib_test.php @@ -482,7 +482,7 @@ public function test_badges_get_user_badges(): void { } - public function data_for_message_from_template() { + public static function data_for_message_from_template(): array { return array( array( 'This is a message with no variables', @@ -1242,7 +1242,7 @@ public function test_save_backpack_credentials(bool $addbackpack = true, ?string * * @return array */ - public function save_backpack_credentials_provider(): array { + public static function save_backpack_credentials_provider(): array { return [ 'Empty fields' => [ false, @@ -1310,7 +1310,7 @@ public function test_badges_save_external_backpack(array $data, bool $adduser, b * * @return array */ - public function badges_save_external_backpack_provider() { + public static function badges_save_external_backpack_provider(): array { $data = [ 'apiversion' => 2, 'backpackapiurl' => 'https://api.ca.badgr.io/v2', @@ -1427,7 +1427,7 @@ public function test_badges_create_site_backpack($isadmin, $updatetest): void { /** * Provider for test_badges_(create/update)_site_backpack */ - public function badges_create_site_backpack_provider() { + public static function badges_create_site_backpack_provider(): array { return [ "Test as admin user - creation test" => [true, true], "Test as admin user - update test" => [true, false], @@ -1598,7 +1598,7 @@ public function test_badges_get_site_primary_backpack($withauth): void { * * @return array */ - public function badges_get_site_primary_backpack_provider() { + public static function badges_get_site_primary_backpack_provider(): array { return [ "Test with auth details" => [true], "Test without auth details" => [false], @@ -1653,7 +1653,7 @@ public function test_badges_change_sortorder_backpacks(int $backpacktomove, int * * @return array */ - public function badges_change_sortorder_backpacks_provider(): array { + public static function badges_change_sortorder_backpacks_provider(): array { return [ "Test up" => [ 'backpacktomove' => 1, @@ -1707,7 +1707,7 @@ public function test_badges_generate_badgr_open_url($type, $expected): void { * Data provider for test_badges_generate_badgr_open_url * @return array */ - public function badgr_open_url_generator() { + public static function badgr_open_url_generator(): array { return [ 'Badgr Assertion URL test' => [ OPEN_BADGES_V2_TYPE_ASSERTION, "https://api.ca.badgr.io/public/assertions/123455" @@ -1754,7 +1754,7 @@ public function test_badges_external_get_mapping($internalid, $externalid, $expe * * @return array */ - public function badges_external_get_mapping_provider() { + public static function badges_external_get_mapping_provider(): array { return [ "Get the site backpack value" => [ 1234, 4321, 'id', 'sitebackpackid' diff --git a/badges/tests/behat/add_badge.feature b/badges/tests/behat/add_badge.feature index 561d661242da6..5375a12a05f10 100644 --- a/badges/tests/behat/add_badge.feature +++ b/badges/tests/behat/add_badge.feature @@ -17,7 +17,7 @@ Feature: Add badges to the system And I add the "Navigation" block if not present And I click on "Site pages" "list_item" in the "Navigation" "block" Given I click on "Site badges" "link" in the "Navigation" "block" - Then I should see "There are currently no badges available for users to earn." + Then I should see "There are no matching badges available for users to earn." @javascript @_file_upload Scenario: Add a badge @@ -51,7 +51,7 @@ Feature: Add badges to the system And I should see "Math" And I should see "Physics" And I navigate to "Badges > Manage badges" in site administration - And I should not see "There are currently no badges available for users to earn." + And I should not see "There are no matching badges available for users to earn." @javascript @_file_upload Scenario: Add a badge related @@ -163,7 +163,7 @@ Feature: Add badges to the system And I should see "Alignments (0)" And I should not see "Create badge" And I navigate to "Badges > Manage badges" in site administration - And I should not see "There are currently no badges available for users to earn." + And I should not see "There are no matching badges available for users to earn." # See buttons from the "Site badges" page. And I am on homepage When I click on "Site pages" "list_item" in the "Navigation" "block" diff --git a/badges/tests/behat/award_badge.feature b/badges/tests/behat/award_badge.feature index 2ce8fcbfe37b9..04daec9771130 100644 --- a/badges/tests/behat/award_badge.feature +++ b/badges/tests/behat/award_badge.feature @@ -104,7 +104,7 @@ Feature: Award badges And I add the "Navigation" block if not present And I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Site badges" "link" in the "Navigation" "block" - Then I should see "There are currently no badges available for users to earn." + Then I should see "There are no matching badges available for users to earn." And I should not see "Manage badges" And I should not see "Add a new badge" @@ -138,7 +138,6 @@ Feature: Award badges And I press "Update profile" And I follow "Profile" in the user menu Then I should see "Profile Badge" - And I should not see "There are currently no badges available for users to earn." @javascript Scenario: Award site badge diff --git a/badges/tests/behat/delete_awarded_badge.feature b/badges/tests/behat/delete_awarded_badge.feature index 39ae200b387de..9d4b7c31a903a 100644 --- a/badges/tests/behat/delete_awarded_badge.feature +++ b/badges/tests/behat/delete_awarded_badge.feature @@ -44,8 +44,7 @@ Feature: Delete course badge already awarded # Navigate to Badges page to confirm that no badges exist, hence, Manage badges would not exist And I navigate to "Badges" in current page administration # Confirm that badges are sucessfully deleted - And I should see "There are currently no badges available for users to earn." - + And I should see "There are no matching badges available for users to earn" Examples: | badgename | deleteoption | visibility | | Badge 1 | Delete and keep existing issued badges | should | diff --git a/badges/tests/behat/manage_badges.feature b/badges/tests/behat/manage_badges.feature index ba300aea1ea19..0a5065f999947 100644 --- a/badges/tests/behat/manage_badges.feature +++ b/badges/tests/behat/manage_badges.feature @@ -45,12 +45,12 @@ Feature: Manage badges And I navigate to "Badges > Manage badges" in site administration And I press "Delete" action in the "Badge #1" report row And I press "Delete and remove existing issued badges" - Then I should see "There are currently no badges available for users to earn" + Then I should see "There are no matching badges available for users to earn." Scenario Outline: Filter managed badges Given the following "core_badges > Badges" exist: - | name | status | version | - | Badge #2 | 1 | 2.0 | + | name | status | version | image | + | Badge #2 | 1 | 2.0 | badges/tests/behat/badge.png | And I log in as "admin" When I navigate to "Badges > Manage badges" in site administration And I click on "Filters" "button" diff --git a/badges/tests/behat/nobadge_navigation.feature b/badges/tests/behat/nobadge_navigation.feature index 714062d974da9..b8ada2fdf5a60 100644 --- a/badges/tests/behat/nobadge_navigation.feature +++ b/badges/tests/behat/nobadge_navigation.feature @@ -4,7 +4,7 @@ Feature: Manage badges is not shown when there are no existing badges. Scenario: Check navigation at site level with no badges Given I log in as "admin" When I navigate to "Badges > Manage badges" in site administration - And I should see "There are currently no badges available for users to earn" + And I should see "There are no matching badges available for users to earn." Then "Manage badges" "button" should not exist Scenario: Check navigation at course level with no badges @@ -99,7 +99,7 @@ Feature: Manage badges is not shown when there are no existing badges. And I follow "Badges" And "Manage badges" "button" should not exist And "Add a new badge" "button" should not exist - And I should not see "There are currently no badges available for users to earn." - And the following should exist in the "reportbuilder-table" table: + And I should not see "There are no matching badges available for users to earn" + And the following should exist in the "Course badges" table: | Name | Description | Criteria | | Testing course badge | Testing course badge description | Awarded by: Teacher | diff --git a/badges/tests/generator/behat_core_badges_generator.php b/badges/tests/generator/behat_core_badges_generator.php index eeb13fb3e564b..349c9f4ef6ade 100644 --- a/badges/tests/generator/behat_core_badges_generator.php +++ b/badges/tests/generator/behat_core_badges_generator.php @@ -37,6 +37,7 @@ protected function get_creatable_entities(): array { 'datagenerator' => 'badge', 'required' => [ 'name', + 'image', ], 'switchids' => [ 'course' => 'courseid', diff --git a/badges/tests/output/manage_badge_action_bar_test.php b/badges/tests/output/manage_badge_action_bar_test.php index 065ef11007ff8..c80a0600529e8 100644 --- a/badges/tests/output/manage_badge_action_bar_test.php +++ b/badges/tests/output/manage_badge_action_bar_test.php @@ -36,7 +36,7 @@ class manage_badge_action_bar_test extends \advanced_testcase { * * @return array */ - public function generate_badge_navigation_provider(): array { + public static function generate_badge_navigation_provider(): array { return [ "Test tertiary nav as an editing teacher" => [ "editingteacher", [ diff --git a/badges/tests/reportbuilder/datasource/users_test.php b/badges/tests/reportbuilder/datasource/users_test.php index b83f36f60f1ca..96da2707cbce8 100644 --- a/badges/tests/reportbuilder/datasource/users_test.php +++ b/badges/tests/reportbuilder/datasource/users_test.php @@ -211,7 +211,7 @@ public function test_datasource_non_default_columns(): void { * * @return array[] */ - public function datasource_filters_provider(): array { + public static function datasource_filters_provider(): array { return [ // User. 'Filter user fullname' => ['user:fullname', [ diff --git a/badges/view.php b/badges/view.php index ff734c9eaf7ab..7582384d97149 100644 --- a/badges/view.php +++ b/badges/view.php @@ -24,6 +24,9 @@ * @author Yuliya Bozhko */ +use core_badges\reportbuilder\local\systemreports\course_badges; +use core_reportbuilder\system_report_factory; + require_once(__DIR__ . '/../config.php'); require_once($CFG->libdir . '/badgeslib.php'); @@ -67,6 +70,8 @@ require_capability('moodle/badges:viewbadges', $PAGE->context); $PAGE->set_title($title); + +/** @var core_badges_renderer $output */ $output = $PAGE->get_renderer('core', 'badges'); // Display "Manage badges" button to users with proper capabilities. @@ -98,9 +103,7 @@ echo $OUTPUT->box(get_string('error:notifycoursedate', 'badges'), 'generalbox notifyproblem'); } -$report = \core_reportbuilder\system_report_factory::create(\core_badges\reportbuilder\local\systemreports\course_badges::class, - $PAGE->context, '', '', 0, ['type' => $type, 'courseid' => $courseid]); -$report->set_default_no_results_notice(new lang_string('nobadges', 'badges')); +$report = system_report_factory::create(course_badges::class, $PAGE->context); echo $report->output(); // Trigger event, badge listing viewed. diff --git a/blocks/accessreview/styles.css b/blocks/accessreview/styles.css index 72b12c337f152..fa5b4f787f48f 100644 --- a/blocks/accessreview/styles.css +++ b/blocks/accessreview/styles.css @@ -1,22 +1,22 @@ .block_accessreview_success, .block_accessreview.block_accessreview_success.hasinfo { color: #1e451e; - background: #d7e6d7; - border-color: #c8ddc8; + background: #eff5ef; + box-shadow: 0 0 2px 2px #619a61; } .block_accessreview_danger, .block_accessreview.block_accessreview_danger.hasinfo { color: #6e211e; - background: #f6d9d8; - border-color: #f3c9c8; + background: #fdf7f7; + box-shadow: 0 0 2px 2px #da6960; } .block_accessreview_warning, .block_accessreview.block_accessreview_warning.hasinfo { - color: #7d5a29; - background: #fcefdc; - border-color: #fbe8cd; + color: #694b21; + background: #fdf2e3; + box-shadow: 0 0 2px 2px #c97a0e; } .block_accessreview_table { diff --git a/blocks/myoverview/lang/en/block_myoverview.php b/blocks/myoverview/lang/en/block_myoverview.php index 08be768497b6b..62bc0c395d481 100644 --- a/blocks/myoverview/lang/en/block_myoverview.php +++ b/blocks/myoverview/lang/en/block_myoverview.php @@ -88,7 +88,7 @@ $string['sortbyshortname'] = 'Sort by short name'; $string['privacy:request:preference:set'] = 'The value of the setting \'{$a->name}\' was \'{$a->value}\''; $string['viewquickstart'] = 'View Quickstart guide'; -$string['zero_default_title'] = 'You\'re not enrolled in any course'; +$string['zero_default_title'] = 'You\'re not enrolled in any courses.'; $string['zero_default_intro'] = 'Once you\'re enrolled in a course, it will appear here.'; $string['zero_request_title'] = 'Request your first course'; $string['zero_request_intro'] = 'Need help getting started? Check out the Moodle documentation or take your first steps with our Quickstart guide.'; diff --git a/blocks/myoverview/tests/behat/block_myoverview_pagination.feature b/blocks/myoverview/tests/behat/block_myoverview_pagination.feature index bc4f23c734b0e..54ba2af40f274 100644 --- a/blocks/myoverview/tests/behat/block_myoverview_pagination.feature +++ b/blocks/myoverview/tests/behat/block_myoverview_pagination.feature @@ -35,7 +35,7 @@ Feature: My overview block pagination Scenario: The pagination controls should be hidden if I am not enrolled in any courses When I am on the "My courses" page logged in as "student1" - Then I should see "You're not enrolled in any course" in the "Course overview" "block" + Then I should see "You're not enrolled in any courses." in the "Course overview" "block" And I should not see "Show" in the "Course overview" "block" And ".block_myoverview .dropdown-menu.show" "css_element" should not be visible And ".block_myoverview [data-control='next']" "css_element" should not be visible diff --git a/blocks/myoverview/tests/behat/block_myoverview_search.feature b/blocks/myoverview/tests/behat/block_myoverview_search.feature index 8a0754d7d2ce0..5e210f277c0d3 100644 --- a/blocks/myoverview/tests/behat/block_myoverview_search.feature +++ b/blocks/myoverview/tests/behat/block_myoverview_search.feature @@ -40,7 +40,7 @@ Feature: My overview block searching Scenario: There is no search if I am not enrolled in any course When I am on the "My courses" page logged in as "student2" - Then I should see "You're not enrolled in any course" in the "Course overview" "block" + Then I should see "You're not enrolled in any courses." in the "Course overview" "block" And "Search courses" "field" should not exist in the "Course overview" "block" And I log out diff --git a/blocks/myoverview/tests/behat/block_myoverview_zerostate.feature b/blocks/myoverview/tests/behat/block_myoverview_zerostate.feature index 79cece88b26fc..19856e27d5486 100644 --- a/blocks/myoverview/tests/behat/block_myoverview_zerostate.feature +++ b/blocks/myoverview/tests/behat/block_myoverview_zerostate.feature @@ -15,7 +15,7 @@ Feature: Zero state on my overview block Scenario: Users with no permissions don't see any CTA Given I am on the "My courses" page logged in as "user" - When I should see "You're not enrolled in any course" + When I should see "You're not enrolled in any courses." Then I should see "Once you're enrolled in a course, it will appear here." And I should not see "Create course" And I should not see "Request a course" @@ -46,7 +46,7 @@ Feature: Zero state on my overview block | fullname | Course 1 | | shortname | C1 | When I am on the "My courses" page logged in as "manager" - Then I should see "You're not enrolled in any course" + Then I should see "You're not enrolled in any courses." Then I should see "Once you're enrolled in a course, it will appear here." And "Manage courses" "button" should exist And "Create course" "button" should exist @@ -64,7 +64,7 @@ Feature: Zero state on my overview block | fullname | Course 1 | | shortname | C1 | When I am on the "My courses" page logged in as "manager" - Then I should see "You're not enrolled in any course" + Then I should see "You're not enrolled in any courses." Then I should not see "To view all courses on this sie, go to Manage courses" And "Manage courses" "button" should not exist And "Create course" "button" should exist diff --git a/blocks/myoverview/tests/privacy/provider_test.php b/blocks/myoverview/tests/privacy/provider_test.php index 72b7eb1784dd5..482ef8c69a083 100644 --- a/blocks/myoverview/tests/privacy/provider_test.php +++ b/blocks/myoverview/tests/privacy/provider_test.php @@ -71,7 +71,7 @@ public function test_export_user_preferences($type, $value, $expected): void { * * @return array Array of valid user preferences. */ - public function user_preference_provider() { + public static function user_preference_provider(): array { return array( array('block_myoverview_user_sort_preference', 'lastaccessed', ''), array('block_myoverview_user_sort_preference', 'title', ''), diff --git a/blocks/rss_client/editfeed.php b/blocks/rss_client/editfeed.php index 97e1706fa63fb..545d422f12797 100644 --- a/blocks/rss_client/editfeed.php +++ b/blocks/rss_client/editfeed.php @@ -180,7 +180,18 @@ public static function autodiscover_feed_url($url){ if ($rssid) { $isadding = false; - $rssrecord = $DB->get_record('block_rss_client', array('id' => $rssid), '*', MUST_EXIST); + + if ($managesharedfeeds) { + $select = 'id = :id AND (userid = :userid OR shared = 1)'; + } else { + $select = 'id = :id AND userid = :userid'; + } + + $rssrecord = $DB->get_record_select('block_rss_client', $select, [ + 'id' => $rssid, + 'userid' => $USER->id, + ], '*', MUST_EXIST); + } else { $isadding = true; $rssrecord = new stdClass; diff --git a/blocks/rss_client/managefeeds.php b/blocks/rss_client/managefeeds.php index d216c3c9d50e4..a7307714f05d9 100644 --- a/blocks/rss_client/managefeeds.php +++ b/blocks/rss_client/managefeeds.php @@ -61,20 +61,27 @@ $baseurl = new moodle_url('/blocks/rss_client/managefeeds.php', $urlparams); $PAGE->set_url($baseurl); +if ($managesharedfeeds) { + $select = '(userid = :userid OR shared = 1)'; +} else { + $select = 'userid = :userid'; +} + // Process any actions if ($deleterssid && confirm_sesskey()) { - $DB->delete_records('block_rss_client', array('id'=>$deleterssid)); + + $deleterssid = $DB->get_field_select('block_rss_client', 'id', "id = :id AND {$select}", [ + 'id' => $deleterssid, + 'userid' => $USER->id + ], MUST_EXIST); + + $DB->delete_records('block_rss_client', ['id' => $deleterssid]); redirect($PAGE->url, get_string('feeddeleted', 'block_rss_client')); } // Display the list of feeds. -if ($managesharedfeeds) { - $select = '(userid = ' . $USER->id . ' OR shared = 1)'; -} else { - $select = 'userid = ' . $USER->id; -} -$feeds = $DB->get_records_select('block_rss_client', $select, null, $DB->sql_order_by_text('title')); +$feeds = $DB->get_records_select('block_rss_client', $select, ['userid' => $USER->id], $DB->sql_order_by_text('title')); $strmanage = get_string('managefeeds', 'block_rss_client'); diff --git a/blocks/rss_client/tests/cron_test.php b/blocks/rss_client/tests/cron_test.php index 21379fb617ac3..9cdaeb1d2c41d 100644 --- a/blocks/rss_client/tests/cron_test.php +++ b/blocks/rss_client/tests/cron_test.php @@ -67,7 +67,7 @@ public function test_skip(): void { * * @return array */ - public function skip_time_increase_provider(): array { + public static function skip_time_increase_provider(): array { return [ 'Never failed' => [ 'skiptime' => 0, diff --git a/blocks/social_activities/block_social_activities.php b/blocks/social_activities/block_social_activities.php index b2dbe40c3451e..5e369450d7c30 100644 --- a/blocks/social_activities/block_social_activities.php +++ b/blocks/social_activities/block_social_activities.php @@ -133,7 +133,7 @@ function get_content() { } if ($ismoving) { - $this->content->icons[] = ' ' . $OUTPUT->pix_icon('t/move', get_string('move')); + $this->content->icons[] = $OUTPUT->pix_icon('t/move', get_string('move'), 'moodle', ['class' => 'pl-1']); $cancelurl = new moodle_url('/course/mod.php', array('cancelcopy' => 'true', 'sesskey' => sesskey())); $this->content->items[] = $USER->activitycopyname . ' (' . $strcancel . ')'; } diff --git a/blocks/starredcourses/templates/view.mustache b/blocks/starredcourses/templates/view.mustache index a41688ec391f3..6be3b28539d04 100644 --- a/blocks/starredcourses/templates/view.mustache +++ b/blocks/starredcourses/templates/view.mustache @@ -29,7 +29,7 @@ data-nocoursesimg="{{nocoursesimg}}">
    -
    +
    {{> core_course/placeholder-course }}
    {{> core_course/placeholder-course }}
    {{> core_course/placeholder-course }}
    diff --git a/blocks/timeline/tests/privacy/provider_test.php b/blocks/timeline/tests/privacy/provider_test.php index b6f404c9a0704..bfced494ccf67 100644 --- a/blocks/timeline/tests/privacy/provider_test.php +++ b/blocks/timeline/tests/privacy/provider_test.php @@ -76,7 +76,7 @@ public function test_export_user_preferences($type, $value, $expected): void { * * @return array Array of valid user preferences. */ - public function user_preference_provider() { + public static function user_preference_provider(): array { return array( array('block_timeline_user_sort_preference', 'sortbydates', ''), array('block_timeline_user_sort_preference', 'sortbycourses', ''), diff --git a/blog/tests/reportbuilder/datasource/blogs_test.php b/blog/tests/reportbuilder/datasource/blogs_test.php index 3db23840c8ce2..b823f40894ca8 100644 --- a/blog/tests/reportbuilder/datasource/blogs_test.php +++ b/blog/tests/reportbuilder/datasource/blogs_test.php @@ -175,7 +175,7 @@ public function test_datasource_non_default_columns(): void { * * @return array[] */ - public function datasource_filters_provider(): array { + public static function datasource_filters_provider(): array { return [ 'Filter title' => ['subject', 'Cool', 'blog:title', [ 'blog:title_operator' => text::CONTAINS, diff --git a/cache/stores/redis/lang/en/cachestore_redis.php b/cache/stores/redis/lang/en/cachestore_redis.php index c1569b24a7fd2..f7674800d7490 100644 --- a/cache/stores/redis/lang/en/cachestore_redis.php +++ b/cache/stores/redis/lang/en/cachestore_redis.php @@ -44,8 +44,8 @@ $string['prefixinvalid'] = 'Invalid prefix. You can only use a-z A-Z 0-9-_.'; $string['privacy:metadata:redis'] = 'The Redis cachestore plugin stores data briefly as part of its caching functionality. This data is stored on an Redis server where data is regularly removed.'; $string['privacy:metadata:redis:data'] = 'The various data stored in the cache'; -$string['serializer_igbinary'] = 'The igbinary serializer.'; -$string['serializer_php'] = 'The default PHP serializer.'; +$string['serializer_igbinary'] = 'Igbinary serializer'; +$string['serializer_php'] = 'Default PHP serializer'; $string['server'] = 'Server(s)'; $string['server_help'] = 'Redis server to use for testing. diff --git a/cache/stores/redis/lib.php b/cache/stores/redis/lib.php index 7e3145324da7e..b5b15c217a333 100644 --- a/cache/stores/redis/lib.php +++ b/cache/stores/redis/lib.php @@ -63,6 +63,9 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_ */ const TTL_EXPIRE_BATCH = 10000; + /** @var int The number of seconds to wait for a connection or response from the Redis server. */ + const CONNECTION_TIMEOUT = 10; + /** * Name of this store. * @@ -266,10 +269,26 @@ protected function new_redis(array $configuration): Redis|RedisCluster|null { try { // Create a $redis object of a RedisCluster or Redis class. if ($clustermode) { - $redis = new RedisCluster(null, $trimmedservers, 1, 1, true, $password, !empty($opts) ? $opts : null); + $redis = new RedisCluster( + null, + $trimmedservers, + self::CONNECTION_TIMEOUT, // Timeout. + self::CONNECTION_TIMEOUT, // Read timeout. + true, + $password, + !empty($opts) ? $opts : null, + ); } else { $redis = new Redis(); - $redis->connect($server, $port, 1, null, 100, 1, $opts); + $redis->connect( + $server, + $port, + self::CONNECTION_TIMEOUT, // Timeout. + null, + 100, // Retry interval. + self::CONNECTION_TIMEOUT, // Read timeout. + $opts, + ); if (!empty($password)) { $redis->auth($password); } diff --git a/cache/stores/redis/tests/compressor_test.php b/cache/stores/redis/tests/compressor_test.php index 6df84410465b8..720f3ca97089d 100644 --- a/cache/stores/redis/tests/compressor_test.php +++ b/cache/stores/redis/tests/compressor_test.php @@ -105,7 +105,7 @@ public function test_it_can_miss_some(): void { * * @return array */ - public function provider_for_test_it_works_with_different_types() { + public static function provider_for_test_it_works_with_different_types(): array { $object = new \stdClass(); $object->field = 'value'; @@ -147,7 +147,7 @@ public function test_it_works_with_different_types($key, $value): void { public function test_it_works_with_different_types_for_many(): void { $store = $this->create_store(cachestore_redis::COMPRESSOR_PHP_GZIP, \Redis::SERIALIZER_PHP); - $provider = $this->provider_for_test_it_works_with_different_types(); + $provider = self::provider_for_test_it_works_with_different_types(); $keys = []; $values = []; $expected = []; @@ -166,7 +166,7 @@ public function test_it_works_with_different_types_for_many(): void { * * @return array */ - public function provider_for_tests_setget() { + public static function provider_for_tests_setget(): array { if (!cachestore_redis::are_requirements_met()) { // Even though we skip all tests in this case, this provider can still show warnings about non-existing class. return []; diff --git a/calendar/tests/action_event_test.php b/calendar/tests/action_event_test.php index 55d4c5cd0e42a..b73ed20f86784 100644 --- a/calendar/tests/action_event_test.php +++ b/calendar/tests/action_event_test.php @@ -45,7 +45,7 @@ class action_event_test extends \advanced_testcase { /** * Test event class getters. * - * @dataProvider getters_testcases() + * @dataProvider getters_testcases * @param array $constructorparams Associative array of constructor parameters. */ public function test_getters($constructorparams): void { @@ -64,7 +64,7 @@ public function test_getters($constructorparams): void { /** * Test cases for getters test. */ - public function getters_testcases() { + public static function getters_testcases(): array { return [ 'Dataset 1' => [ 'constructorparams' => [ diff --git a/calendar/tests/action_test.php b/calendar/tests/action_test.php index 4ecb81378daa4..ea84559f63b38 100644 --- a/calendar/tests/action_test.php +++ b/calendar/tests/action_test.php @@ -29,7 +29,7 @@ class action_test extends \advanced_testcase { /** * Test action class getters. * - * @dataProvider getters_testcases() + * @dataProvider getters_testcases * @param array $constructorparams Associative array of constructor parameters. */ public function test_getters($constructorparams): void { @@ -52,7 +52,7 @@ public function test_getters($constructorparams): void { /** * Test cases for getters test. */ - public function getters_testcases() { + public static function getters_testcases(): array { return [ 'Dataset 1' => [ 'constructorparams' => [ diff --git a/calendar/tests/calendar_event_exporter_test.php b/calendar/tests/calendar_event_exporter_test.php index 5b9eb1bed0f8c..53496c8830465 100644 --- a/calendar/tests/calendar_event_exporter_test.php +++ b/calendar/tests/calendar_event_exporter_test.php @@ -34,7 +34,7 @@ class calendar_event_exporter_test extends \advanced_testcase { * Data provider for the timestamp min limit test case to confirm * that the minimum time limit is set correctly on the boundary cases. */ - public function get_timestamp_min_limit_test_cases() { + public static function get_timestamp_min_limit_test_cases(): array { $now = time(); $todaymidnight = usergetmidnight($now); $tomorrowmidnight = $todaymidnight + DAYSECS; @@ -70,7 +70,7 @@ public function get_timestamp_min_limit_test_cases() { } /** - * @dataProvider get_timestamp_min_limit_test_cases() + * @dataProvider get_timestamp_min_limit_test_cases */ public function test_get_timestamp_min_limit($starttime, $min, $expected): void { $class = calendar_event_exporter::class; @@ -90,7 +90,7 @@ public function test_get_timestamp_min_limit($starttime, $min, $expected): void * Data provider for the timestamp max limit test case to confirm * that the maximum time limit is set correctly on the boundary cases. */ - public function get_timestamp_max_limit_test_cases() { + public static function get_timestamp_max_limit_test_cases(): array { $now = time(); $todaymidnight = usergetmidnight($now); $yesterdaymidnight = $todaymidnight - DAYSECS; @@ -126,7 +126,7 @@ public function get_timestamp_max_limit_test_cases() { } /** - * @dataProvider get_timestamp_max_limit_test_cases() + * @dataProvider get_timestamp_max_limit_test_cases */ public function test_get_timestamp_max_limit($starttime, $max, $expected): void { $class = calendar_event_exporter::class; diff --git a/calendar/tests/calendartype_test.php b/calendar/tests/calendartype_test.php index 428aebbd9b2c0..5319b35690567 100644 --- a/calendar/tests/calendartype_test.php +++ b/calendar/tests/calendartype_test.php @@ -195,6 +195,12 @@ private function core_functions_test($type) { $this->assertEquals($calendar->timestamp_to_date_string($this->user->timecreated, '', 99, true, true), userdate($this->user->timecreated)); + // Test the userdate function with a timezone. + $this->assertEquals( + $calendar->timestamp_to_date_string($this->user->timecreated, '', 'Australia/Sydney', true, true), + userdate($this->user->timecreated, timezone: 'Australia/Sydney'), + ); + // Test the calendar/lib.php functions. $this->assertEquals($calendar->get_weekdays(), calendar_get_days()); $this->assertEquals($calendar->get_starting_weekday(), calendar_get_starting_weekday()); diff --git a/calendar/tests/container_test.php b/calendar/tests/container_test.php index 00a9f8badf29a..f943eb007f4eb 100644 --- a/calendar/tests/container_test.php +++ b/calendar/tests/container_test.php @@ -67,7 +67,7 @@ public function test_get_event_factory(): void { /** * Test that the event factory correctly creates instances of events. * - * @dataProvider get_event_factory_testcases() + * @dataProvider get_event_factory_testcases * @param \stdClass $dbrow Row from the "database". */ public function test_event_factory_create_instance($dbrow): void { @@ -128,7 +128,7 @@ public function test_event_factory_create_instance($dbrow): void { /** * Test that the event factory deals with invisible modules properly as admin. * - * @dataProvider get_event_factory_testcases() + * @dataProvider get_event_factory_testcases * @param \stdClass $dbrow Row from the "database". */ public function test_event_factory_when_module_visibility_is_toggled_as_admin($dbrow): void { @@ -154,7 +154,7 @@ public function test_event_factory_when_module_visibility_is_toggled_as_admin($d /** * Test that the event factory deals with invisible modules properly as a guest. * - * @dataProvider get_event_factory_testcases() + * @dataProvider get_event_factory_testcases * @param \stdClass $dbrow Row from the "database". */ public function test_event_factory_when_module_visibility_is_toggled_as_guest($dbrow): void { @@ -183,7 +183,7 @@ public function test_event_factory_when_module_visibility_is_toggled_as_guest($d /** * Test that the event factory deals with invisible courses as an admin. * - * @dataProvider get_event_factory_testcases() + * @dataProvider get_event_factory_testcases * @param \stdClass $dbrow Row from the "database". */ public function test_event_factory_when_course_visibility_is_toggled_as_admin($dbrow): void { @@ -208,7 +208,7 @@ public function test_event_factory_when_course_visibility_is_toggled_as_admin($d /** * Test that the event factory deals with invisible courses as a student. * - * @dataProvider get_event_factory_testcases() + * @dataProvider get_event_factory_testcases * @param \stdClass $dbrow Row from the "database". */ public function test_event_factory_when_course_visibility_is_toggled_as_student($dbrow): void { @@ -504,7 +504,7 @@ public function test_get_event_mapper(): void { /** * Test cases for the get event factory test. */ - public function get_event_factory_testcases() { + public static function get_event_factory_testcases(): array { return [ 'Data set 1' => [ 'dbrow' => (object)[ diff --git a/calendar/tests/event_description_test.php b/calendar/tests/event_description_test.php index d071dbe394c6c..05bc94410cc17 100644 --- a/calendar/tests/event_description_test.php +++ b/calendar/tests/event_description_test.php @@ -29,7 +29,7 @@ class event_description_test extends \advanced_testcase { /** * Test event description class getters. * - * @dataProvider getters_testcases() + * @dataProvider getters_testcases * @param array $constructorparams Associative array of constructor parameters. */ public function test_getters($constructorparams): void { @@ -45,7 +45,7 @@ public function test_getters($constructorparams): void { /** * Test cases for getters test. */ - public function getters_testcases() { + public static function getters_testcases(): array { return [ 'Dataset 1' => [ 'constructorparams' => [ diff --git a/calendar/tests/event_factory_test.php b/calendar/tests/event_factory_test.php index b571aa7ea90ff..34226b9f8141c 100644 --- a/calendar/tests/event_factory_test.php +++ b/calendar/tests/event_factory_test.php @@ -35,7 +35,7 @@ class event_factory_test extends \advanced_testcase { /** * Test event class getters. * - * @dataProvider create_instance_testcases() + * @dataProvider create_instance_testcases * @param \stdClass $dbrow Row from the event table. * @param callable $actioncallbackapplier Action callback applier. * @param callable $visibilitycallbackapplier Visibility callback applier. @@ -346,7 +346,7 @@ function () { * * @return array Array of testcases. */ - public function create_instance_testcases() { + public static function create_instance_testcases(): array { return [ 'Sample event record with event exposed' => [ 'dbrow' => (object)[ diff --git a/calendar/tests/event_test.php b/calendar/tests/event_test.php index 150018ea08854..433a5d3bdbdc5 100644 --- a/calendar/tests/event_test.php +++ b/calendar/tests/event_test.php @@ -36,7 +36,7 @@ class event_test extends \advanced_testcase { /** * Test event class getters. * - * @dataProvider getters_testcases() + * @dataProvider getters_testcases * @param array $constructorparams Associative array of constructor parameters. */ public function test_getters($constructorparams): void { @@ -71,7 +71,7 @@ public function test_getters($constructorparams): void { /** * Test cases for getters test. */ - public function getters_testcases() { + public static function getters_testcases(): array { $lamecallable = function($id) { return (object)['id' => $id, 'modname' => 'assign']; }; diff --git a/calendar/tests/event_times_test.php b/calendar/tests/event_times_test.php index 22f21b234b28c..aec5d9e928bb3 100644 --- a/calendar/tests/event_times_test.php +++ b/calendar/tests/event_times_test.php @@ -29,7 +29,7 @@ class event_times_test extends \advanced_testcase { /** * Test event times class getters. * - * @dataProvider getters_testcases() + * @dataProvider getters_testcases * @param array $constructorparams Associative array of constructor parameters. */ public function test_getters($constructorparams): void { @@ -51,7 +51,7 @@ public function test_getters($constructorparams): void { /** * Test cases for getters test. */ - public function getters_testcases() { + public static function getters_testcases(): array { return [ 'Dataset 1' => [ 'constructorparams' => [ diff --git a/calendar/tests/externallib_test.php b/calendar/tests/externallib_test.php index e987aaa7eac08..6fd0467175785 100644 --- a/calendar/tests/externallib_test.php +++ b/calendar/tests/externallib_test.php @@ -2796,7 +2796,7 @@ public function test_get_calendar_event_by_id_no_course_permission(): void { * * @return array */ - public function get_calendar_event_by_id_prevent_read_other_users_events_data_provider(): array { + public static function get_calendar_event_by_id_prevent_read_other_users_events_data_provider(): array { $syscontext = \context_system::instance(); $managerrole = 'manager'; return [ @@ -2867,7 +2867,7 @@ public function test_get_calendar_event_by_id_prevent_read_other_users_events( * * @return array */ - public function edit_or_delete_other_users_events_data_provider(): array { + public static function edit_or_delete_other_users_events_data_provider(): array { $syscontext = \context_system::instance(); $managerrole = 'manager'; return [ diff --git a/calendar/tests/lib_test.php b/calendar/tests/lib_test.php index f4eb322af39a7..2102b0b9ac9bb 100644 --- a/calendar/tests/lib_test.php +++ b/calendar/tests/lib_test.php @@ -1226,7 +1226,7 @@ public function test_calendar_can_manage_user_event(): void { * * @return array[] */ - public function calendar_format_event_location_provider(): array { + public static function calendar_format_event_location_provider(): array { return [ 'Empty' => ['', ''], 'Text' => ['Barcelona', 'Barcelona'], diff --git a/calendar/tests/std_proxy_test.php b/calendar/tests/std_proxy_test.php index af3080df97906..c9a32cd5108a5 100644 --- a/calendar/tests/std_proxy_test.php +++ b/calendar/tests/std_proxy_test.php @@ -115,7 +115,7 @@ public function test_get_proxied_instance($id): void { /** * Test cases for proxying test. */ - public function proxy_testcases() { + public static function proxy_testcases(): array { return [ 'Object 1 member 1' => [ 1, @@ -153,7 +153,7 @@ public function proxy_testcases() { /** * Test cases for getting and setting tests. */ - public function get_set_testcases() { + public static function get_set_testcases(): array { return [ 'Object 1' => [1], 'Object 2' => [5] diff --git a/calendar/type/gregorian/classes/structure.php b/calendar/type/gregorian/classes/structure.php index a01e84b1c1dbb..7680a874afd74 100644 --- a/calendar/type/gregorian/classes/structure.php +++ b/calendar/type/gregorian/classes/structure.php @@ -276,13 +276,6 @@ public function get_next_month($year, $month) { /** * Returns a formatted string that represents a date in user time. * - * Returns a formatted string that represents a date in user time - * WARNING: note that the format is for strftime(), not date(). - * Because of a bug in most Windows time libraries, we can't use - * the nicer %e, so we have to use %d which has leading zeroes. - * A lot of the fuss in the function is just getting rid of these leading - * zeroes as efficiently as possible. - * * If parameter fixday = true (default), then take off leading * zero from %d, else maintain it. * @@ -303,43 +296,62 @@ public function timestamp_to_date_string($time, $format, $timezone, $fixday, $fi $format = get_string('strftimedaydatetime', 'langconfig'); } - if (!empty($CFG->nofixday)) { // Config.php can force %d not to be fixed. - $fixday = false; - } else if ($fixday) { - $formatnoday = str_replace('%d', 'DD', $format); - $fixday = ($formatnoday != $format); - $format = $formatnoday; + // Note: This historical logic was about fixing 12-hour time to remove + // unnecessary leading zero was required because on Windows, PHP strftime + // function did not support the correct 'hour without leading zero' parameter (%l). + // This is no longer required because we use IntlDateFormatter. + // Unfortunately though the original implementation was done incorrectly. + // The documentation for strftime notes that for the "%l" and "%e" specifiers where + // no leading zero is used, a space is used instead. + // As a result we switch to the new format specifiers "%l" and "%e", wrap them in placeholders + // and then remove the spaces. + + if (empty($CFG->nofixday) && $fixday) { + // Config.php can force %d not to be fixed, but only if the format did not specify it. + $format = str_replace( + '%d', + 'DDHH%eHHDD', + $format, + ); + } + + if (empty($CFG->nofixhour) && $fixhour) { + $format = str_replace( + '%I', + 'DDHH%lHHDD', + $format, + ); + } + + if (is_string($time) && !is_numeric($time)) { + debugging( + "Invalid time passed to timestamp_to_date_string: '{$time}'", + DEBUG_DEVELOPER, + ); + $time = 0; } - // Note: This logic about fixing 12-hour time to remove unnecessary leading - // zero is required because on Windows, PHP strftime function does not - // support the correct 'hour without leading zero' parameter (%l). - if (!empty($CFG->nofixhour)) { - // Config.php can force %I not to be fixed. - $fixhour = false; - } else if ($fixhour) { - $formatnohour = str_replace('%I', 'HH', $format); - $fixhour = ($formatnohour != $format); - $format = $formatnohour; + if ($time === null || $time === '') { + $time = 0; } - $time = (int)$time; // Moodle allows rubbish in input... - $datestring = date_format_string($time, $format, $timezone); + $time = new \DateTime("@{$time}", new \DateTimeZone(date_default_timezone_get())); date_default_timezone_set(\core_date::get_user_timezone($timezone)); - if ($fixday) { - $daystring = ltrim(str_replace(array(' 0', ' '), '', date(' d', $time))); - $datestring = str_replace('DD', $daystring, $datestring); - } - if ($fixhour) { - $hourstring = ltrim(str_replace(array(' 0', ' '), '', date(' h', $time))); - $datestring = str_replace('HH', $hourstring, $datestring); - } + $formattedtime = \core_date::strftime( + $format, + $time, + get_string('locale', 'langconfig'), + ); \core_date::set_default_server_timezone(); - return $datestring; + // Use a simple regex to remove the placeholders and any leading spaces to match the historically + // generated format. + $formattedtime = preg_replace('/DDHH ?(\d{1,2})HHDD/', '$1', $formattedtime); + + return $formattedtime; } /** diff --git a/calendar/type/gregorian/tests/structure_test.php b/calendar/type/gregorian/tests/structure_test.php new file mode 100644 index 0000000000000..dc917ad8a1e2b --- /dev/null +++ b/calendar/type/gregorian/tests/structure_test.php @@ -0,0 +1,193 @@ +. + +namespace calendartype_gregorian; + +/** + * Tests for Gregorian calendar type + * + * @package calendartype_gregorian + * @category test + * @copyright Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \calendartype_gregorian\structure + */ +final class structure_test extends \advanced_testcase { + public function tearDown(): void { + parent::tearDown(); + + get_string_manager(true); + } + + /** + * Test the timestamp_to_date_string method with different input values. + * + * @dataProvider timestamp_to_date_string_provider + * @param string $locale + * @param int $timestamp + * @param string $format + * @param string $timezone + * @param bool $fixday + * @param bool $fixhour + * @param string $expected + */ + public function test_timestamp_to_date_string( + string $locale, + int $timestamp, + string $format, + string $timezone, + bool $fixday, + bool $fixhour, + string $expected, + ): void { + $this->resetAfterTest(); + + $stringmanager = $this->get_mocked_string_manager(); + $stringmanager->mock_string('locale', 'langconfig', $locale); + + $structure = new structure(); + $this->assertEquals( + $expected, + $structure->timestamp_to_date_string( + $timestamp, + $format, + $timezone, + $fixday, + $fixhour, + ), + ); + } + + /** + * Data provider for timestamp_to_date_string tests. + * + * @return array + */ + public static function timestamp_to_date_string_provider(): array { + return [ + 'English with UTC timezone' => [ + 'en', + 0, + '%Y-%m-%d %H:%M:%S', + 'UTC', + false, + false, + '1970-01-01 00:00:00', + ], + 'English with London timezone' => [ + 'en', + 1728487003, + "%d %B %Y", + 'Europe/London', + false, + false, + "09 October 2024", + ], + 'English with Sydney (+11) timezone' => [ + 'en', + 1728487003, + "%d %B %Y", + 'Australia/Sydney', + false, + false, + "10 October 2024", + ], + 'Russian with Sydney (+11) timezone' => [ + 'ru', + 1728487003, + "%d %B %Y %H:%M:%S", + 'Australia/Sydney', + false, + false, + '10 октября 2024 02:16:43', + ], + 'Russian %B %Y (Genitive) with Sydney (+11) timezone' => [ + 'ru', + 1728487003, + "%B %Y", + 'Australia/Sydney', + false, + false, + "октябрь 2024", + ], + 'Russian %d %B %Y (Nominative) with London timezone' => [ + 'ru', + 1728487003, + "%d %B %Y", + 'Europe/London', + false, + false, + "09 октября 2024", + ], + 'Russian %d %B %Y (Nominative) with London timezone fixing leading zero' => [ + 'ru', + 1728487003, + "%d %B %Y", + 'Europe/London', + true, + false, + "9 октября 2024", + ], + 'Russian %e %B %Y (Nominative) with London timezone' => [ + 'ru', + 1728487003, + "%e %B %Y", + 'Europe/London', + false, + false, + " 9 октября 2024", + ], + 'Time %I without fixing leading zero' => [ + 'ru', + 1728487003, + "%I:%M:%S", + 'Australia/Sydney', + false, + false + , + "02:16:43", + ], + 'Time %I fixing leading zero' => [ + 'ru', + 1728487003, + "%I:%M:%S", + 'Australia/Sydney', + false, + true + , + "2:16:43", + ], + 'Time %l without fixing leading zero' => [ + 'ru', + 1728487003, + "%l:%M:%S", + 'Australia/Sydney', + false, + false, + " 2:16:43", + ], + 'Time %l fixing leading zero' => [ + 'ru', + 1728487003, + "%l:%M:%S", + 'Australia/Sydney', + false, + true, + " 2:16:43", + ], + ]; + } +} diff --git a/cohort/tests/reportbuilder/datasource/cohorts_test.php b/cohort/tests/reportbuilder/datasource/cohorts_test.php index 455209c33c1f7..a44fcdd2effec 100644 --- a/cohort/tests/reportbuilder/datasource/cohorts_test.php +++ b/cohort/tests/reportbuilder/datasource/cohorts_test.php @@ -145,7 +145,7 @@ public function test_datasource_non_default_columns(): void { * * @return array[] */ - public function datasource_filters_provider(): array { + public static function datasource_filters_provider(): array { return [ // Cohort. 'Filter cohort' => ['cohort:cohortselect', [ diff --git a/comment/tests/reportbuilder/datasource/comments_test.php b/comment/tests/reportbuilder/datasource/comments_test.php index 177e9ed57982c..cc8d81669fc89 100644 --- a/comment/tests/reportbuilder/datasource/comments_test.php +++ b/comment/tests/reportbuilder/datasource/comments_test.php @@ -136,7 +136,7 @@ public function test_datasource_non_default_columns(): void { * * @return array[] */ - public function datasource_filters_provider(): array { + public static function datasource_filters_provider(): array { return [ // Comment. 'Filter content' => ['comment:content', [ diff --git a/communication/classes/helper.php b/communication/classes/helper.php index 524f4044a16c3..dbb4b4460a307 100644 --- a/communication/classes/helper.php +++ b/communication/classes/helper.php @@ -431,13 +431,13 @@ public static function update_course_communication_instance( if (empty($provider)) { $provider = $coursecommunication->get_provider(); } - $roomnameidenfier = $provider . 'roomname'; + $roomnameidentifier = $provider . 'roomname'; // Determine the communication room name if none was provided and add it to the course data. - if (empty($course->$roomnameidenfier)) { - $course->$roomnameidenfier = $coursecommunication->get_room_name(); - if (empty($course->$roomnameidenfier)) { - $course->$roomnameidenfier = $course->fullname ?? get_course($course->id)->fullname; + if (empty($course->$roomnameidentifier)) { + $course->$roomnameidentifier = $coursecommunication->get_room_name(); + if (empty($course->$roomnameidentifier)) { + $course->$roomnameidentifier = $course->fullname ?? get_course($course->id)->fullname; } } @@ -470,7 +470,7 @@ public static function update_course_communication_instance( $communication->configure_room_and_membership_by_provider( provider: $provider, instance: $course, - communicationroomname: $course->$roomnameidenfier, + communicationroomname: $course->$roomnameidentifier, users: $enrolledusers, instanceimage: $courseimage, ); @@ -491,7 +491,7 @@ public static function update_course_communication_instance( $communication->configure_room_and_membership_by_provider( provider: $provider, instance: $course, - communicationroomname: $course->$roomnameidenfier, + communicationroomname: $course->$roomnameidentifier, users: $enrolledusers, instanceimage: $courseimage, queue: false, @@ -542,9 +542,9 @@ public static function update_group_communication_instances_for_course( context: $coursecontext, ); - $roomnameidenfier = $provider . 'roomname'; + $roomnameidentifier = $provider . 'roomname'; $communicationroomname = self::format_group_room_name( - baseroomname: $course->$roomnameidenfier, + baseroomname: $course->$roomnameidentifier, groupname: $coursegroup->name, ); diff --git a/communication/classes/hook_listener.php b/communication/classes/hook_listener.php index 67f3d2dcf9767..82a25d19bb99f 100644 --- a/communication/classes/hook_listener.php +++ b/communication/classes/hook_listener.php @@ -442,8 +442,10 @@ public static function delete_user_room_memberships( courseid: $course->id, context: $coursecontext, ); - $communication->get_room_user_provider()->remove_members_from_room(userids: [$user->id]); - $communication->get_processor()->delete_instance_user_mapping(userids: [$user->id]); + if ($communication->get_processor() !== null) { + $communication->get_room_user_provider()->remove_members_from_room(userids: [$user->id]); + $communication->get_processor()->delete_instance_user_mapping(userids: [$user->id]); + } } else { // If group mode is set then handle the group communication rooms. $coursegroups = groups_get_all_groups(courseid: $course->id); @@ -452,8 +454,11 @@ public static function delete_user_room_memberships( groupid: $coursegroup->id, context: $coursecontext, ); - $communication->get_room_user_provider()->remove_members_from_room(userids: [$user->id]); - $communication->get_processor()->delete_instance_user_mapping(userids: [$user->id]); + if ($communication->get_processor() !== null) { + $communication->get_room_user_provider()->remove_members_from_room(userids: [$user->id]); + $communication->get_processor()->delete_instance_user_mapping(userids: [$user->id]); + } + } } } diff --git a/communication/provider/matrix/tests/matrix_client_test.php b/communication/provider/matrix/tests/matrix_client_test.php index a3e8c89508680..646cc6a89af86 100644 --- a/communication/provider/matrix/tests/matrix_client_test.php +++ b/communication/provider/matrix/tests/matrix_client_test.php @@ -20,10 +20,7 @@ use communication_matrix\local\spec\v1p7; use communication_matrix\local\spec\features; use communication_matrix\tests\fixtures\mocked_matrix_client; -use core\http_client; use GuzzleHttp\Handler\MockHandler; -use GuzzleHttp\HandlerStack; -use GuzzleHttp\Middleware; use GuzzleHttp\Psr7\Response; use moodle_exception; @@ -89,16 +86,13 @@ public function test_instance( ?array $versions, string $expectedversion, ): void { - // Create a mock and queue two responses. - - $mock = new MockHandler([ - $this->get_mocked_version_response($versions), - ]); - $handlerstack = HandlerStack::create($mock); $container = []; - $history = Middleware::history($container); - $handlerstack->push($history); - $client = new http_client(['handler' => $handlerstack]); + ['client' => $client, 'mock' => $mock] = $this->get_mocked_http_client( + history: $container, + ); + + $mock->append($this->get_mocked_version_response($versions)); + mocked_matrix_client::set_client($client); $instance = mocked_matrix_client::instance( @@ -121,15 +115,15 @@ public function test_instance( * Test that the instance method returns a valid instance for the given versions. */ public function test_instance_cached(): void { - $mock = new MockHandler([ - $this->get_mocked_version_response(), - $this->get_mocked_version_response(), - ]); - $handlerstack = HandlerStack::create($mock); $container = []; - $history = Middleware::history($container); - $handlerstack->push($history); - $client = new http_client(['handler' => $handlerstack]); + ['client' => $client, 'mock' => $mock] = $this->get_mocked_http_client( + history: $container, + ); + + // Queue two responses. + $mock->append($this->get_mocked_version_response()); + $mock->append($this->get_mocked_version_response()); + mocked_matrix_client::set_client($client); $instance = mocked_matrix_client::instance('https://example.com', 'testtoken'); @@ -153,16 +147,10 @@ public function test_instance_cached(): void { * Test that the instance method throws an appropriate exception if no support is found. */ public function test_instance_no_support(): void { - // Create a mock and queue two responses. + ['client' => $client, 'mock' => $mock] = $this->get_mocked_http_client(); + + $mock->append($this->get_mocked_version_response(['v99.9'])); - $mock = new MockHandler([ - $this->get_mocked_version_response(['v99.9']), - ]); - $handlerstack = HandlerStack::create($mock); - $container = []; - $history = Middleware::history($container); - $handlerstack->push($history); - $client = new http_client(['handler' => $handlerstack]); mocked_matrix_client::set_client($client); $this->expectException(moodle_exception::class); diff --git a/communication/provider/matrix/tests/matrix_client_test_trait.php b/communication/provider/matrix/tests/matrix_client_test_trait.php index 02d33f337c284..d23d165b097a1 100644 --- a/communication/provider/matrix/tests/matrix_client_test_trait.php +++ b/communication/provider/matrix/tests/matrix_client_test_trait.php @@ -67,19 +67,24 @@ protected function get_mocked_instance_for_version( array &$historycontainer = [], ?MockHandler $mock = null, ): matrix_client { + // If no mock is provided, use get_mocked_http_client to create the mock and client. if ($mock === null) { - $mock = new MockHandler(); + ['mock' => $mock, 'client' => $client] = $this->get_mocked_http_client( + history: $historycontainer + ); + } else { + // If mock is provided, create the handlerstack and history middleware. + $handlerstack = HandlerStack::create($mock); + $history = Middleware::history($historycontainer); + $handlerstack->push($history); + $client = new http_client(['handler' => $handlerstack]); } // Add the version response. $mock->append($this->get_mocked_version_response([$version])); - $handlerstack = HandlerStack::create($mock); - $history = Middleware::history($historycontainer); - $handlerstack->push($history); - $client = new http_client(['handler' => $handlerstack]); mocked_matrix_client::set_client($client); - $client = mocked_matrix_client::instance( + $instance = mocked_matrix_client::instance( 'https://example.com', 'testtoken', ); @@ -87,7 +92,7 @@ protected function get_mocked_instance_for_version( // Remove the request that is required to fetch the version from the history. array_shift($historycontainer); - return $client; + return $instance; } /** diff --git a/communication/tests/api_test.php b/communication/tests/api_test.php index 8a5436a99ee89..ea61511b86489 100644 --- a/communication/tests/api_test.php +++ b/communication/tests/api_test.php @@ -69,10 +69,10 @@ public function test_set_data(): void { // Set the data. $communication->set_data($course); - $roomnameidenfier = $communication->get_provider() . 'roomname'; + $roomnameidentifier = $communication->get_provider() . 'roomname'; // Test the set data. - $this->assertEquals($roomname, $course->$roomnameidenfier); + $this->assertEquals($roomname, $course->$roomnameidentifier); $this->assertEquals($provider, $course->selectedcommunication); } diff --git a/communication/tests/behat/communication_configuration.feature b/communication/tests/behat/communication_configuration.feature index 7c001a090f653..e27d5ed036584 100644 --- a/communication/tests/behat/communication_configuration.feature +++ b/communication/tests/behat/communication_configuration.feature @@ -107,3 +107,26 @@ Feature: Access the communication configuration page And I navigate to "Communication" in current page administration And the field "Room name" matches value "Matrix room" And the field "Room topic" matches value "Matrix topic" + + @javascript + Scenario: Emptying the room name field always sets course name as default + Given a Matrix mock server is configured + And I am on the "Test course" "Course" page logged in as "teacher1" + When I navigate to "Communication" in current page administration + And I set the following fields to these values: + | selectedcommunication | communication_matrix | + And I wait to be redirected + And I should see "Room name" + And I should see "Room topic" + And I set the following fields to these values: + | communication_matrixroomname | Matrix room | + | matrixroomtopic | Matrix topic | + And I click on "Save changes" "button" + And I navigate to "Communication" in current page administration + Then the field "Room name" matches value "Matrix room" + And the field "Room topic" matches value "Matrix topic" + And I set the following fields to these values: + | communication_matrixroomname | | + And I click on "Save changes" "button" + And I navigate to "Communication" in current page administration + And the field "Room name" matches value "Test course" diff --git a/completion/tests/activity_custom_completion_test.php b/completion/tests/activity_custom_completion_test.php index 07ed75a3fe8f7..0323c2ffebf7e 100644 --- a/completion/tests/activity_custom_completion_test.php +++ b/completion/tests/activity_custom_completion_test.php @@ -48,7 +48,7 @@ protected function setup_mock(array $methods) { /** * Data provider for test_get_overall_completion_state(). */ - public function overall_completion_state_provider(): array { + public static function overall_completion_state_provider(): array { global $CFG; require_once($CFG->libdir . '/completionlib.php'); return [ @@ -113,13 +113,14 @@ public function test_get_overall_completion_state(array $rules, array $rulestate // Mock activity_custom_completion's get_state() method. if ($invokecount > 0) { - $stub->expects($this->exactly($invokecount)) + $stateinvocations = $this->exactly($invokecount); + $stub->expects($stateinvocations) ->method('get_state') - ->withConsecutive( - [$rules[0]], - [$rules[1]] - ) - ->willReturn($rulestates[0], $rulestates[1]); + ->willReturnCallback(function ($rule) use ($stateinvocations, $rules, $rulestates) { + $index = self::getInvocationCount($stateinvocations) - 1; + $this->assertEquals($rules[$index], $rule); + return $rulestates[$index]; + }); } else { $stub->expects($this->never()) ->method('get_state'); @@ -133,7 +134,7 @@ public function test_get_overall_completion_state(array $rules, array $rulestate * * @return array[] */ - public function validate_rule_provider() { + public static function validate_rule_provider(): array { return [ 'Not defined' => [ false, true, coding_exception::class diff --git a/completion/tests/bulk_update_test.php b/completion/tests/bulk_update_test.php index b6b2db80fb590..af9e45ea83cc6 100644 --- a/completion/tests/bulk_update_test.php +++ b/completion/tests/bulk_update_test.php @@ -36,7 +36,7 @@ class bulk_update_test extends \advanced_testcase { * Provider for test_bulk_form_submit_single * @return array */ - public function bulk_form_submit_single_provider() { + public static function bulk_form_submit_single_provider(): array { return [ 'assign-1' => ['assign', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionsubmit' => 1]], 'assign-2' => ['assign', ['completion' => COMPLETION_TRACKING_MANUAL]], @@ -184,7 +184,7 @@ protected function create_course_and_modules($modulenames) { * Provider for test_bulk_form_submit_multiple * @return array */ - public function bulk_form_submit_multiple_provider() { + public static function bulk_form_submit_multiple_provider(): array { return [ 'Several modules with the same module type (choice)' => [ [ diff --git a/completion/tests/cm_completion_details_test.php b/completion/tests/cm_completion_details_test.php index faede9eafdaf7..c5e06a339391f 100644 --- a/completion/tests/cm_completion_details_test.php +++ b/completion/tests/cm_completion_details_test.php @@ -105,7 +105,7 @@ protected function setup_data(?int $completion, array $completionoptions = [], * * @return array[] */ - public function has_completion_provider(): array { + public static function has_completion_provider(): array { return [ 'Automatic' => [ COMPLETION_TRACKING_AUTOMATIC, true @@ -138,7 +138,7 @@ public function test_has_completion(int $completion, bool $expectedresult): void * * @return array[] */ - public function is_automatic_provider(): array { + public static function is_automatic_provider(): array { return [ 'Automatic' => [ COMPLETION_TRACKING_AUTOMATIC, true @@ -171,7 +171,7 @@ public function test_is_automatic(int $completion, bool $expectedresult): void { * * @return array[] */ - public function is_manual_provider(): array { + public static function is_manual_provider(): array { return [ 'Automatic' => [ COMPLETION_TRACKING_AUTOMATIC, false @@ -203,7 +203,7 @@ public function test_is_manual(int $completion, bool $expectedresult): void { * Data provider for test_get_overall_completion(). * @return array[] */ - public function overall_completion_provider(): array { + public static function overall_completion_provider(): array { return [ 'Complete' => [COMPLETION_COMPLETE], 'Incomplete' => [COMPLETION_INCOMPLETE], @@ -362,7 +362,7 @@ public function test_is_overall_complete( * Data provider for test_get_details(). * @return array[] */ - public function get_details_provider() { + public static function get_details_provider(): array { return [ 'No completion tracking' => [ COMPLETION_TRACKING_NONE, null, null, null, [] @@ -523,7 +523,7 @@ public function test_get_details(int $completion, ?int $completionview, * Data provider for test_get_details_custom_order(). * @return array[] */ - public function get_details_custom_order_provider() { + public static function get_details_custom_order_provider(): array { return [ 'Custom and view/grade standard conditions, view first and grade last' => [ true, diff --git a/completion/tests/generator_test.php b/completion/tests/generator_test.php index b10de8bdfbf34..7a987662d4247 100644 --- a/completion/tests/generator_test.php +++ b/completion/tests/generator_test.php @@ -80,7 +80,7 @@ public function test_create_default_completion($course, $module, bool $exception * Data provider for test_create_default_completion(). * @return array[] */ - public function create_default_completion_provider(): array { + public static function create_default_completion_provider(): array { global $SITE; return [ diff --git a/contentbank/contenttype/h5p/tests/content_h5p_test.php b/contentbank/contenttype/h5p/tests/content_h5p_test.php index a1b982f88eef2..ad3e6960ff06b 100644 --- a/contentbank/contenttype/h5p/tests/content_h5p_test.php +++ b/contentbank/contenttype/h5p/tests/content_h5p_test.php @@ -138,7 +138,7 @@ public function test_is_view_allowed(string $role, array $disabledlibraries, arr * * @return array */ - public function is_view_allowed_provider(): array { + public static function is_view_allowed_provider(): array { return [ 'Editing teacher with all libraries enabled' => [ 'role' => 'editingteacher', diff --git a/contentbank/tests/behat/edit_content.feature b/contentbank/tests/behat/edit_content.feature index 05209d3ca724f..76dd50cd07491 100644 --- a/contentbank/tests/behat/edit_content.feature +++ b/contentbank/tests/behat/edit_content.feature @@ -56,6 +56,7 @@ Feature: Content bank use editor feature Then I click on "Edit" "link" And I switch to "h5p-editor-iframe" class iframe And I switch to the main frame + And I change viewport size to "800x1400" And I click on "Cancel" "button" And "filltheblanks.h5p" "heading" should exist diff --git a/contentbank/tests/content_test.php b/contentbank/tests/content_test.php index a0dac533b6fc0..8dbe36e3b5028 100644 --- a/contentbank/tests/content_test.php +++ b/contentbank/tests/content_test.php @@ -74,7 +74,7 @@ public function test_get_name(): void { * * @return array */ - public function set_name_provider() { + public static function set_name_provider(): array { return [ 'Standard name' => ['New name', 'New name'], 'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017'], diff --git a/contentbank/tests/contentbank_test.php b/contentbank/tests/contentbank_test.php index 2efb13eef28cd..7b318f9ee159f 100644 --- a/contentbank/tests/contentbank_test.php +++ b/contentbank/tests/contentbank_test.php @@ -65,7 +65,7 @@ public static function setupBeforeClass(): void { * * @return array */ - public function get_extension_provider() { + public static function get_extension_provider(): array { return [ 'H5P file' => ['something.h5p', '.h5p'], 'PDF file' => ['something.pdf', '.pdf'] @@ -95,7 +95,7 @@ public function test_get_extension(string $filename, string $expected): void { * * @return array */ - public function get_extension_supporters_provider() { + public static function get_extension_supporters_provider(): array { return [ 'H5P first' => [['.h5p' => ['h5p', 'testable']], '.h5p', 'h5p'], 'Testable first (but upload not implemented)' => [['.h5p' => ['testable', 'h5p']], '.h5p', 'h5p'], @@ -259,7 +259,7 @@ public function test_search_contents(?string $search, string $where, int $expect * * @return array */ - public function search_contents_provider(): array { + public static function search_contents_provider(): array { return [ 'Search all content in all contexts' => [ @@ -519,7 +519,7 @@ public function test_move_contents_for_empty_contentbank(): void { * * @return array */ - public function get_contenttypes_with_capability_feature_provider(): array { + public static function get_contenttypes_with_capability_feature_provider(): array { return [ 'no-contenttypes_enabled' => [ 'contenttypesenabled' => [], diff --git a/contentbank/tests/contenttype_test.php b/contentbank/tests/contenttype_test.php index 5ef8ebf1c4d1c..008daebf91f4f 100644 --- a/contentbank/tests/contenttype_test.php +++ b/contentbank/tests/contenttype_test.php @@ -240,7 +240,7 @@ public function test_upload_content(bool $userecord): void { * * @return array */ - public function upload_content_provider() { + public static function upload_content_provider(): array { return [ 'With record' => [true], 'Without record' => [false], @@ -449,7 +449,7 @@ protected function contenttype_setup_scenario_data(string $contenttype = 'conten * * @return array */ - public function rename_content_provider() { + public static function rename_content_provider(): array { return [ 'Standard name' => ['New name', 'New name', true], 'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017', true], diff --git a/contentbank/tests/external/rename_content_test.php b/contentbank/tests/external/rename_content_test.php index 45463c68e27b0..0c237f0497eaf 100644 --- a/contentbank/tests/external/rename_content_test.php +++ b/contentbank/tests/external/rename_content_test.php @@ -50,7 +50,7 @@ class rename_content_test extends \externallib_advanced_testcase { * * @return array */ - public function rename_content_provider() { + public static function rename_content_provider(): array { return [ 'Standard name' => ['New name', 'New name', true], 'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017', true], diff --git a/course/format/amd/build/local/courseeditor/fileuploader.min.js b/course/format/amd/build/local/courseeditor/fileuploader.min.js index 4396849599e9c..2b108e72054c1 100644 --- a/course/format/amd/build/local/courseeditor/fileuploader.min.js +++ b/course/format/amd/build/local/courseeditor/fileuploader.min.js @@ -1,3 +1,3 @@ -define("core_courseformat/local/courseeditor/fileuploader",["exports","core/config","core/modal_save_cancel","core/modal_events","core/templates","core/normalise","core/prefetch","core/str","core_courseformat/courseeditor","core/process_monitor","core/utils"],(function(_exports,_config,_modal_save_cancel,_modal_events,_templates,_normalise,_prefetch,_str,_courseeditor,_process_monitor,_utils){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.uploadFilesToCourse=void 0,_config=_interopRequireDefault(_config),_modal_save_cancel=_interopRequireDefault(_modal_save_cancel),_modal_events=_interopRequireDefault(_modal_events),_templates=_interopRequireDefault(_templates);const UPLOADURL=_config.default.wwwroot+"/course/dndupload.php";let uploadQueue=null,handlerManagers={},courseUpdates=new Map,errors=null;(0,_prefetch.prefetchStrings)("moodle",["addresourceoractivity","upload"]),(0,_prefetch.prefetchStrings)("core_error",["dndmaxbytes","dndread","dndupload","dndunkownfile"]);class FileUploader{constructor(courseId,sectionId,sectionNum,fileInfo,handler){this.courseId=courseId,this.sectionId=sectionId,this.sectionNum=sectionNum,this.fileInfo=fileInfo,this.handler=handler}execute(process){const fileInfo=this.fileInfo,xhr=this._createXhrRequest(process),formData=this._createUploadFormData(),reader=new FileReader;reader.onload=function(){xhr.open("POST",UPLOADURL,!0),xhr.send(formData)},reader.onerror=function(){process.setError(errors.dndread)},fileInfo.size>0?reader.readAsText(fileInfo.slice(0,5)):reader.readAsText(fileInfo)}getExecutionFunction(){return this.execute.bind(this)}_createXhrRequest(process){const xhr=new XMLHttpRequest;return xhr.upload.addEventListener("progress",(event=>{if(event.lengthComputable){const percent=Math.round(100*event.loaded/event.total);process.setPercentage(percent)}}),!1),xhr.onreadystatechange=()=>{if(1==xhr.readyState&&process.setPercentage(1),4==xhr.readyState)if(200==xhr.status){var result=JSON.parse(xhr.responseText);result&&0==result.error?this._finishProcess(process):process.setError(result.error)}else process.setError(errors.dndupload)},xhr}_createUploadFormData(){const formData=new FormData;try{formData.append("repo_upload_file",this.fileInfo)}catch(error){throw Error(error.dndread)}return formData.append("sesskey",_config.default.sesskey),formData.append("course",this.courseId),formData.append("section",this.sectionNum),formData.append("module",this.handler.module),formData.append("type","Files"),formData}_finishProcess(process){!function(courseId,sectionId){let refresh=courseUpdates.get(courseId);refresh||(refresh=new Set);refresh.add(sectionId),courseUpdates.set(courseId,refresh),refreshCourseEditors()}(this.courseId,this.sectionId),process.setPercentage(100),process.finish()}}class HandlerManager{constructor(courseId){var _this$courseEditor$ge,_this$courseEditor$ge2;if(_defineProperty(this,"lastHandlers",{}),_defineProperty(this,"allHandlers",null),this.courseId=courseId,this.lastUploadId=0,this.courseEditor=(0,_courseeditor.getCourseEditor)(courseId),!this.courseEditor)throw Error("Unkown course editor");this.maxbytes=null!==(_this$courseEditor$ge=null===(_this$courseEditor$ge2=this.courseEditor.get("course"))||void 0===_this$courseEditor$ge2?void 0:_this$courseEditor$ge2.maxbytes)&&void 0!==_this$courseEditor$ge?_this$courseEditor$ge:0}async loadHandlers(){this.allHandlers=await this.courseEditor.getFileHandlersPromise()}getFileExtension(fileInfo){let extension="";const dotpos=fileInfo.name.lastIndexOf(".");return-1!=dotpos&&(extension=fileInfo.name.substring(dotpos+1,fileInfo.name.length).toLowerCase()),extension}validateFile(fileInfo){if(-1!==this.maxbytes&&fileInfo.size>this.maxbytes)throw Error(errors.dndmaxbytes)}filterHandlers(fileInfo){const extension=this.getFileExtension(fileInfo);return this.allHandlers.filter((handler=>"*"==handler.extension||handler.extension==extension))}async getFileHandler(fileInfo){const fileHandlers=this.filterHandlers(fileInfo);if(0==fileHandlers.length)throw Error(errors.dndunkownfile);let fileHandler=null;return fileHandler=1==fileHandlers.length?fileHandlers[0]:await this.askHandlerToUser(fileHandlers,fileInfo),fileHandler}async askHandlerToUser(fileHandlers,fileInfo){var _this$lastHandlers$ex;const extension=this.getFileExtension(fileInfo),modalParams={title:(0,_str.getString)("addresourceoractivity","moodle"),body:_templates.default.render("core_courseformat/fileuploader",this.getModalData(fileHandlers,fileInfo,null!==(_this$lastHandlers$ex=this.lastHandlers[extension])&&void 0!==_this$lastHandlers$ex?_this$lastHandlers$ex:null)),saveButtonText:(0,_str.getString)("upload","moodle")},modal=await this.modalBodyRenderedPromise(modalParams),selectedHandler=await this.modalUserAnswerPromise(modal,fileHandlers);return null===selectedHandler?null:(this.lastHandlers[extension]=selectedHandler.module,selectedHandler)}getModalData(fileHandlers,fileInfo,defaultModule){const data={filename:fileInfo.name,uploadid:++this.lastUploadId,handlers:[]};let hasDefault=!1;if(fileHandlers.forEach(((handler,index)=>{const isDefault=defaultModule==handler.module;data.handlers.push({...handler,selected:isDefault,labelid:"fileuploader_".concat(data.uploadid),value:index}),hasDefault=hasDefault||isDefault})),!hasDefault&&data.handlers.length>0){const lastHandler=data.handlers.pop();lastHandler.selected=!0,data.handlers.push(lastHandler)}return data}modalUserAnswerPromise(modal,fileHandlers){const modalBody=(0,_normalise.getFirst)(modal.getBody());return new Promise(((resolve,reject)=>{modal.getRoot().on(_modal_events.default.save,(event=>{const index=modalBody.querySelector("input:checked").value;event.preventDefault(),modal.destroy(),fileHandlers[index]||reject("Invalid handler selected"),resolve(fileHandlers[index])})),modal.getRoot().on(_modal_events.default.cancel,(()=>{resolve(null)}))}))}modalBodyRenderedPromise(modalParams){return new Promise(((resolve,reject)=>{_modal_save_cancel.default.create(modalParams).then((modal=>{modal.setRemoveOnClose(!0),modal.getRoot().on(_modal_events.default.bodyRendered,(()=>{resolve(modal)})),void 0!==modalParams.saveButtonText&&modal.setSaveButtonText(modalParams.saveButtonText),modal.show()})).catch((()=>{reject("Cannot load modal content")}))}))}}const refreshCourseEditors=(0,_utils.debounce)((()=>{const refreshes=courseUpdates;courseUpdates=new Map,refreshes.forEach(((sectionIds,courseId)=>{const courseEditor=(0,_courseeditor.getCourseEditor)(courseId);courseEditor&&courseEditor.dispatch("sectionState",[...sectionIds])}))}),500);const queueFileUpload=async function(courseId,sectionId,sectionNum,fileInfo,handlerManager){let handler;uploadQueue=await _process_monitor.processMonitor.createProcessQueue();try{handlerManager.validateFile(fileInfo),handler=await handlerManager.getFileHandler(fileInfo)}catch(error){return void uploadQueue.addError(fileInfo.name,error.message)}if(!handler)return;const fileProcessor=new FileUploader(courseId,sectionId,sectionNum,fileInfo,handler);uploadQueue.addPending(fileInfo.name,fileProcessor.getExecutionFunction())};_exports.uploadFilesToCourse=async function(courseId,sectionId,sectionNum,files){const handlerManager=await async function(courseId){if(void 0!==handlerManagers[courseId])return handlerManagers[courseId];const handlerManager=new HandlerManager(courseId);return await handlerManager.loadHandlers(),handlerManagers[courseId]=handlerManager,handlerManagers[courseId]}(courseId);await async function(courseId){var _courseEditor$get$max,_courseEditor$get;if(null!==errors)return;const maxbytestext=null!==(_courseEditor$get$max=null===(_courseEditor$get=(0,_courseeditor.getCourseEditor)(courseId).get("course"))||void 0===_courseEditor$get?void 0:_courseEditor$get.maxbytestext)&&void 0!==_courseEditor$get$max?_courseEditor$get$max:"0";errors={};const allStrings=[{key:"dndmaxbytes",component:"core_error",param:{size:maxbytestext}},{key:"dndread",component:"core_error"},{key:"dndupload",component:"core_error"},{key:"dndunkownfile",component:"core_error"}],loadedStrings=await(0,_str.getStrings)(allStrings);allStrings.forEach(((_ref,index)=>{let{key:key}=_ref;errors[key]=loadedStrings[index]}))}(courseId);for(let index=0;index0?reader.readAsText(fileInfo.slice(0,5)):reader.readAsText(fileInfo)}getExecutionFunction(){return this.execute.bind(this)}_createXhrRequest(process){const xhr=new XMLHttpRequest;return xhr.upload.addEventListener("progress",(event=>{if(event.lengthComputable){const percent=Math.round(100*event.loaded/event.total);process.setPercentage(percent)}}),!1),xhr.onreadystatechange=()=>{if(1==xhr.readyState&&process.setPercentage(1),4==xhr.readyState)if(200==xhr.status){var result=JSON.parse(xhr.responseText);result&&0==result.error?this._finishProcess(process):process.setError(result.error)}else process.setError(errors.dndupload)},xhr}_createUploadFormData(){const formData=new FormData;try{formData.append("repo_upload_file",this.fileInfo)}catch(error){throw Error(error.dndread)}return formData.append("sesskey",_config.default.sesskey),formData.append("course",this.courseId),formData.append("section",this.sectionNum),formData.append("module",this.handler.module),formData.append("type","Files"),formData}_finishProcess(process){!function(courseId,sectionId){let refresh=courseUpdates.get(courseId);refresh||(refresh=new Set);refresh.add(sectionId),courseUpdates.set(courseId,refresh),refreshCourseEditors()}(this.courseId,this.sectionId),process.setPercentage(100),process.finish()}}class HandlerManager{constructor(courseId){var _this$courseEditor$ge,_this$courseEditor$ge2;if(_defineProperty(this,"lastHandlers",{}),_defineProperty(this,"allHandlers",null),this.courseId=courseId,this.lastUploadId=0,this.courseEditor=(0,_courseeditor.getCourseEditor)(courseId),!this.courseEditor)throw Error("Unkown course editor");this.maxbytes=null!==(_this$courseEditor$ge=null===(_this$courseEditor$ge2=this.courseEditor.get("course"))||void 0===_this$courseEditor$ge2?void 0:_this$courseEditor$ge2.maxbytes)&&void 0!==_this$courseEditor$ge?_this$courseEditor$ge:0}async loadHandlers(){this.allHandlers=await this.courseEditor.getFileHandlersPromise()}getFileExtension(fileInfo){let extension="";const dotpos=fileInfo.name.lastIndexOf(".");return-1!=dotpos&&(extension=fileInfo.name.substring(dotpos+1,fileInfo.name.length).toLowerCase()),extension}validateFile(fileInfo){if(-1!==this.maxbytes&&fileInfo.size>this.maxbytes)throw Error(errors.dndmaxbytes)}filterHandlers(fileInfo){const extension=this.getFileExtension(fileInfo);return this.allHandlers.filter((handler=>"*"==handler.extension||handler.extension==extension))}async getFileHandler(fileInfo){const fileHandlers=this.filterHandlers(fileInfo);if(0==fileHandlers.length)throw Error(errors.dndunkownfile);let fileHandler=null;return fileHandler=1==fileHandlers.length?fileHandlers[0]:await this.askHandlerToUser(fileHandlers,fileInfo),fileHandler}async askHandlerToUser(fileHandlers,fileInfo){var _this$lastHandlers$ex;const extension=this.getFileExtension(fileInfo),modalParams={title:(0,_str.getString)("addresourceoractivity","moodle"),body:_templates.default.render("core_courseformat/fileuploader",this.getModalData(fileHandlers,fileInfo,null!==(_this$lastHandlers$ex=this.lastHandlers[extension])&&void 0!==_this$lastHandlers$ex?_this$lastHandlers$ex:null)),saveButtonText:(0,_str.getString)("upload","moodle")},modal=await this.modalBodyRenderedPromise(modalParams),selectedHandler=await this.modalUserAnswerPromise(modal,fileHandlers);return null===selectedHandler?null:(this.lastHandlers[extension]=selectedHandler.module,selectedHandler)}getModalData(fileHandlers,fileInfo,defaultModule){const data={filename:fileInfo.name,uploadid:++this.lastUploadId,handlers:[]};let hasDefault=!1;if(fileHandlers.forEach(((handler,index)=>{const isDefault=defaultModule==handler.module,optionNumber=index+1;data.handlers.push({...handler,selected:isDefault,labelid:"fileuploader_".concat(data.uploadid,"_").concat(optionNumber),value:index}),hasDefault=hasDefault||isDefault})),!hasDefault&&data.handlers.length>0){const lastHandler=data.handlers.pop();lastHandler.selected=!0,data.handlers.push(lastHandler)}return data}modalUserAnswerPromise(modal,fileHandlers){const modalBody=(0,_normalise.getFirst)(modal.getBody());return new Promise(((resolve,reject)=>{modal.getRoot().on(_modal_events.default.save,(event=>{const index=modalBody.querySelector("input:checked").value;event.preventDefault(),modal.destroy(),fileHandlers[index]||reject("Invalid handler selected"),resolve(fileHandlers[index])})),modal.getRoot().on(_modal_events.default.cancel,(()=>{resolve(null)}))}))}modalBodyRenderedPromise(modalParams){return new Promise(((resolve,reject)=>{_modal_save_cancel.default.create(modalParams).then((modal=>{modal.setRemoveOnClose(!0),modal.getRoot().on(_modal_events.default.bodyRendered,(()=>{resolve(modal)})),void 0!==modalParams.saveButtonText&&modal.setSaveButtonText(modalParams.saveButtonText),modal.show()})).catch((()=>{reject("Cannot load modal content")}))}))}}const refreshCourseEditors=(0,_utils.debounce)((()=>{const refreshes=courseUpdates;courseUpdates=new Map,refreshes.forEach(((sectionIds,courseId)=>{const courseEditor=(0,_courseeditor.getCourseEditor)(courseId);courseEditor&&courseEditor.dispatch("sectionState",[...sectionIds])}))}),500);const queueFileUpload=async function(courseId,sectionId,sectionNum,fileInfo,handlerManager){let handler;uploadQueue=await _process_monitor.processMonitor.createProcessQueue();try{handlerManager.validateFile(fileInfo),handler=await handlerManager.getFileHandler(fileInfo)}catch(error){return void uploadQueue.addError(fileInfo.name,error.message)}if(!handler)return;const fileProcessor=new FileUploader(courseId,sectionId,sectionNum,fileInfo,handler);uploadQueue.addPending(fileInfo.name,fileProcessor.getExecutionFunction())};_exports.uploadFilesToCourse=async function(courseId,sectionId,sectionNum,files){const handlerManager=await async function(courseId){if(void 0!==handlerManagers[courseId])return handlerManagers[courseId];const handlerManager=new HandlerManager(courseId);return await handlerManager.loadHandlers(),handlerManagers[courseId]=handlerManager,handlerManagers[courseId]}(courseId);await async function(courseId){var _courseEditor$get$max,_courseEditor$get;if(null!==errors)return;const maxbytestext=null!==(_courseEditor$get$max=null===(_courseEditor$get=(0,_courseeditor.getCourseEditor)(courseId).get("course"))||void 0===_courseEditor$get?void 0:_courseEditor$get.maxbytestext)&&void 0!==_courseEditor$get$max?_courseEditor$get$max:"0";errors={};const allStrings=[{key:"dndmaxbytes",component:"core_error",param:{size:maxbytestext}},{key:"dndread",component:"core_error"},{key:"dndupload",component:"core_error"},{key:"dndunkownfile",component:"core_error"}],loadedStrings=await(0,_str.getStrings)(allStrings);allStrings.forEach(((_ref,index)=>{let{key:key}=_ref;errors[key]=loadedStrings[index]}))}(courseId);for(let index=0;index.\n\n/**\n * The course file uploader.\n *\n * This module is used to upload files directly into the course.\n *\n * @module core_courseformat/local/courseeditor/fileuploader\n * @copyright 2022 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * @typedef {Object} Handler\n * @property {String} extension the handled extension or * for any\n * @property {String} message the handler message\n * @property {String} module the module name\n */\n\nimport Config from 'core/config';\nimport ModalSaveCancel from 'core/modal_save_cancel';\nimport ModalEvents from 'core/modal_events';\nimport Templates from 'core/templates';\nimport {getFirst} from 'core/normalise';\nimport {prefetchStrings} from 'core/prefetch';\nimport {getString, getStrings} from 'core/str';\nimport {getCourseEditor} from 'core_courseformat/courseeditor';\nimport {processMonitor} from 'core/process_monitor';\nimport {debounce} from 'core/utils';\n\n// Uploading url.\nconst UPLOADURL = Config.wwwroot + '/course/dndupload.php';\nconst DEBOUNCETIMER = 500;\nconst USERCANIGNOREFILESIZELIMITS = -1;\n\n/** @var {ProcessQueue} uploadQueue the internal uploadQueue instance. */\nlet uploadQueue = null;\n/** @var {Object} handlerManagers the courseId indexed loaded handler managers. */\nlet handlerManagers = {};\n/** @var {Map} courseUpdates the pending course sections updates. */\nlet courseUpdates = new Map();\n/** @var {Object} errors the error messages. */\nlet errors = null;\n\n// Load global strings.\nprefetchStrings('moodle', ['addresourceoractivity', 'upload']);\nprefetchStrings('core_error', ['dndmaxbytes', 'dndread', 'dndupload', 'dndunkownfile']);\n\n/**\n * Class to upload a file into the course.\n * @private\n */\nclass FileUploader {\n /**\n * Class constructor.\n *\n * @param {number} courseId the course id\n * @param {number} sectionId the section id\n * @param {number} sectionNum the section number\n * @param {File} fileInfo the file information object\n * @param {Handler} handler the file selected file handler\n */\n constructor(courseId, sectionId, sectionNum, fileInfo, handler) {\n this.courseId = courseId;\n this.sectionId = sectionId;\n this.sectionNum = sectionNum;\n this.fileInfo = fileInfo;\n this.handler = handler;\n }\n\n /**\n * Execute the file upload and update the state in the given process.\n *\n * @param {LoadingProcess} process the process to store the upload result\n */\n execute(process) {\n const fileInfo = this.fileInfo;\n const xhr = this._createXhrRequest(process);\n const formData = this._createUploadFormData();\n\n // Try reading the file to check it is not a folder, before sending it to the server.\n const reader = new FileReader();\n reader.onload = function() {\n // File was read OK - send it to the server.\n xhr.open(\"POST\", UPLOADURL, true);\n xhr.send(formData);\n };\n reader.onerror = function() {\n // Unable to read the file (it is probably a folder) - display an error message.\n process.setError(errors.dndread);\n };\n if (fileInfo.size > 0) {\n // If this is a non-empty file, try reading the first few bytes.\n // This will trigger reader.onerror() for folders and reader.onload() for ordinary, readable files.\n reader.readAsText(fileInfo.slice(0, 5));\n } else {\n // If you call slice() on a 0-byte folder, before calling readAsText, then Firefox triggers reader.onload(),\n // instead of reader.onerror().\n // So, for 0-byte files, just call readAsText on the whole file (and it will trigger load/error functions as expected).\n reader.readAsText(fileInfo);\n }\n }\n\n /**\n * Returns the bind version of execute function.\n *\n * This method is used to queue the process into a ProcessQueue instance.\n *\n * @returns {Function} the bind function to execute the process\n */\n getExecutionFunction() {\n return this.execute.bind(this);\n }\n\n /**\n * Generate a upload XHR file request.\n *\n * @param {LoadingProcess} process the current process\n * @return {XMLHttpRequest} the XHR request\n */\n _createXhrRequest(process) {\n const xhr = new XMLHttpRequest();\n // Update the progress bar as the file is uploaded.\n xhr.upload.addEventListener(\n 'progress',\n (event) => {\n if (event.lengthComputable) {\n const percent = Math.round((event.loaded * 100) / event.total);\n process.setPercentage(percent);\n }\n },\n false\n );\n // Wait for the AJAX call to complete.\n xhr.onreadystatechange = () => {\n if (xhr.readyState == 1) {\n // Add a 1% just to indicate that it is uploading.\n process.setPercentage(1);\n }\n // State 4 is DONE. Otherwise the connection is still ongoing.\n if (xhr.readyState != 4) {\n return;\n }\n if (xhr.status == 200) {\n var result = JSON.parse(xhr.responseText);\n if (result && result.error == 0) {\n // All OK.\n this._finishProcess(process);\n } else {\n process.setError(result.error);\n }\n } else {\n process.setError(errors.dndupload);\n }\n };\n return xhr;\n }\n\n /**\n * Upload a file into the course.\n *\n * @return {FormData|null} the new form data object\n */\n _createUploadFormData() {\n const formData = new FormData();\n try {\n formData.append('repo_upload_file', this.fileInfo);\n } catch (error) {\n throw Error(error.dndread);\n }\n formData.append('sesskey', Config.sesskey);\n formData.append('course', this.courseId);\n formData.append('section', this.sectionNum);\n formData.append('module', this.handler.module);\n formData.append('type', 'Files');\n return formData;\n }\n\n /**\n * Finishes the current process.\n * @param {LoadingProcess} process the process\n */\n _finishProcess(process) {\n addRefreshSection(this.courseId, this.sectionId);\n process.setPercentage(100);\n process.finish();\n }\n}\n\n/**\n * The file handler manager class.\n *\n * @private\n */\nclass HandlerManager {\n\n /** @var {Object} lastHandlers the last handlers selected per each file extension. */\n lastHandlers = {};\n\n /** @var {Handler[]|null} allHandlers all the available handlers. */\n allHandlers = null;\n\n /**\n * Class constructor.\n *\n * @param {Number} courseId\n */\n constructor(courseId) {\n this.courseId = courseId;\n this.lastUploadId = 0;\n this.courseEditor = getCourseEditor(courseId);\n if (!this.courseEditor) {\n throw Error('Unkown course editor');\n }\n this.maxbytes = this.courseEditor.get('course')?.maxbytes ?? 0;\n }\n\n /**\n * Load the course file handlers.\n */\n async loadHandlers() {\n this.allHandlers = await this.courseEditor.getFileHandlersPromise();\n }\n\n /**\n * Extract the file extension from a fileInfo.\n *\n * @param {File} fileInfo\n * @returns {String} the file extension or an empty string.\n */\n getFileExtension(fileInfo) {\n let extension = '';\n const dotpos = fileInfo.name.lastIndexOf('.');\n if (dotpos != -1) {\n extension = fileInfo.name.substring(dotpos + 1, fileInfo.name.length).toLowerCase();\n }\n return extension;\n }\n\n /**\n * Check if the file is valid.\n *\n * @param {File} fileInfo the file info\n */\n validateFile(fileInfo) {\n if (this.maxbytes !== USERCANIGNOREFILESIZELIMITS && fileInfo.size > this.maxbytes) {\n throw Error(errors.dndmaxbytes);\n }\n }\n\n /**\n * Get the file handlers of an specific file.\n *\n * @param {File} fileInfo the file indo\n * @return {Array} Array of handlers\n */\n filterHandlers(fileInfo) {\n const extension = this.getFileExtension(fileInfo);\n return this.allHandlers.filter(handler => handler.extension == '*' || handler.extension == extension);\n }\n\n /**\n * Get the Handler to upload a specific file.\n *\n * It will ask the used if more than one handler is available.\n *\n * @param {File} fileInfo the file info\n * @returns {Promise} the selected handler or null if the user cancel\n */\n async getFileHandler(fileInfo) {\n const fileHandlers = this.filterHandlers(fileInfo);\n if (fileHandlers.length == 0) {\n throw Error(errors.dndunkownfile);\n }\n let fileHandler = null;\n if (fileHandlers.length == 1) {\n fileHandler = fileHandlers[0];\n } else {\n fileHandler = await this.askHandlerToUser(fileHandlers, fileInfo);\n }\n return fileHandler;\n }\n\n /**\n * Ask the user to select a specific handler.\n *\n * @param {Handler[]} fileHandlers\n * @param {File} fileInfo the file info\n * @return {Promise} the selected handler\n */\n async askHandlerToUser(fileHandlers, fileInfo) {\n const extension = this.getFileExtension(fileInfo);\n // Build the modal parameters from the event data.\n const modalParams = {\n title: getString('addresourceoractivity', 'moodle'),\n body: Templates.render(\n 'core_courseformat/fileuploader',\n this.getModalData(\n fileHandlers,\n fileInfo,\n this.lastHandlers[extension] ?? null\n )\n ),\n saveButtonText: getString('upload', 'moodle'),\n };\n // Create the modal.\n const modal = await this.modalBodyRenderedPromise(modalParams);\n const selectedHandler = await this.modalUserAnswerPromise(modal, fileHandlers);\n // Cancel action.\n if (selectedHandler === null) {\n return null;\n }\n // Save last selected handler.\n this.lastHandlers[extension] = selectedHandler.module;\n return selectedHandler;\n }\n\n /**\n * Generated the modal template data.\n *\n * @param {Handler[]} fileHandlers\n * @param {File} fileInfo the file info\n * @param {String|null} defaultModule the default module if any\n * @return {Object} the modal template data.\n */\n getModalData(fileHandlers, fileInfo, defaultModule) {\n const data = {\n filename: fileInfo.name,\n uploadid: ++this.lastUploadId,\n handlers: [],\n };\n let hasDefault = false;\n fileHandlers.forEach((handler, index) => {\n const isDefault = (defaultModule == handler.module);\n data.handlers.push({\n ...handler,\n selected: isDefault,\n labelid: `fileuploader_${data.uploadid}`,\n value: index,\n });\n hasDefault = hasDefault || isDefault;\n });\n if (!hasDefault && data.handlers.length > 0) {\n const lastHandler = data.handlers.pop();\n lastHandler.selected = true;\n data.handlers.push(lastHandler);\n }\n return data;\n }\n\n /**\n * Get the user handler choice.\n *\n * Wait for the user answer in the modal and resolve with the selected index.\n *\n * @param {Modal} modal the modal instance\n * @param {Handler[]} fileHandlers the availabvle file handlers\n * @return {Promise} with the option selected by the user.\n */\n modalUserAnswerPromise(modal, fileHandlers) {\n const modalBody = getFirst(modal.getBody());\n return new Promise((resolve, reject) => {\n modal.getRoot().on(\n ModalEvents.save,\n event => {\n // Get the selected option.\n const index = modalBody.querySelector('input:checked').value;\n event.preventDefault();\n modal.destroy();\n if (!fileHandlers[index]) {\n reject('Invalid handler selected');\n }\n resolve(fileHandlers[index]);\n\n }\n );\n modal.getRoot().on(\n ModalEvents.cancel,\n () => {\n resolve(null);\n }\n );\n });\n }\n\n /**\n * Create a new modal and return a Promise to the body rendered.\n *\n * @param {Object} modalParams the modal params\n * @returns {Promise} the modal body rendered promise\n */\n modalBodyRenderedPromise(modalParams) {\n return new Promise((resolve, reject) => {\n ModalSaveCancel.create(modalParams).then((modal) => {\n modal.setRemoveOnClose(true);\n // Handle body loading event.\n modal.getRoot().on(ModalEvents.bodyRendered, () => {\n resolve(modal);\n });\n // Configure some extra modal params.\n if (modalParams.saveButtonText !== undefined) {\n modal.setSaveButtonText(modalParams.saveButtonText);\n }\n modal.show();\n return;\n }).catch(() => {\n reject(`Cannot load modal content`);\n });\n });\n }\n}\n\n/**\n * Add a section to refresh.\n *\n * @param {number} courseId the course id\n * @param {number} sectionId the seciton id\n */\nfunction addRefreshSection(courseId, sectionId) {\n let refresh = courseUpdates.get(courseId);\n if (!refresh) {\n refresh = new Set();\n }\n refresh.add(sectionId);\n courseUpdates.set(courseId, refresh);\n refreshCourseEditors();\n}\n\n/**\n * Debounced processing all pending course refreshes.\n * @private\n */\nconst refreshCourseEditors = debounce(\n () => {\n const refreshes = courseUpdates;\n courseUpdates = new Map();\n refreshes.forEach((sectionIds, courseId) => {\n const courseEditor = getCourseEditor(courseId);\n if (!courseEditor) {\n return;\n }\n courseEditor.dispatch('sectionState', [...sectionIds]);\n });\n },\n DEBOUNCETIMER\n);\n\n/**\n * Load and return the course handler manager instance.\n *\n * @param {Number} courseId the course Id to load\n * @returns {Promise} promise of the the loaded handleManager\n */\nasync function loadCourseHandlerManager(courseId) {\n if (handlerManagers[courseId] !== undefined) {\n return handlerManagers[courseId];\n }\n const handlerManager = new HandlerManager(courseId);\n await handlerManager.loadHandlers();\n handlerManagers[courseId] = handlerManager;\n return handlerManagers[courseId];\n}\n\n/**\n * Load all the erros messages at once in the module \"errors\" variable.\n * @param {Number} courseId the course id\n */\nasync function loadErrorStrings(courseId) {\n if (errors !== null) {\n return;\n }\n const courseEditor = getCourseEditor(courseId);\n const maxbytestext = courseEditor.get('course')?.maxbytestext ?? '0';\n\n errors = {};\n const allStrings = [\n {key: 'dndmaxbytes', component: 'core_error', param: {size: maxbytestext}},\n {key: 'dndread', component: 'core_error'},\n {key: 'dndupload', component: 'core_error'},\n {key: 'dndunkownfile', component: 'core_error'},\n ];\n\n const loadedStrings = await getStrings(allStrings);\n allStrings.forEach(({key}, index) => {\n errors[key] = loadedStrings[index];\n });\n}\n\n/**\n * Start a batch file uploading into the course.\n *\n * @private\n * @param {number} courseId the course id.\n * @param {number} sectionId the section id.\n * @param {number} sectionNum the section number.\n * @param {File} fileInfo the file information object\n * @param {HandlerManager} handlerManager the course handler manager\n */\nconst queueFileUpload = async function(courseId, sectionId, sectionNum, fileInfo, handlerManager) {\n let handler;\n uploadQueue = await processMonitor.createProcessQueue();\n try {\n handlerManager.validateFile(fileInfo);\n handler = await handlerManager.getFileHandler(fileInfo);\n } catch (error) {\n uploadQueue.addError(fileInfo.name, error.message);\n return;\n }\n // If we don't have a handler means the user cancel the upload.\n if (!handler) {\n return;\n }\n const fileProcessor = new FileUploader(courseId, sectionId, sectionNum, fileInfo, handler);\n uploadQueue.addPending(fileInfo.name, fileProcessor.getExecutionFunction());\n};\n\n/**\n * Upload a file to the course.\n *\n * This method will show any necesary modal to handle the request.\n *\n * @param {number} courseId the course id\n * @param {number} sectionId the section id\n * @param {number} sectionNum the section number\n * @param {Array} files and array of files\n */\nexport const uploadFilesToCourse = async function(courseId, sectionId, sectionNum, files) {\n // Get the course handlers.\n const handlerManager = await loadCourseHandlerManager(courseId);\n await loadErrorStrings(courseId);\n for (let index = 0; index < files.length; index++) {\n const fileInfo = files[index];\n await queueFileUpload(courseId, sectionId, sectionNum, fileInfo, handlerManager);\n }\n};\n"],"names":["UPLOADURL","Config","wwwroot","uploadQueue","handlerManagers","courseUpdates","Map","errors","FileUploader","constructor","courseId","sectionId","sectionNum","fileInfo","handler","execute","process","this","xhr","_createXhrRequest","formData","_createUploadFormData","reader","FileReader","onload","open","send","onerror","setError","dndread","size","readAsText","slice","getExecutionFunction","bind","XMLHttpRequest","upload","addEventListener","event","lengthComputable","percent","Math","round","loaded","total","setPercentage","onreadystatechange","readyState","status","result","JSON","parse","responseText","error","_finishProcess","dndupload","FormData","append","Error","sesskey","module","refresh","get","Set","add","set","refreshCourseEditors","addRefreshSection","finish","HandlerManager","lastUploadId","courseEditor","maxbytes","_this$courseEditor$ge2","allHandlers","getFileHandlersPromise","getFileExtension","extension","dotpos","name","lastIndexOf","substring","length","toLowerCase","validateFile","dndmaxbytes","filterHandlers","filter","fileHandlers","dndunkownfile","fileHandler","askHandlerToUser","modalParams","title","body","Templates","render","getModalData","lastHandlers","saveButtonText","modal","modalBodyRenderedPromise","selectedHandler","modalUserAnswerPromise","defaultModule","data","filename","uploadid","handlers","hasDefault","forEach","index","isDefault","push","selected","labelid","value","lastHandler","pop","modalBody","getBody","Promise","resolve","reject","getRoot","on","ModalEvents","save","querySelector","preventDefault","destroy","cancel","create","then","setRemoveOnClose","bodyRendered","undefined","setSaveButtonText","show","catch","refreshes","sectionIds","dispatch","queueFileUpload","async","handlerManager","processMonitor","createProcessQueue","getFileHandler","addError","message","fileProcessor","addPending","files","loadHandlers","loadCourseHandlerManager","maxbytestext","_courseEditor$get","allStrings","key","component","param","loadedStrings","loadErrorStrings"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"fileuploader.min.js","sources":["../../../src/local/courseeditor/fileuploader.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * The course file uploader.\n *\n * This module is used to upload files directly into the course.\n *\n * @module core_courseformat/local/courseeditor/fileuploader\n * @copyright 2022 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * @typedef {Object} Handler\n * @property {String} extension the handled extension or * for any\n * @property {String} message the handler message\n * @property {String} module the module name\n */\n\nimport Config from 'core/config';\nimport ModalSaveCancel from 'core/modal_save_cancel';\nimport ModalEvents from 'core/modal_events';\nimport Templates from 'core/templates';\nimport {getFirst} from 'core/normalise';\nimport {prefetchStrings} from 'core/prefetch';\nimport {getString, getStrings} from 'core/str';\nimport {getCourseEditor} from 'core_courseformat/courseeditor';\nimport {processMonitor} from 'core/process_monitor';\nimport {debounce} from 'core/utils';\n\n// Uploading url.\nconst UPLOADURL = Config.wwwroot + '/course/dndupload.php';\nconst DEBOUNCETIMER = 500;\nconst USERCANIGNOREFILESIZELIMITS = -1;\n\n/** @var {ProcessQueue} uploadQueue the internal uploadQueue instance. */\nlet uploadQueue = null;\n/** @var {Object} handlerManagers the courseId indexed loaded handler managers. */\nlet handlerManagers = {};\n/** @var {Map} courseUpdates the pending course sections updates. */\nlet courseUpdates = new Map();\n/** @var {Object} errors the error messages. */\nlet errors = null;\n\n// Load global strings.\nprefetchStrings('moodle', ['addresourceoractivity', 'upload']);\nprefetchStrings('core_error', ['dndmaxbytes', 'dndread', 'dndupload', 'dndunkownfile']);\n\n/**\n * Class to upload a file into the course.\n * @private\n */\nclass FileUploader {\n /**\n * Class constructor.\n *\n * @param {number} courseId the course id\n * @param {number} sectionId the section id\n * @param {number} sectionNum the section number\n * @param {File} fileInfo the file information object\n * @param {Handler} handler the file selected file handler\n */\n constructor(courseId, sectionId, sectionNum, fileInfo, handler) {\n this.courseId = courseId;\n this.sectionId = sectionId;\n this.sectionNum = sectionNum;\n this.fileInfo = fileInfo;\n this.handler = handler;\n }\n\n /**\n * Execute the file upload and update the state in the given process.\n *\n * @param {LoadingProcess} process the process to store the upload result\n */\n execute(process) {\n const fileInfo = this.fileInfo;\n const xhr = this._createXhrRequest(process);\n const formData = this._createUploadFormData();\n\n // Try reading the file to check it is not a folder, before sending it to the server.\n const reader = new FileReader();\n reader.onload = function() {\n // File was read OK - send it to the server.\n xhr.open(\"POST\", UPLOADURL, true);\n xhr.send(formData);\n };\n reader.onerror = function() {\n // Unable to read the file (it is probably a folder) - display an error message.\n process.setError(errors.dndread);\n };\n if (fileInfo.size > 0) {\n // If this is a non-empty file, try reading the first few bytes.\n // This will trigger reader.onerror() for folders and reader.onload() for ordinary, readable files.\n reader.readAsText(fileInfo.slice(0, 5));\n } else {\n // If you call slice() on a 0-byte folder, before calling readAsText, then Firefox triggers reader.onload(),\n // instead of reader.onerror().\n // So, for 0-byte files, just call readAsText on the whole file (and it will trigger load/error functions as expected).\n reader.readAsText(fileInfo);\n }\n }\n\n /**\n * Returns the bind version of execute function.\n *\n * This method is used to queue the process into a ProcessQueue instance.\n *\n * @returns {Function} the bind function to execute the process\n */\n getExecutionFunction() {\n return this.execute.bind(this);\n }\n\n /**\n * Generate a upload XHR file request.\n *\n * @param {LoadingProcess} process the current process\n * @return {XMLHttpRequest} the XHR request\n */\n _createXhrRequest(process) {\n const xhr = new XMLHttpRequest();\n // Update the progress bar as the file is uploaded.\n xhr.upload.addEventListener(\n 'progress',\n (event) => {\n if (event.lengthComputable) {\n const percent = Math.round((event.loaded * 100) / event.total);\n process.setPercentage(percent);\n }\n },\n false\n );\n // Wait for the AJAX call to complete.\n xhr.onreadystatechange = () => {\n if (xhr.readyState == 1) {\n // Add a 1% just to indicate that it is uploading.\n process.setPercentage(1);\n }\n // State 4 is DONE. Otherwise the connection is still ongoing.\n if (xhr.readyState != 4) {\n return;\n }\n if (xhr.status == 200) {\n var result = JSON.parse(xhr.responseText);\n if (result && result.error == 0) {\n // All OK.\n this._finishProcess(process);\n } else {\n process.setError(result.error);\n }\n } else {\n process.setError(errors.dndupload);\n }\n };\n return xhr;\n }\n\n /**\n * Upload a file into the course.\n *\n * @return {FormData|null} the new form data object\n */\n _createUploadFormData() {\n const formData = new FormData();\n try {\n formData.append('repo_upload_file', this.fileInfo);\n } catch (error) {\n throw Error(error.dndread);\n }\n formData.append('sesskey', Config.sesskey);\n formData.append('course', this.courseId);\n formData.append('section', this.sectionNum);\n formData.append('module', this.handler.module);\n formData.append('type', 'Files');\n return formData;\n }\n\n /**\n * Finishes the current process.\n * @param {LoadingProcess} process the process\n */\n _finishProcess(process) {\n addRefreshSection(this.courseId, this.sectionId);\n process.setPercentage(100);\n process.finish();\n }\n}\n\n/**\n * The file handler manager class.\n *\n * @private\n */\nclass HandlerManager {\n\n /** @var {Object} lastHandlers the last handlers selected per each file extension. */\n lastHandlers = {};\n\n /** @var {Handler[]|null} allHandlers all the available handlers. */\n allHandlers = null;\n\n /**\n * Class constructor.\n *\n * @param {Number} courseId\n */\n constructor(courseId) {\n this.courseId = courseId;\n this.lastUploadId = 0;\n this.courseEditor = getCourseEditor(courseId);\n if (!this.courseEditor) {\n throw Error('Unkown course editor');\n }\n this.maxbytes = this.courseEditor.get('course')?.maxbytes ?? 0;\n }\n\n /**\n * Load the course file handlers.\n */\n async loadHandlers() {\n this.allHandlers = await this.courseEditor.getFileHandlersPromise();\n }\n\n /**\n * Extract the file extension from a fileInfo.\n *\n * @param {File} fileInfo\n * @returns {String} the file extension or an empty string.\n */\n getFileExtension(fileInfo) {\n let extension = '';\n const dotpos = fileInfo.name.lastIndexOf('.');\n if (dotpos != -1) {\n extension = fileInfo.name.substring(dotpos + 1, fileInfo.name.length).toLowerCase();\n }\n return extension;\n }\n\n /**\n * Check if the file is valid.\n *\n * @param {File} fileInfo the file info\n */\n validateFile(fileInfo) {\n if (this.maxbytes !== USERCANIGNOREFILESIZELIMITS && fileInfo.size > this.maxbytes) {\n throw Error(errors.dndmaxbytes);\n }\n }\n\n /**\n * Get the file handlers of an specific file.\n *\n * @param {File} fileInfo the file indo\n * @return {Array} Array of handlers\n */\n filterHandlers(fileInfo) {\n const extension = this.getFileExtension(fileInfo);\n return this.allHandlers.filter(handler => handler.extension == '*' || handler.extension == extension);\n }\n\n /**\n * Get the Handler to upload a specific file.\n *\n * It will ask the used if more than one handler is available.\n *\n * @param {File} fileInfo the file info\n * @returns {Promise} the selected handler or null if the user cancel\n */\n async getFileHandler(fileInfo) {\n const fileHandlers = this.filterHandlers(fileInfo);\n if (fileHandlers.length == 0) {\n throw Error(errors.dndunkownfile);\n }\n let fileHandler = null;\n if (fileHandlers.length == 1) {\n fileHandler = fileHandlers[0];\n } else {\n fileHandler = await this.askHandlerToUser(fileHandlers, fileInfo);\n }\n return fileHandler;\n }\n\n /**\n * Ask the user to select a specific handler.\n *\n * @param {Handler[]} fileHandlers\n * @param {File} fileInfo the file info\n * @return {Promise} the selected handler\n */\n async askHandlerToUser(fileHandlers, fileInfo) {\n const extension = this.getFileExtension(fileInfo);\n // Build the modal parameters from the event data.\n const modalParams = {\n title: getString('addresourceoractivity', 'moodle'),\n body: Templates.render(\n 'core_courseformat/fileuploader',\n this.getModalData(\n fileHandlers,\n fileInfo,\n this.lastHandlers[extension] ?? null\n )\n ),\n saveButtonText: getString('upload', 'moodle'),\n };\n // Create the modal.\n const modal = await this.modalBodyRenderedPromise(modalParams);\n const selectedHandler = await this.modalUserAnswerPromise(modal, fileHandlers);\n // Cancel action.\n if (selectedHandler === null) {\n return null;\n }\n // Save last selected handler.\n this.lastHandlers[extension] = selectedHandler.module;\n return selectedHandler;\n }\n\n /**\n * Generated the modal template data.\n *\n * @param {Handler[]} fileHandlers\n * @param {File} fileInfo the file info\n * @param {String|null} defaultModule the default module if any\n * @return {Object} the modal template data.\n */\n getModalData(fileHandlers, fileInfo, defaultModule) {\n const data = {\n filename: fileInfo.name,\n uploadid: ++this.lastUploadId,\n handlers: [],\n };\n let hasDefault = false;\n fileHandlers.forEach((handler, index) => {\n const isDefault = (defaultModule == handler.module);\n const optionNumber = index + 1;\n data.handlers.push({\n ...handler,\n selected: isDefault,\n labelid: `fileuploader_${data.uploadid}_${optionNumber}`,\n value: index,\n });\n hasDefault = hasDefault || isDefault;\n });\n if (!hasDefault && data.handlers.length > 0) {\n const lastHandler = data.handlers.pop();\n lastHandler.selected = true;\n data.handlers.push(lastHandler);\n }\n return data;\n }\n\n /**\n * Get the user handler choice.\n *\n * Wait for the user answer in the modal and resolve with the selected index.\n *\n * @param {Modal} modal the modal instance\n * @param {Handler[]} fileHandlers the availabvle file handlers\n * @return {Promise} with the option selected by the user.\n */\n modalUserAnswerPromise(modal, fileHandlers) {\n const modalBody = getFirst(modal.getBody());\n return new Promise((resolve, reject) => {\n modal.getRoot().on(\n ModalEvents.save,\n event => {\n // Get the selected option.\n const index = modalBody.querySelector('input:checked').value;\n event.preventDefault();\n modal.destroy();\n if (!fileHandlers[index]) {\n reject('Invalid handler selected');\n }\n resolve(fileHandlers[index]);\n\n }\n );\n modal.getRoot().on(\n ModalEvents.cancel,\n () => {\n resolve(null);\n }\n );\n });\n }\n\n /**\n * Create a new modal and return a Promise to the body rendered.\n *\n * @param {Object} modalParams the modal params\n * @returns {Promise} the modal body rendered promise\n */\n modalBodyRenderedPromise(modalParams) {\n return new Promise((resolve, reject) => {\n ModalSaveCancel.create(modalParams).then((modal) => {\n modal.setRemoveOnClose(true);\n // Handle body loading event.\n modal.getRoot().on(ModalEvents.bodyRendered, () => {\n resolve(modal);\n });\n // Configure some extra modal params.\n if (modalParams.saveButtonText !== undefined) {\n modal.setSaveButtonText(modalParams.saveButtonText);\n }\n modal.show();\n return;\n }).catch(() => {\n reject(`Cannot load modal content`);\n });\n });\n }\n}\n\n/**\n * Add a section to refresh.\n *\n * @param {number} courseId the course id\n * @param {number} sectionId the seciton id\n */\nfunction addRefreshSection(courseId, sectionId) {\n let refresh = courseUpdates.get(courseId);\n if (!refresh) {\n refresh = new Set();\n }\n refresh.add(sectionId);\n courseUpdates.set(courseId, refresh);\n refreshCourseEditors();\n}\n\n/**\n * Debounced processing all pending course refreshes.\n * @private\n */\nconst refreshCourseEditors = debounce(\n () => {\n const refreshes = courseUpdates;\n courseUpdates = new Map();\n refreshes.forEach((sectionIds, courseId) => {\n const courseEditor = getCourseEditor(courseId);\n if (!courseEditor) {\n return;\n }\n courseEditor.dispatch('sectionState', [...sectionIds]);\n });\n },\n DEBOUNCETIMER\n);\n\n/**\n * Load and return the course handler manager instance.\n *\n * @param {Number} courseId the course Id to load\n * @returns {Promise} promise of the the loaded handleManager\n */\nasync function loadCourseHandlerManager(courseId) {\n if (handlerManagers[courseId] !== undefined) {\n return handlerManagers[courseId];\n }\n const handlerManager = new HandlerManager(courseId);\n await handlerManager.loadHandlers();\n handlerManagers[courseId] = handlerManager;\n return handlerManagers[courseId];\n}\n\n/**\n * Load all the erros messages at once in the module \"errors\" variable.\n * @param {Number} courseId the course id\n */\nasync function loadErrorStrings(courseId) {\n if (errors !== null) {\n return;\n }\n const courseEditor = getCourseEditor(courseId);\n const maxbytestext = courseEditor.get('course')?.maxbytestext ?? '0';\n\n errors = {};\n const allStrings = [\n {key: 'dndmaxbytes', component: 'core_error', param: {size: maxbytestext}},\n {key: 'dndread', component: 'core_error'},\n {key: 'dndupload', component: 'core_error'},\n {key: 'dndunkownfile', component: 'core_error'},\n ];\n\n const loadedStrings = await getStrings(allStrings);\n allStrings.forEach(({key}, index) => {\n errors[key] = loadedStrings[index];\n });\n}\n\n/**\n * Start a batch file uploading into the course.\n *\n * @private\n * @param {number} courseId the course id.\n * @param {number} sectionId the section id.\n * @param {number} sectionNum the section number.\n * @param {File} fileInfo the file information object\n * @param {HandlerManager} handlerManager the course handler manager\n */\nconst queueFileUpload = async function(courseId, sectionId, sectionNum, fileInfo, handlerManager) {\n let handler;\n uploadQueue = await processMonitor.createProcessQueue();\n try {\n handlerManager.validateFile(fileInfo);\n handler = await handlerManager.getFileHandler(fileInfo);\n } catch (error) {\n uploadQueue.addError(fileInfo.name, error.message);\n return;\n }\n // If we don't have a handler means the user cancel the upload.\n if (!handler) {\n return;\n }\n const fileProcessor = new FileUploader(courseId, sectionId, sectionNum, fileInfo, handler);\n uploadQueue.addPending(fileInfo.name, fileProcessor.getExecutionFunction());\n};\n\n/**\n * Upload a file to the course.\n *\n * This method will show any necesary modal to handle the request.\n *\n * @param {number} courseId the course id\n * @param {number} sectionId the section id\n * @param {number} sectionNum the section number\n * @param {Array} files and array of files\n */\nexport const uploadFilesToCourse = async function(courseId, sectionId, sectionNum, files) {\n // Get the course handlers.\n const handlerManager = await loadCourseHandlerManager(courseId);\n await loadErrorStrings(courseId);\n for (let index = 0; index < files.length; index++) {\n const fileInfo = files[index];\n await queueFileUpload(courseId, sectionId, sectionNum, fileInfo, handlerManager);\n }\n};\n"],"names":["UPLOADURL","Config","wwwroot","uploadQueue","handlerManagers","courseUpdates","Map","errors","FileUploader","constructor","courseId","sectionId","sectionNum","fileInfo","handler","execute","process","this","xhr","_createXhrRequest","formData","_createUploadFormData","reader","FileReader","onload","open","send","onerror","setError","dndread","size","readAsText","slice","getExecutionFunction","bind","XMLHttpRequest","upload","addEventListener","event","lengthComputable","percent","Math","round","loaded","total","setPercentage","onreadystatechange","readyState","status","result","JSON","parse","responseText","error","_finishProcess","dndupload","FormData","append","Error","sesskey","module","refresh","get","Set","add","set","refreshCourseEditors","addRefreshSection","finish","HandlerManager","lastUploadId","courseEditor","maxbytes","_this$courseEditor$ge2","allHandlers","getFileHandlersPromise","getFileExtension","extension","dotpos","name","lastIndexOf","substring","length","toLowerCase","validateFile","dndmaxbytes","filterHandlers","filter","fileHandlers","dndunkownfile","fileHandler","askHandlerToUser","modalParams","title","body","Templates","render","getModalData","lastHandlers","saveButtonText","modal","modalBodyRenderedPromise","selectedHandler","modalUserAnswerPromise","defaultModule","data","filename","uploadid","handlers","hasDefault","forEach","index","isDefault","optionNumber","push","selected","labelid","value","lastHandler","pop","modalBody","getBody","Promise","resolve","reject","getRoot","on","ModalEvents","save","querySelector","preventDefault","destroy","cancel","create","then","setRemoveOnClose","bodyRendered","undefined","setSaveButtonText","show","catch","refreshes","sectionIds","dispatch","queueFileUpload","async","handlerManager","processMonitor","createProcessQueue","getFileHandler","addError","message","fileProcessor","addPending","files","loadHandlers","loadCourseHandlerManager","maxbytestext","_courseEditor$get","allStrings","key","component","param","loadedStrings","loadErrorStrings"],"mappings":"46BA4CMA,UAAYC,gBAAOC,QAAU,4BAK/BC,YAAc,KAEdC,gBAAkB,GAElBC,cAAgB,IAAIC,IAEpBC,OAAS,mCAGG,SAAU,CAAC,wBAAyB,yCACpC,aAAc,CAAC,cAAe,UAAW,YAAa,wBAMhEC,aAUFC,YAAYC,SAAUC,UAAWC,WAAYC,SAAUC,cAC9CJ,SAAWA,cACXC,UAAYA,eACZC,WAAaA,gBACbC,SAAWA,cACXC,QAAUA,QAQnBC,QAAQC,eACEH,SAAWI,KAAKJ,SAChBK,IAAMD,KAAKE,kBAAkBH,SAC7BI,SAAWH,KAAKI,wBAGhBC,OAAS,IAAIC,WACnBD,OAAOE,OAAS,WAEZN,IAAIO,KAAK,OAAQzB,WAAW,GAC5BkB,IAAIQ,KAAKN,WAEbE,OAAOK,QAAU,WAEbX,QAAQY,SAASrB,OAAOsB,UAExBhB,SAASiB,KAAO,EAGhBR,OAAOS,WAAWlB,SAASmB,MAAM,EAAG,IAKpCV,OAAOS,WAAWlB,UAW1BoB,8BACWhB,KAAKF,QAAQmB,KAAKjB,MAS7BE,kBAAkBH,eACRE,IAAM,IAAIiB,sBAEhBjB,IAAIkB,OAAOC,iBACP,YACCC,WACOA,MAAMC,iBAAkB,OAClBC,QAAUC,KAAKC,MAAsB,IAAfJ,MAAMK,OAAgBL,MAAMM,OACxD5B,QAAQ6B,cAAcL,aAG9B,GAGJtB,IAAI4B,mBAAqB,QACC,GAAlB5B,IAAI6B,YAEJ/B,QAAQ6B,cAAc,GAGJ,GAAlB3B,IAAI6B,cAGU,KAAd7B,IAAI8B,OAAe,KACfC,OAASC,KAAKC,MAAMjC,IAAIkC,cACxBH,QAA0B,GAAhBA,OAAOI,WAEZC,eAAetC,SAEpBA,QAAQY,SAASqB,OAAOI,YAG5BrC,QAAQY,SAASrB,OAAOgD,YAGzBrC,IAQXG,8BACUD,SAAW,IAAIoC,aAEjBpC,SAASqC,OAAO,mBAAoBxC,KAAKJ,UAC3C,MAAOwC,aACCK,MAAML,MAAMxB,gBAEtBT,SAASqC,OAAO,UAAWxD,gBAAO0D,SAClCvC,SAASqC,OAAO,SAAUxC,KAAKP,UAC/BU,SAASqC,OAAO,UAAWxC,KAAKL,YAChCQ,SAASqC,OAAO,SAAUxC,KAAKH,QAAQ8C,QACvCxC,SAASqC,OAAO,OAAQ,SACjBrC,SAOXkC,eAAetC,mBA6OQN,SAAUC,eAC7BkD,QAAUxD,cAAcyD,IAAIpD,UAC3BmD,UACDA,QAAU,IAAIE,KAElBF,QAAQG,IAAIrD,WACZN,cAAc4D,IAAIvD,SAAUmD,SAC5BK,uBAnPIC,CAAkBlD,KAAKP,SAAUO,KAAKN,WACtCK,QAAQ6B,cAAc,KACtB7B,QAAQoD,gBASVC,eAaF5D,YAAYC,kGAVG,uCAGD,WAQLA,SAAWA,cACX4D,aAAe,OACfC,cAAe,iCAAgB7D,WAC/BO,KAAKsD,mBACAb,MAAM,6BAEXc,sEAAWvD,KAAKsD,aAAaT,IAAI,mDAAtBW,uBAAiCD,gEAAY,4BAOxDE,kBAAoBzD,KAAKsD,aAAaI,yBAS/CC,iBAAiB/D,cACTgE,UAAY,SACVC,OAASjE,SAASkE,KAAKC,YAAY,YAC1B,GAAXF,SACAD,UAAYhE,SAASkE,KAAKE,UAAUH,OAAS,EAAGjE,SAASkE,KAAKG,QAAQC,eAEnEN,UAQXO,aAAavE,cAnNmB,IAoNxBI,KAAKuD,UAA4C3D,SAASiB,KAAOb,KAAKuD,eAChEd,MAAMnD,OAAO8E,aAU3BC,eAAezE,gBACLgE,UAAY5D,KAAK2D,iBAAiB/D,iBACjCI,KAAKyD,YAAYa,QAAOzE,SAAgC,KAArBA,QAAQ+D,WAAoB/D,QAAQ+D,WAAaA,iCAW1EhE,gBACX2E,aAAevE,KAAKqE,eAAezE,aACd,GAAvB2E,aAAaN,aACPxB,MAAMnD,OAAOkF,mBAEnBC,YAAc,YAEdA,YADuB,GAAvBF,aAAaN,OACCM,aAAa,SAEPvE,KAAK0E,iBAAiBH,aAAc3E,UAErD6E,mCAUYF,aAAc3E,0CAC3BgE,UAAY5D,KAAK2D,iBAAiB/D,UAElC+E,YAAc,CAChBC,OAAO,kBAAU,wBAAyB,UAC1CC,KAAMC,mBAAUC,OACZ,iCACA/E,KAAKgF,aACDT,aACA3E,uCACAI,KAAKiF,aAAarB,kEAAc,OAGxCsB,gBAAgB,kBAAU,SAAU,WAGlCC,YAAcnF,KAAKoF,yBAAyBT,aAC5CU,sBAAwBrF,KAAKsF,uBAAuBH,MAAOZ,qBAEzC,OAApBc,gBACO,WAGNJ,aAAarB,WAAayB,gBAAgB1C,OACxC0C,iBAWXL,aAAaT,aAAc3E,SAAU2F,qBAC3BC,KAAO,CACTC,SAAU7F,SAASkE,KACnB4B,WAAY1F,KAAKqD,aACjBsC,SAAU,QAEVC,YAAa,KACjBrB,aAAasB,SAAQ,CAAChG,QAASiG,eACrBC,UAAaR,eAAiB1F,QAAQ8C,OACtCqD,aAAeF,MAAQ,EAC7BN,KAAKG,SAASM,KAAK,IACZpG,QACHqG,SAAUH,UACVI,+BAAyBX,KAAKE,qBAAYM,cAC1CI,MAAON,QAEXF,WAAaA,YAAcG,cAE1BH,YAAcJ,KAAKG,SAAS1B,OAAS,EAAG,OACnCoC,YAAcb,KAAKG,SAASW,MAClCD,YAAYH,UAAW,EACvBV,KAAKG,SAASM,KAAKI,oBAEhBb,KAYXF,uBAAuBH,MAAOZ,oBACpBgC,WAAY,uBAASpB,MAAMqB,kBAC1B,IAAIC,SAAQ,CAACC,QAASC,UACzBxB,MAAMyB,UAAUC,GACZC,sBAAYC,MACZ1F,cAEUyE,MAAQS,UAAUS,cAAc,iBAAiBZ,MACvD/E,MAAM4F,iBACN9B,MAAM+B,UACD3C,aAAauB,QACda,OAAO,4BAEXD,QAAQnC,aAAauB,WAI7BX,MAAMyB,UAAUC,GACZC,sBAAYK,QACZ,KACIT,QAAQ,YAYxBtB,yBAAyBT,oBACd,IAAI8B,SAAQ,CAACC,QAASC,qCACTS,OAAOzC,aAAa0C,MAAMlC,QACtCA,MAAMmC,kBAAiB,GAEvBnC,MAAMyB,UAAUC,GAAGC,sBAAYS,cAAc,KACzCb,QAAQvB,eAGuBqC,IAA/B7C,YAAYO,gBACZC,MAAMsC,kBAAkB9C,YAAYO,gBAExCC,MAAMuC,UAEPC,OAAM,KACLhB,iDA0BV1D,sBAAuB,oBACzB,WACU2E,UAAYxI,cAClBA,cAAgB,IAAIC,IACpBuI,UAAU/B,SAAQ,CAACgC,WAAYpI,kBACrB6D,cAAe,iCAAgB7D,UAChC6D,cAGLA,aAAawE,SAAS,eAAgB,IAAID,kBA1ZhC,WAmdhBE,gBAAkBC,eAAevI,SAAUC,UAAWC,WAAYC,SAAUqI,oBAC1EpI,QACJX,kBAAoBgJ,gCAAeC,yBAE/BF,eAAe9D,aAAavE,UAC5BC,cAAgBoI,eAAeG,eAAexI,UAChD,MAAOwC,mBACLlD,YAAYmJ,SAASzI,SAASkE,KAAM1B,MAAMkG,aAIzCzI,qBAGC0I,cAAgB,IAAIhJ,aAAaE,SAAUC,UAAWC,WAAYC,SAAUC,SAClFX,YAAYsJ,WAAW5I,SAASkE,KAAMyE,cAAcvH,sDAarBgH,eAAevI,SAAUC,UAAWC,WAAY8I,aAEzER,oCA3E8BxI,kBACF+H,IAA9BrI,gBAAgBM,iBACTN,gBAAgBM,gBAErBwI,eAAiB,IAAI7E,eAAe3D,uBACpCwI,eAAeS,eACrBvJ,gBAAgBM,UAAYwI,eACrB9I,gBAAgBM,UAoEMkJ,CAAyBlJ,+BA7D1BA,yDACb,OAAXH,oBAIEsJ,sEADe,iCAAgBnJ,UACHoD,IAAI,8CAAjBgG,kBAA4BD,oEAAgB,IAEjEtJ,OAAS,SACHwJ,WAAa,CACf,CAACC,IAAK,cAAeC,UAAW,aAAcC,MAAO,CAACpI,KAAM+H,eAC5D,CAACG,IAAK,UAAWC,UAAW,cAC5B,CAACD,IAAK,YAAaC,UAAW,cAC9B,CAACD,IAAK,gBAAiBC,UAAW,eAGhCE,oBAAsB,mBAAWJ,YACvCA,WAAWjD,SAAQ,MAAQC,aAAPiD,IAACA,UACjBzJ,OAAOyJ,KAAOG,cAAcpD,UA6C1BqD,CAAiB1J,cAClB,IAAIqG,MAAQ,EAAGA,MAAQ2C,MAAMxE,OAAQ6B,QAAS,OACzClG,SAAW6I,MAAM3C,aACjBiC,gBAAgBtI,SAAUC,UAAWC,WAAYC,SAAUqI"} \ No newline at end of file diff --git a/course/format/amd/build/local/courseindex/cm.min.js b/course/format/amd/build/local/courseindex/cm.min.js index 47f1a92579d12..d7ba008d456fb 100644 --- a/course/format/amd/build/local/courseindex/cm.min.js +++ b/course/format/amd/build/local/courseindex/cm.min.js @@ -8,6 +8,6 @@ define("core_courseformat/local/courseindex/cm",["exports","core_courseformat/lo * @class core_courseformat/local/courseindex/cm * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_dndcmitem=_interopRequireDefault(_dndcmitem),_templates=_interopRequireDefault(_templates),_prefetch=_interopRequireDefault(_prefetch),_config=_interopRequireDefault(_config),_pending=_interopRequireDefault(_pending);_prefetch.default.prefetchTemplate("core_courseformat/local/courseindex/cmcompletion");class Component extends _dndcmitem.default{create(){this.name="courseindex_cm",this.selectors={CM_NAME:"[data-for='cm_name']",CM_COMPLETION:"[data-for='cm_completion']"},this.classes={CMHIDDEN:"dimmed",LOCKED:"editinprogress",RESTRICTIONS:"restrictions",PAGEITEM:"pageitem",INDENTED:"indented"},this.id=this.element.dataset.id}static init(target,selectors){return new this({element:document.getElementById(target),selectors:selectors})}stateReady(state){this.configDragDrop(this.id);const cm=state.cm.get(this.id),course=state.course;this._refreshCompletion({state:state,element:cm});const anchor=new URL(window.location.href).hash.replace("#","");(window.location.href==cm.url||window.location.href.includes(course.baseurl)&&anchor==cm.anchor)&&this.element.scrollIntoView({block:"center"}),_config.default.contextid!=_config.default.courseContextId&&_config.default.contextInstanceId==this.id&&(this.reactive.dispatch("setPageItem","cm",this.id,!0),this.element.scrollIntoView({block:"center"})),cm.uservisible&&cm.url||this.addEventListener(this.getElement(this.selectors.CM_NAME),"click",this._activityAnchor)}getWatchers(){return[{watch:"cm[".concat(this.id,"]:deleted"),handler:this.remove},{watch:"cm[".concat(this.id,"]:updated"),handler:this._refreshCm},{watch:"cm[".concat(this.id,"].completionstate:updated"),handler:this._refreshCompletion},{watch:"course.pageItem:updated",handler:this._refreshPageItem}]}_refreshCm(_ref){var _element$dragging,_element$locked,_element$hascmrestric;let{element:element}=_ref;this.element.classList.toggle(this.classes.CMHIDDEN,!element.visible),this.getElement(this.selectors.CM_NAME).innerHTML=element.name,this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.element.classList.toggle(this.classes.RESTRICTIONS,null!==(_element$hascmrestric=element.hascmrestrictions)&&void 0!==_element$hascmrestric&&_element$hascmrestric),this.element.classList.toggle(this.classes.INDENTED,element.indent),this.locked=element.locked}_refreshPageItem(_ref2){let{element:element}=_ref2;if(!element.pageItem)return;const isPageId="cm"==element.pageItem.type&&element.pageItem.id==this.id;this.element.classList.toggle(this.classes.PAGEITEM,isPageId),isPageId&&!this.reactive.isEditing&&this.element.scrollIntoView({block:"nearest"})}async _refreshCompletion(_ref3){let{state:state,element:element}=_ref3;if(this.reactive.isEditing||!element.istrackeduser)return;const completionElement=this.getElement(this.selectors.CM_COMPLETION);if(completionElement.dataset.value==element.completionstate)return;const data=this.reactive.getExporter().cmCompletion(state,element),{html:html,js:js}=await _templates.default.renderForPromise("core_courseformat/local/courseindex/cmcompletion",data);_templates.default.replaceNode(completionElement,html,js)}_activityAnchor(event){const cm=this.reactive.get("cm",this.id);if(document.getElementById(cm.anchor)){this.reactive.dispatch("sectionContentCollapsed",[cm.sectionid],!1);const pendingAnchor=new _pending.default("courseformat/activity:openAnchor");return void setTimeout((()=>{this.reactive.dispatch("setPageItem","cm",cm.id),pendingAnchor.resolve()}),50)}const course=this.reactive.get("course"),section=this.reactive.get("section",cm.sectionid);if(!section)return;const url="".concat(course.baseurl,"§ion=").concat(section.number,"#").concat(cm.anchor);event.preventDefault(),window.location=url}}return _exports.default=Component,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_dndcmitem=_interopRequireDefault(_dndcmitem),_templates=_interopRequireDefault(_templates),_prefetch=_interopRequireDefault(_prefetch),_config=_interopRequireDefault(_config),_pending=_interopRequireDefault(_pending);_prefetch.default.prefetchTemplate("core_courseformat/local/courseindex/cmcompletion");class Component extends _dndcmitem.default{create(){this.name="courseindex_cm",this.selectors={CM_NAME:"[data-for='cm_name']",CM_COMPLETION:"[data-for='cm_completion']"},this.classes={CMHIDDEN:"dimmed",LOCKED:"editinprogress",RESTRICTIONS:"restrictions",PAGEITEM:"pageitem",INDENTED:"indented"},this.id=this.element.dataset.id}static init(target,selectors){return new this({element:document.getElementById(target),selectors:selectors})}stateReady(state){this.configDragDrop(this.id);const cm=state.cm.get(this.id),course=state.course;this._refreshCompletion({state:state,element:cm});const anchor=new URL(window.location.href).hash.replace("#","");if((window.location.href==cm.url||window.location.href.includes(course.baseurl)&&anchor==cm.anchor)&&this.element.scrollIntoView({block:"center"}),_config.default.contextid!=_config.default.courseContextId&&_config.default.contextInstanceId==this.id&&(this.reactive.dispatch("setPageItem","cm",this.id,!0),this.element.scrollIntoView({block:"center"})),!cm.uservisible||!cm.url){const element=this.getElement(this.selectors.CM_NAME);this.addEventListener(element,"click",this._activityAnchor),document.getElementById(cm.anchor)||element.setAttribute("href",this._getActivitySectionURL(cm))}}getWatchers(){return[{watch:"cm[".concat(this.id,"]:deleted"),handler:this.remove},{watch:"cm[".concat(this.id,"]:updated"),handler:this._refreshCm},{watch:"cm[".concat(this.id,"].completionstate:updated"),handler:this._refreshCompletion},{watch:"course.pageItem:updated",handler:this._refreshPageItem}]}_refreshCm(_ref){var _element$dragging,_element$locked,_element$hascmrestric;let{element:element}=_ref;this.element.classList.toggle(this.classes.CMHIDDEN,!element.visible),this.getElement(this.selectors.CM_NAME).innerHTML=element.name,this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.element.classList.toggle(this.classes.RESTRICTIONS,null!==(_element$hascmrestric=element.hascmrestrictions)&&void 0!==_element$hascmrestric&&_element$hascmrestric),this.element.classList.toggle(this.classes.INDENTED,element.indent),this.locked=element.locked}_refreshPageItem(_ref2){let{element:element}=_ref2;if(!element.pageItem)return;const isPageId="cm"==element.pageItem.type&&element.pageItem.id==this.id;this.element.classList.toggle(this.classes.PAGEITEM,isPageId),isPageId&&!this.reactive.isEditing&&this.element.scrollIntoView({block:"nearest"})}async _refreshCompletion(_ref3){let{state:state,element:element}=_ref3;if(this.reactive.isEditing||!element.istrackeduser)return;const completionElement=this.getElement(this.selectors.CM_COMPLETION);if(completionElement.dataset.value==element.completionstate)return;const data=this.reactive.getExporter().cmCompletion(state,element),{html:html,js:js}=await _templates.default.renderForPromise("core_courseformat/local/courseindex/cmcompletion",data);_templates.default.replaceNode(completionElement,html,js)}_activityAnchor(event){const cm=this.reactive.get("cm",this.id);if(document.getElementById(cm.anchor)){this.reactive.dispatch("sectionContentCollapsed",[cm.sectionid],!1);const pendingAnchor=new _pending.default("courseformat/activity:openAnchor");setTimeout((()=>{this.reactive.dispatch("setPageItem","cm",cm.id),pendingAnchor.resolve()}),50)}else event.preventDefault(),window.location=this._getActivitySectionURL(cm)}_getActivitySectionURL(cm){const section=this.reactive.get("section",cm.sectionid);return section?"".concat(section.sectionurl,"#").concat(cm.anchor):"#"}}return _exports.default=Component,_exports.default})); //# sourceMappingURL=cm.min.js.map \ No newline at end of file diff --git a/course/format/amd/build/local/courseindex/cm.min.js.map b/course/format/amd/build/local/courseindex/cm.min.js.map index 6fff4994e3055..25ea6be056e3c 100644 --- a/course/format/amd/build/local/courseindex/cm.min.js.map +++ b/course/format/amd/build/local/courseindex/cm.min.js.map @@ -1 +1 @@ -{"version":3,"file":"cm.min.js","sources":["../../../src/local/courseindex/cm.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Course index cm component.\n *\n * This component is used to control specific course modules interactions like drag and drop.\n *\n * @module core_courseformat/local/courseindex/cm\n * @class core_courseformat/local/courseindex/cm\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport DndCmItem from 'core_courseformat/local/courseeditor/dndcmitem';\nimport Templates from 'core/templates';\nimport Prefetch from 'core/prefetch';\nimport Config from 'core/config';\nimport Pending from \"core/pending\";\n\n// Prefetch the completion icons template.\nconst completionTemplate = 'core_courseformat/local/courseindex/cmcompletion';\nPrefetch.prefetchTemplate(completionTemplate);\n\nexport default class Component extends DndCmItem {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex_cm';\n // Default query selectors.\n this.selectors = {\n CM_NAME: `[data-for='cm_name']`,\n CM_COMPLETION: `[data-for='cm_completion']`,\n };\n // Default classes to toggle on refresh.\n this.classes = {\n CMHIDDEN: 'dimmed',\n LOCKED: 'editinprogress',\n RESTRICTIONS: 'restrictions',\n PAGEITEM: 'pageitem',\n INDENTED: 'indented',\n };\n // We need our id to watch specific events.\n this.id = this.element.dataset.id;\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.getElementById(target),\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the course state.\n */\n stateReady(state) {\n this.configDragDrop(this.id);\n const cm = state.cm.get(this.id);\n const course = state.course;\n // Refresh completion icon.\n this._refreshCompletion({\n state,\n element: cm,\n });\n const url = new URL(window.location.href);\n const anchor = url.hash.replace('#', '');\n // Check if the current url is the cm url.\n if (window.location.href == cm.url\n || (window.location.href.includes(course.baseurl) && anchor == cm.anchor)\n ) {\n this.element.scrollIntoView({block: \"center\"});\n }\n // Check if this we are displaying this activity page.\n if (Config.contextid != Config.courseContextId && Config.contextInstanceId == this.id) {\n this.reactive.dispatch('setPageItem', 'cm', this.id, true);\n this.element.scrollIntoView({block: \"center\"});\n }\n // Add anchor logic if the element is not user visible or the element hasn't URL.\n if (!cm.uservisible || !cm.url) {\n this.addEventListener(\n this.getElement(this.selectors.CM_NAME),\n 'click',\n this._activityAnchor,\n );\n }\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `cm[${this.id}]:deleted`, handler: this.remove},\n {watch: `cm[${this.id}]:updated`, handler: this._refreshCm},\n {watch: `cm[${this.id}].completionstate:updated`, handler: this._refreshCompletion},\n {watch: `course.pageItem:updated`, handler: this._refreshPageItem},\n ];\n }\n\n /**\n * Update a course index cm using the state information.\n *\n * @param {object} param\n * @param {Object} param.element details the update details.\n */\n _refreshCm({element}) {\n // Update classes.\n this.element.classList.toggle(this.classes.CMHIDDEN, !element.visible);\n this.getElement(this.selectors.CM_NAME).innerHTML = element.name;\n this.element.classList.toggle(this.classes.DRAGGING, element.dragging ?? false);\n this.element.classList.toggle(this.classes.LOCKED, element.locked ?? false);\n this.element.classList.toggle(this.classes.RESTRICTIONS, element.hascmrestrictions ?? false);\n this.element.classList.toggle(this.classes.INDENTED, element.indent);\n this.locked = element.locked;\n }\n\n /**\n * Handle a page item update.\n *\n * @param {Object} details the update details\n * @param {Object} details.element the course state data.\n */\n _refreshPageItem({element}) {\n if (!element.pageItem) {\n return;\n }\n const isPageId = (element.pageItem.type == 'cm' && element.pageItem.id == this.id);\n this.element.classList.toggle(this.classes.PAGEITEM, isPageId);\n if (isPageId && !this.reactive.isEditing) {\n this.element.scrollIntoView({block: \"nearest\"});\n }\n }\n\n /**\n * Update the activity completion icon.\n *\n * @param {Object} details the update details\n * @param {Object} details.state the state data\n * @param {Object} details.element the element data\n */\n async _refreshCompletion({state, element}) {\n // No completion icons are displayed in edit mode.\n if (this.reactive.isEditing || !element.istrackeduser) {\n return;\n }\n // Check if the completion value has changed.\n const completionElement = this.getElement(this.selectors.CM_COMPLETION);\n if (completionElement.dataset.value == element.completionstate) {\n return;\n }\n\n // Collect section information from the state.\n const exporter = this.reactive.getExporter();\n const data = exporter.cmCompletion(state, element);\n\n const {html, js} = await Templates.renderForPromise(completionTemplate, data);\n Templates.replaceNode(completionElement, html, js);\n }\n\n /**\n * The activity anchor event.\n *\n * @param {Event} event\n */\n _activityAnchor(event) {\n const cm = this.reactive.get('cm', this.id);\n // If the user cannot access the element but the element is present in the page\n // the new url should be an anchor link.\n const element = document.getElementById(cm.anchor);\n if (element) {\n // Make sure the section is expanded.\n this.reactive.dispatch('sectionContentCollapsed', [cm.sectionid], false);\n // Marc the element as page item once the event is handled.\n const pendingAnchor = new Pending(`courseformat/activity:openAnchor`);\n setTimeout(() => {\n this.reactive.dispatch('setPageItem', 'cm', cm.id);\n pendingAnchor.resolve();\n }, 50);\n return;\n }\n // If the element is not present in the page we need to go to the specific section.\n const course = this.reactive.get('course');\n const section = this.reactive.get('section', cm.sectionid);\n if (!section) {\n return;\n }\n const url = `${course.baseurl}§ion=${section.number}#${cm.anchor}`;\n event.preventDefault();\n window.location = url;\n }\n}\n"],"names":["prefetchTemplate","Component","DndCmItem","create","name","selectors","CM_NAME","CM_COMPLETION","classes","CMHIDDEN","LOCKED","RESTRICTIONS","PAGEITEM","INDENTED","id","this","element","dataset","target","document","getElementById","stateReady","state","configDragDrop","cm","get","course","_refreshCompletion","anchor","URL","window","location","href","hash","replace","url","includes","baseurl","scrollIntoView","block","Config","contextid","courseContextId","contextInstanceId","reactive","dispatch","uservisible","addEventListener","getElement","_activityAnchor","getWatchers","watch","handler","remove","_refreshCm","_refreshPageItem","classList","toggle","visible","innerHTML","DRAGGING","dragging","locked","hascmrestrictions","indent","pageItem","isPageId","type","isEditing","istrackeduser","completionElement","value","completionstate","data","getExporter","cmCompletion","html","js","Templates","renderForPromise","replaceNode","event","sectionid","pendingAnchor","Pending","setTimeout","resolve","section","number","preventDefault"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"cm.min.js","sources":["../../../src/local/courseindex/cm.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Course index cm component.\n *\n * This component is used to control specific course modules interactions like drag and drop.\n *\n * @module core_courseformat/local/courseindex/cm\n * @class core_courseformat/local/courseindex/cm\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport DndCmItem from 'core_courseformat/local/courseeditor/dndcmitem';\nimport Templates from 'core/templates';\nimport Prefetch from 'core/prefetch';\nimport Config from 'core/config';\nimport Pending from \"core/pending\";\n\n// Prefetch the completion icons template.\nconst completionTemplate = 'core_courseformat/local/courseindex/cmcompletion';\nPrefetch.prefetchTemplate(completionTemplate);\n\nexport default class Component extends DndCmItem {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex_cm';\n // Default query selectors.\n this.selectors = {\n CM_NAME: `[data-for='cm_name']`,\n CM_COMPLETION: `[data-for='cm_completion']`,\n };\n // Default classes to toggle on refresh.\n this.classes = {\n CMHIDDEN: 'dimmed',\n LOCKED: 'editinprogress',\n RESTRICTIONS: 'restrictions',\n PAGEITEM: 'pageitem',\n INDENTED: 'indented',\n };\n // We need our id to watch specific events.\n this.id = this.element.dataset.id;\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.getElementById(target),\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the course state.\n */\n stateReady(state) {\n this.configDragDrop(this.id);\n const cm = state.cm.get(this.id);\n const course = state.course;\n // Refresh completion icon.\n this._refreshCompletion({\n state,\n element: cm,\n });\n const url = new URL(window.location.href);\n const anchor = url.hash.replace('#', '');\n // Check if the current url is the cm url.\n if (window.location.href == cm.url\n || (window.location.href.includes(course.baseurl) && anchor == cm.anchor)\n ) {\n this.element.scrollIntoView({block: \"center\"});\n }\n // Check if this we are displaying this activity page.\n if (Config.contextid != Config.courseContextId && Config.contextInstanceId == this.id) {\n this.reactive.dispatch('setPageItem', 'cm', this.id, true);\n this.element.scrollIntoView({block: \"center\"});\n }\n // Add anchor logic if the element is not user visible or the element hasn't URL.\n if (!cm.uservisible || !cm.url) {\n const element = this.getElement(this.selectors.CM_NAME);\n this.addEventListener(\n element,\n 'click',\n this._activityAnchor,\n );\n // If the element is not user visible we also need to update the anchor link including the section page.\n if (!document.getElementById(cm.anchor)) {\n element.setAttribute('href', this._getActivitySectionURL(cm));\n }\n }\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `cm[${this.id}]:deleted`, handler: this.remove},\n {watch: `cm[${this.id}]:updated`, handler: this._refreshCm},\n {watch: `cm[${this.id}].completionstate:updated`, handler: this._refreshCompletion},\n {watch: `course.pageItem:updated`, handler: this._refreshPageItem},\n ];\n }\n\n /**\n * Update a course index cm using the state information.\n *\n * @param {object} param\n * @param {Object} param.element details the update details.\n */\n _refreshCm({element}) {\n // Update classes.\n this.element.classList.toggle(this.classes.CMHIDDEN, !element.visible);\n this.getElement(this.selectors.CM_NAME).innerHTML = element.name;\n this.element.classList.toggle(this.classes.DRAGGING, element.dragging ?? false);\n this.element.classList.toggle(this.classes.LOCKED, element.locked ?? false);\n this.element.classList.toggle(this.classes.RESTRICTIONS, element.hascmrestrictions ?? false);\n this.element.classList.toggle(this.classes.INDENTED, element.indent);\n this.locked = element.locked;\n }\n\n /**\n * Handle a page item update.\n *\n * @param {Object} details the update details\n * @param {Object} details.element the course state data.\n */\n _refreshPageItem({element}) {\n if (!element.pageItem) {\n return;\n }\n const isPageId = (element.pageItem.type == 'cm' && element.pageItem.id == this.id);\n this.element.classList.toggle(this.classes.PAGEITEM, isPageId);\n if (isPageId && !this.reactive.isEditing) {\n this.element.scrollIntoView({block: \"nearest\"});\n }\n }\n\n /**\n * Update the activity completion icon.\n *\n * @param {Object} details the update details\n * @param {Object} details.state the state data\n * @param {Object} details.element the element data\n */\n async _refreshCompletion({state, element}) {\n // No completion icons are displayed in edit mode.\n if (this.reactive.isEditing || !element.istrackeduser) {\n return;\n }\n // Check if the completion value has changed.\n const completionElement = this.getElement(this.selectors.CM_COMPLETION);\n if (completionElement.dataset.value == element.completionstate) {\n return;\n }\n\n // Collect section information from the state.\n const exporter = this.reactive.getExporter();\n const data = exporter.cmCompletion(state, element);\n\n const {html, js} = await Templates.renderForPromise(completionTemplate, data);\n Templates.replaceNode(completionElement, html, js);\n }\n\n /**\n * The activity anchor event.\n *\n * @param {Event} event\n */\n _activityAnchor(event) {\n const cm = this.reactive.get('cm', this.id);\n // If the user cannot access the element but the element is present in the page\n // the new url should be an anchor link.\n const element = document.getElementById(cm.anchor);\n if (element) {\n // Make sure the section is expanded.\n this.reactive.dispatch('sectionContentCollapsed', [cm.sectionid], false);\n // Marc the element as page item once the event is handled.\n const pendingAnchor = new Pending(`courseformat/activity:openAnchor`);\n setTimeout(() => {\n this.reactive.dispatch('setPageItem', 'cm', cm.id);\n pendingAnchor.resolve();\n }, 50);\n return;\n }\n // If the element is not present in the page we need to go to the specific section.\n event.preventDefault();\n window.location = this._getActivitySectionURL(cm);\n }\n\n /**\n * Get the anchor link in section page for the cm.\n *\n * @param {Object} cm the course module data.\n * @return {String} the anchor link.\n */\n _getActivitySectionURL(cm) {\n const section = this.reactive.get('section', cm.sectionid);\n if (!section) {\n return '#';\n }\n\n return `${section.sectionurl}#${cm.anchor}`;\n }\n}\n"],"names":["prefetchTemplate","Component","DndCmItem","create","name","selectors","CM_NAME","CM_COMPLETION","classes","CMHIDDEN","LOCKED","RESTRICTIONS","PAGEITEM","INDENTED","id","this","element","dataset","target","document","getElementById","stateReady","state","configDragDrop","cm","get","course","_refreshCompletion","anchor","URL","window","location","href","hash","replace","url","includes","baseurl","scrollIntoView","block","Config","contextid","courseContextId","contextInstanceId","reactive","dispatch","uservisible","getElement","addEventListener","_activityAnchor","setAttribute","_getActivitySectionURL","getWatchers","watch","handler","remove","_refreshCm","_refreshPageItem","classList","toggle","visible","innerHTML","DRAGGING","dragging","locked","hascmrestrictions","indent","pageItem","isPageId","type","isEditing","istrackeduser","completionElement","value","completionstate","data","getExporter","cmCompletion","html","js","Templates","renderForPromise","replaceNode","event","sectionid","pendingAnchor","Pending","setTimeout","resolve","preventDefault","section","sectionurl"],"mappings":"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"} \ No newline at end of file diff --git a/course/format/amd/build/local/courseindex/courseindex.min.js b/course/format/amd/build/local/courseindex/courseindex.min.js index 9ad6f2fe452ca..0f6f864a3c90b 100644 --- a/course/format/amd/build/local/courseindex/courseindex.min.js +++ b/course/format/amd/build/local/courseindex/courseindex.min.js @@ -6,6 +6,6 @@ define("core_courseformat/local/courseindex/courseindex",["exports","core/reacti * @class core_courseformat/local/courseindex/courseindex * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),_contenttree=_interopRequireDefault(_contenttree);class Component extends _reactive.BaseComponent{create(){this.name="courseindex",this.selectors={SECTION:"[data-for='section']",SECTION_CMLIST:"[data-for='cmlist']",CM:"[data-for='cm']",TOGGLER:'[data-action="togglecourseindexsection"]',COLLAPSE:'[data-toggle="collapse"]',DRAWER:".drawer"},this.classes={SECTIONHIDDEN:"dimmed",CMHIDDEN:"dimmed",SECTIONCURRENT:"current",COLLAPSED:"collapsed",SHOW:"show"},this.sections={},this.cms={}}static init(target,selectors){return new this({element:document.getElementById(target),reactive:(0,_courseeditor.getCurrentCourseEditor)(),selectors:selectors})}stateReady(state){this.addEventListener(this.element,"click",this._sectionTogglers);this.getElements(this.selectors.SECTION).forEach((section=>{this.sections[section.dataset.id]=section}));this.getElements(this.selectors.CM).forEach((cm=>{this.cms[cm.dataset.id]=cm})),this._expandPageCmSectionIfNecessary(state),this._refreshPageItem({element:state.course,state:state}),this.contentTree=new _contenttree.default(this.element,this.selectors,this.reactive.isEditing)}getWatchers(){return[{watch:"section.indexcollapsed:updated",handler:this._refreshSectionCollapsed},{watch:"cm:created",handler:this._createCm},{watch:"cm:deleted",handler:this._deleteCm},{watch:"section:created",handler:this._createSection},{watch:"section:deleted",handler:this._deleteSection},{watch:"course.pageItem:created",handler:this._refreshPageItem},{watch:"course.pageItem:updated",handler:this._refreshPageItem},{watch:"course.sectionlist:updated",handler:this._refreshCourseSectionlist},{watch:"section.cmlist:updated",handler:this._refreshSectionCmlist}]}_sectionTogglers(event){const sectionlink=event.target.closest(this.selectors.TOGGLER),isChevron=event.target.closest(this.selectors.COLLAPSE);if(sectionlink||isChevron){var _toggler$classList$co;const section=event.target.closest(this.selectors.SECTION),toggler=section.querySelector(this.selectors.COLLAPSE),isCollapsed=null!==(_toggler$classList$co=null==toggler?void 0:toggler.classList.contains(this.classes.COLLAPSED))&&void 0!==_toggler$classList$co&&_toggler$classList$co,sectionId=section.getAttribute("data-id");sectionlink&&!isCollapsed||this.reactive.dispatch("sectionIndexCollapsed",[sectionId],!isCollapsed)}}_refreshSectionCollapsed(_ref){var _toggler$classList$co2;let{element:element}=_ref;const target=this.getElement(this.selectors.SECTION,element.id);if(!target)throw new Error("Unkown section with ID ".concat(element.id));const toggler=target.querySelector(this.selectors.COLLAPSE),isCollapsed=null!==(_toggler$classList$co2=null==toggler?void 0:toggler.classList.contains(this.classes.COLLAPSED))&&void 0!==_toggler$classList$co2&&_toggler$classList$co2;element.indexcollapsed!==isCollapsed&&this._expandSectionNode(element)}_expandSectionNode(element,forceValue){var _toggler$dataset$targ;const toggler=this.getElement(this.selectors.SECTION,element.id).querySelector(this.selectors.COLLAPSE);let collapsibleId=null!==(_toggler$dataset$targ=toggler.dataset.target)&&void 0!==_toggler$dataset$targ?_toggler$dataset$targ:toggler.getAttribute("href");if(!collapsibleId)return;collapsibleId=collapsibleId.replace("#","");const collapsible=document.getElementById(collapsibleId);if(!collapsible)return;void 0===forceValue&&(forceValue=!element.indexcollapsed);const togglerValue=forceValue?"show":"hide";(0,_jquery.default)(collapsible).collapse(togglerValue)}_refreshPageItem(_ref2){var _element$pageItem;let{element:element,state:state}=_ref2;if(null==element||null===(_element$pageItem=element.pageItem)||void 0===_element$pageItem||!_element$pageItem.isStatic||"cm"!=element.pageItem.type)return;const section=state.section.get(element.pageItem.sectionId);section.indexcollapsed&&(this._expandSectionNode(section,!0),setTimeout((()=>{var _this$cms$element$pag;return null===(_this$cms$element$pag=this.cms[element.pageItem.id])||void 0===_this$cms$element$pag?void 0:_this$cms$element$pag.scrollIntoView({block:"nearest"})}),250))}_expandPageCmSectionIfNecessary(state){const pageCmInfo=this.reactive.getPageAnchorCmInfo();pageCmInfo&&this._expandSectionNode(state.section.get(pageCmInfo.sectionid),!0)}async _createCm(_ref3){let{state:state,element:element}=_ref3;const fakeelement=document.createElement("li");fakeelement.classList.add("bg-pulse-grey","w-100"),fakeelement.innerHTML=" ",this.cms[element.id]=fakeelement,this._refreshSectionCmlist({state:state,element:state.section.get(element.sectionid)});const data=this.reactive.getExporter().cm(state,element),newelement=(await this.renderComponent(fakeelement,"core_courseformat/local/courseindex/cm",data)).getElement();this.cms[element.id]=newelement,fakeelement.parentNode.replaceChild(newelement,fakeelement)}async _createSection(_ref4){let{state:state,element:element}=_ref4;const fakeelement=document.createElement("div");fakeelement.classList.add("bg-pulse-grey","w-100"),fakeelement.innerHTML=" ",this.sections[element.id]=fakeelement,this._refreshCourseSectionlist({state:state,element:state.course});const data=this.reactive.getExporter().section(state,element),newelement=(await this.renderComponent(fakeelement,"core_courseformat/local/courseindex/section",data)).getElement();this.sections[element.id]=newelement,fakeelement.parentNode.replaceChild(newelement,fakeelement)}_refreshSectionCmlist(_ref5){var _element$cmlist;let{element:element}=_ref5;const cmlist=null!==(_element$cmlist=element.cmlist)&&void 0!==_element$cmlist?_element$cmlist:[],listparent=this.getElement(this.selectors.SECTION_CMLIST,element.id);this._fixOrder(listparent,cmlist,this.cms)}_refreshCourseSectionlist(_ref6){let{state:state}=_ref6;const sectionlist=this.reactive.getExporter().listedSectionIds(state);this._fixOrder(this.element,sectionlist,this.sections)}_fixOrder(container,neworder,allitems){if(!neworder.length)return container.classList.add("hidden"),void(container.innerHTML="");for(container.classList.remove("hidden"),neworder.forEach(((itemid,index)=>{const item=allitems[itemid],currentitem=container.children[index];void 0!==currentitem?currentitem!==item&&item&&container.insertBefore(item,currentitem):container.append(item)}));container.children.length>neworder.length;)container.removeChild(container.lastChild)}_deleteCm(_ref7){let{element:element}=_ref7;delete this.cms[element.id]}_deleteSection(_ref8){let{element:element}=_ref8;delete this.sections[element.id]}}return _exports.default=Component,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),_contenttree=_interopRequireDefault(_contenttree);class Component extends _reactive.BaseComponent{create(){this.name="courseindex",this.selectors={SECTION:"[data-for='section']",SECTION_CMLIST:"[data-for='cmlist']",CM:"[data-for='cm']",TOGGLER:'[data-action="togglecourseindexsection"]',COLLAPSE:'[data-toggle="collapse"]',DRAWER:".drawer"},this.classes={SECTIONHIDDEN:"dimmed",CMHIDDEN:"dimmed",SECTIONCURRENT:"current",COLLAPSED:"collapsed",SHOW:"show"},this.sections={},this.cms={}}static init(target,selectors){return new this({element:document.getElementById(target),reactive:(0,_courseeditor.getCurrentCourseEditor)(),selectors:selectors})}stateReady(state){this.addEventListener(this.element,"click",this._sectionTogglers);this.getElements(this.selectors.SECTION).forEach((section=>{this.sections[section.dataset.id]=section}));this.getElements(this.selectors.CM).forEach((cm=>{this.cms[cm.dataset.id]=cm})),this._expandPageCmSectionIfNecessary(state),this._refreshPageItem({element:state.course,state:state}),this.contentTree=new _contenttree.default(this.element,this.selectors,this.reactive.isEditing)}getWatchers(){return[{watch:"section.indexcollapsed:updated",handler:this._refreshSectionCollapsed},{watch:"cm:created",handler:this._createCm},{watch:"cm:deleted",handler:this._deleteCm},{watch:"section:created",handler:this._createSection},{watch:"section:deleted",handler:this._deleteSection},{watch:"course.pageItem:created",handler:this._refreshPageItem},{watch:"course.pageItem:updated",handler:this._refreshPageItem},{watch:"course.sectionlist:updated",handler:this._refreshCourseSectionlist},{watch:"section.cmlist:updated",handler:this._refreshSectionCmlist}]}_sectionTogglers(event){const sectionlink=event.target.closest(this.selectors.TOGGLER),isChevron=event.target.closest(this.selectors.COLLAPSE);if(sectionlink||isChevron){var _toggler$classList$co;const section=event.target.closest(this.selectors.SECTION),toggler=section.querySelector(this.selectors.COLLAPSE),isCollapsed=null!==(_toggler$classList$co=null==toggler?void 0:toggler.classList.contains(this.classes.COLLAPSED))&&void 0!==_toggler$classList$co&&_toggler$classList$co,sectionId=section.getAttribute("data-id");sectionlink&&!isCollapsed||this.reactive.dispatch("sectionIndexCollapsed",[sectionId],!isCollapsed)}}_refreshSectionCollapsed(_ref){var _toggler$classList$co2;let{element:element}=_ref;const target=this.getElement(this.selectors.SECTION,element.id);if(!target)throw new Error("Unkown section with ID ".concat(element.id));const toggler=target.querySelector(this.selectors.COLLAPSE),isCollapsed=null!==(_toggler$classList$co2=null==toggler?void 0:toggler.classList.contains(this.classes.COLLAPSED))&&void 0!==_toggler$classList$co2&&_toggler$classList$co2;element.indexcollapsed!==isCollapsed&&this._expandSectionNode(element)}_expandSectionNode(element,forceValue){var _toggler$dataset$targ;const toggler=this.getElement(this.selectors.SECTION,element.id).querySelector(this.selectors.COLLAPSE);let collapsibleId=null!==(_toggler$dataset$targ=toggler.dataset.target)&&void 0!==_toggler$dataset$targ?_toggler$dataset$targ:toggler.getAttribute("href");if(!collapsibleId)return;collapsibleId=collapsibleId.replace("#","");const collapsible=document.getElementById(collapsibleId);if(!collapsible)return;void 0===forceValue&&(forceValue=!element.indexcollapsed);const togglerValue=forceValue?"show":"hide";(0,_jquery.default)(collapsible).collapse(togglerValue)}_refreshPageItem(_ref2){var _element$pageItem;let{element:element,state:state}=_ref2;if(null==element||null===(_element$pageItem=element.pageItem)||void 0===_element$pageItem||!_element$pageItem.isStatic||"cm"!=element.pageItem.type)return;const section=state.section.get(element.pageItem.sectionId);section.indexcollapsed&&(this._expandSectionNode(section,!0),setTimeout((()=>{var _this$cms$element$pag;return null===(_this$cms$element$pag=this.cms[element.pageItem.id])||void 0===_this$cms$element$pag?void 0:_this$cms$element$pag.scrollIntoView({block:"nearest"})}),250))}_expandPageCmSectionIfNecessary(state){const pageCmInfo=this.reactive.getPageAnchorCmInfo();pageCmInfo&&this._expandSectionNode(state.section.get(pageCmInfo.sectionid),!0)}async _createCm(_ref3){let{state:state,element:element}=_ref3;const fakeelement=document.createElement("li");fakeelement.classList.add("bg-pulse-grey","w-100"),fakeelement.innerHTML=" ",this.cms[element.id]=fakeelement,this._refreshSectionCmlist({state:state,element:state.section.get(element.sectionid)});const data=this.reactive.getExporter().cm(state,element),newelement=(await this.renderComponent(fakeelement,"core_courseformat/local/courseindex/cm",data)).getElement();this.cms[element.id]=newelement,fakeelement.parentNode.replaceChild(newelement,fakeelement)}async _createSection(_ref4){let{state:state,element:element}=_ref4;const fakeelement=document.createElement("div");fakeelement.classList.add("bg-pulse-grey","w-100"),fakeelement.innerHTML=" ",this.sections[element.id]=fakeelement,this._refreshCourseSectionlist({state:state,element:state.course});const data=this.reactive.getExporter().section(state,element),newelement=(await this.renderComponent(fakeelement,"core_courseformat/local/courseindex/section",data)).getElement();this.sections[element.id]=newelement,fakeelement.parentNode.replaceChild(newelement,fakeelement)}_refreshSectionCmlist(_ref5){var _element$cmlist;let{element:element}=_ref5;const cmlist=null!==(_element$cmlist=element.cmlist)&&void 0!==_element$cmlist?_element$cmlist:[],listparent=this.getElement(this.selectors.SECTION_CMLIST,element.id);this._fixOrder(listparent,cmlist,this.cms)}_refreshCourseSectionlist(_ref6){let{state:state}=_ref6;const sectionlist=this.reactive.getExporter().listedSectionIds(state);this._fixOrder(this.element,sectionlist,this.sections)}_fixOrder(container,neworder,allitems){if(!neworder.length)return container.classList.add("hidden"),void(container.innerHTML="");for(container.classList.remove("hidden"),neworder.forEach(((itemid,index)=>{const item=allitems[itemid],currentitem=container.children[index];void 0!==currentitem||null==item?currentitem!==item&&item&&container.insertBefore(item,currentitem):container.append(item)}));container.children.length>neworder.length;)container.removeChild(container.lastChild)}_deleteCm(_ref7){let{element:element}=_ref7;delete this.cms[element.id]}_deleteSection(_ref8){let{element:element}=_ref8;delete this.sections[element.id]}}return _exports.default=Component,_exports.default})); //# sourceMappingURL=courseindex.min.js.map \ No newline at end of file diff --git a/course/format/amd/build/local/courseindex/courseindex.min.js.map b/course/format/amd/build/local/courseindex/courseindex.min.js.map index 9301b660f0be4..63e1057c0f1f1 100644 --- a/course/format/amd/build/local/courseindex/courseindex.min.js.map +++ b/course/format/amd/build/local/courseindex/courseindex.min.js.map @@ -1 +1 @@ -{"version":3,"file":"courseindex.min.js","sources":["../../../src/local/courseindex/courseindex.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Course index main component.\n *\n * @module core_courseformat/local/courseindex/courseindex\n * @class core_courseformat/local/courseindex/courseindex\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\nimport jQuery from 'jquery';\nimport ContentTree from 'core_courseformat/local/courseeditor/contenttree';\n\nexport default class Component extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex';\n // Default query selectors.\n this.selectors = {\n SECTION: `[data-for='section']`,\n SECTION_CMLIST: `[data-for='cmlist']`,\n CM: `[data-for='cm']`,\n TOGGLER: `[data-action=\"togglecourseindexsection\"]`,\n COLLAPSE: `[data-toggle=\"collapse\"]`,\n DRAWER: `.drawer`,\n };\n // Default classes to toggle on refresh.\n this.classes = {\n SECTIONHIDDEN: 'dimmed',\n CMHIDDEN: 'dimmed',\n SECTIONCURRENT: 'current',\n COLLAPSED: `collapsed`,\n SHOW: `show`,\n };\n // Arrays to keep cms and sections elements.\n this.sections = {};\n this.cms = {};\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.getElementById(target),\n reactive: getCurrentCourseEditor(),\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the state data\n */\n stateReady(state) {\n // Activate section togglers.\n this.addEventListener(this.element, 'click', this._sectionTogglers);\n\n // Get cms and sections elements.\n const sections = this.getElements(this.selectors.SECTION);\n sections.forEach((section) => {\n this.sections[section.dataset.id] = section;\n });\n const cms = this.getElements(this.selectors.CM);\n cms.forEach((cm) => {\n this.cms[cm.dataset.id] = cm;\n });\n\n this._expandPageCmSectionIfNecessary(state);\n this._refreshPageItem({element: state.course, state});\n\n // Configure Aria Tree.\n this.contentTree = new ContentTree(this.element, this.selectors, this.reactive.isEditing);\n }\n\n getWatchers() {\n return [\n {watch: `section.indexcollapsed:updated`, handler: this._refreshSectionCollapsed},\n {watch: `cm:created`, handler: this._createCm},\n {watch: `cm:deleted`, handler: this._deleteCm},\n {watch: `section:created`, handler: this._createSection},\n {watch: `section:deleted`, handler: this._deleteSection},\n {watch: `course.pageItem:created`, handler: this._refreshPageItem},\n {watch: `course.pageItem:updated`, handler: this._refreshPageItem},\n // Sections and cm sorting.\n {watch: `course.sectionlist:updated`, handler: this._refreshCourseSectionlist},\n {watch: `section.cmlist:updated`, handler: this._refreshSectionCmlist},\n ];\n }\n\n /**\n * Setup sections toggler.\n *\n * Toggler click is delegated to the main course index element because new sections can\n * appear at any moment and this way we prevent accidental double bindings.\n *\n * @param {Event} event the triggered event\n */\n _sectionTogglers(event) {\n const sectionlink = event.target.closest(this.selectors.TOGGLER);\n const isChevron = event.target.closest(this.selectors.COLLAPSE);\n\n if (sectionlink || isChevron) {\n\n const section = event.target.closest(this.selectors.SECTION);\n const toggler = section.querySelector(this.selectors.COLLAPSE);\n const isCollapsed = toggler?.classList.contains(this.classes.COLLAPSED) ?? false;\n\n // Update the state.\n const sectionId = section.getAttribute('data-id');\n if (!sectionlink || isCollapsed) {\n this.reactive.dispatch(\n 'sectionIndexCollapsed',\n [sectionId],\n !isCollapsed\n );\n }\n }\n }\n\n /**\n * Update section collapsed.\n *\n * @param {object} args\n * @param {object} args.element The leement to be expanded\n */\n _refreshSectionCollapsed({element}) {\n const target = this.getElement(this.selectors.SECTION, element.id);\n if (!target) {\n throw new Error(`Unkown section with ID ${element.id}`);\n }\n // Check if it is already done.\n const toggler = target.querySelector(this.selectors.COLLAPSE);\n const isCollapsed = toggler?.classList.contains(this.classes.COLLAPSED) ?? false;\n\n if (element.indexcollapsed !== isCollapsed) {\n this._expandSectionNode(element);\n }\n }\n\n /**\n * Expand a section node.\n *\n * By default the method will use element.indexcollapsed to decide if the\n * section is opened or closed. However, using forceValue it is possible\n * to open or close a section independant from the indexcollapsed attribute.\n *\n * @param {Object} element the course module state element\n * @param {boolean} forceValue optional forced expanded value\n */\n _expandSectionNode(element, forceValue) {\n const target = this.getElement(this.selectors.SECTION, element.id);\n const toggler = target.querySelector(this.selectors.COLLAPSE);\n let collapsibleId = toggler.dataset.target ?? toggler.getAttribute(\"href\");\n if (!collapsibleId) {\n return;\n }\n collapsibleId = collapsibleId.replace('#', '');\n const collapsible = document.getElementById(collapsibleId);\n if (!collapsible) {\n return;\n }\n\n if (forceValue === undefined) {\n forceValue = (element.indexcollapsed) ? false : true;\n }\n\n // Course index is based on Bootstrap 4 collapsibles. To collapse them we need jQuery to\n // interact with collapsibles methods. Hopefully, this will change in Bootstrap 5 because\n // it does not require jQuery anymore (when MDL-71979 is integrated).\n const togglerValue = (forceValue) ? 'show' : 'hide';\n jQuery(collapsible).collapse(togglerValue);\n }\n\n /**\n * Handle a page item update.\n *\n * @param {Object} details the update details\n * @param {Object} details.state the state data.\n * @param {Object} details.element the course state data.\n */\n _refreshPageItem({element, state}) {\n if (!element?.pageItem?.isStatic || element.pageItem.type != 'cm') {\n return;\n }\n // Check if we need to uncollapse the section and scroll to the element.\n const section = state.section.get(element.pageItem.sectionId);\n if (section.indexcollapsed) {\n this._expandSectionNode(section, true);\n setTimeout(\n () => this.cms[element.pageItem.id]?.scrollIntoView({block: \"nearest\"}),\n 250\n );\n }\n }\n\n /**\n * Expand a section if the current page is a section's cm.\n *\n * @private\n * @param {Object} state the course state.\n */\n _expandPageCmSectionIfNecessary(state) {\n const pageCmInfo = this.reactive.getPageAnchorCmInfo();\n if (!pageCmInfo) {\n return;\n }\n this._expandSectionNode(state.section.get(pageCmInfo.sectionid), true);\n }\n\n /**\n * Create a newcm instance.\n *\n * @param {object} param\n * @param {Object} param.state\n * @param {Object} param.element\n */\n async _createCm({state, element}) {\n // Create a fake node while the component is loading.\n const fakeelement = document.createElement('li');\n fakeelement.classList.add('bg-pulse-grey', 'w-100');\n fakeelement.innerHTML = ' ';\n this.cms[element.id] = fakeelement;\n // Place the fake node on the correct position.\n this._refreshSectionCmlist({\n state,\n element: state.section.get(element.sectionid),\n });\n // Collect render data.\n const exporter = this.reactive.getExporter();\n const data = exporter.cm(state, element);\n // Create the new content.\n const newcomponent = await this.renderComponent(fakeelement, 'core_courseformat/local/courseindex/cm', data);\n // Replace the fake node with the real content.\n const newelement = newcomponent.getElement();\n this.cms[element.id] = newelement;\n fakeelement.parentNode.replaceChild(newelement, fakeelement);\n }\n\n /**\n * Create a new section instance.\n *\n * @param {Object} details the update details.\n * @param {Object} details.state the state data.\n * @param {Object} details.element the element data.\n */\n async _createSection({state, element}) {\n // Create a fake node while the component is loading.\n const fakeelement = document.createElement('div');\n fakeelement.classList.add('bg-pulse-grey', 'w-100');\n fakeelement.innerHTML = ' ';\n this.sections[element.id] = fakeelement;\n // Place the fake node on the correct position.\n this._refreshCourseSectionlist({\n state,\n element: state.course,\n });\n // Collect render data.\n const exporter = this.reactive.getExporter();\n const data = exporter.section(state, element);\n // Create the new content.\n const newcomponent = await this.renderComponent(fakeelement, 'core_courseformat/local/courseindex/section', data);\n // Replace the fake node with the real content.\n const newelement = newcomponent.getElement();\n this.sections[element.id] = newelement;\n fakeelement.parentNode.replaceChild(newelement, fakeelement);\n }\n\n /**\n * Refresh a section cm list.\n *\n * @param {object} param\n * @param {Object} param.element\n */\n _refreshSectionCmlist({element}) {\n const cmlist = element.cmlist ?? [];\n const listparent = this.getElement(this.selectors.SECTION_CMLIST, element.id);\n this._fixOrder(listparent, cmlist, this.cms);\n }\n\n /**\n * Refresh the section list.\n *\n * @param {object} param\n * @param {Object} param.state\n */\n _refreshCourseSectionlist({state}) {\n const sectionlist = this.reactive.getExporter().listedSectionIds(state);\n this._fixOrder(this.element, sectionlist, this.sections);\n }\n\n /**\n * Fix/reorder the section or cms order.\n *\n * @param {Element} container the HTML element to reorder.\n * @param {Array} neworder an array with the ids order\n * @param {Array} allitems the list of html elements that can be placed in the container\n */\n _fixOrder(container, neworder, allitems) {\n\n // Empty lists should not be visible.\n if (!neworder.length) {\n container.classList.add('hidden');\n container.innerHTML = '';\n return;\n }\n\n // Grant the list is visible (in case it was empty).\n container.classList.remove('hidden');\n\n // Move the elements in order at the beginning of the list.\n neworder.forEach((itemid, index) => {\n const item = allitems[itemid];\n // Get the current element at that position.\n const currentitem = container.children[index];\n if (currentitem === undefined) {\n container.append(item);\n return;\n }\n if (currentitem !== item && item) {\n container.insertBefore(item, currentitem);\n }\n });\n // Remove the remaining elements.\n while (container.children.length > neworder.length) {\n container.removeChild(container.lastChild);\n }\n }\n\n /**\n * Remove a cm from the list.\n *\n * The actual DOM element removal is delegated to the cm component.\n *\n * @param {object} param\n * @param {Object} param.element\n */\n _deleteCm({element}) {\n delete this.cms[element.id];\n }\n\n /**\n * Remove a section from the list.\n *\n * The actual DOM element removal is delegated to the section component.\n *\n * @param {Object} details the update details.\n * @param {Object} details.element the element data.\n */\n _deleteSection({element}) {\n delete this.sections[element.id];\n }\n}\n"],"names":["Component","BaseComponent","create","name","selectors","SECTION","SECTION_CMLIST","CM","TOGGLER","COLLAPSE","DRAWER","classes","SECTIONHIDDEN","CMHIDDEN","SECTIONCURRENT","COLLAPSED","SHOW","sections","cms","target","this","element","document","getElementById","reactive","stateReady","state","addEventListener","_sectionTogglers","getElements","forEach","section","dataset","id","cm","_expandPageCmSectionIfNecessary","_refreshPageItem","course","contentTree","ContentTree","isEditing","getWatchers","watch","handler","_refreshSectionCollapsed","_createCm","_deleteCm","_createSection","_deleteSection","_refreshCourseSectionlist","_refreshSectionCmlist","event","sectionlink","closest","isChevron","toggler","querySelector","isCollapsed","classList","contains","sectionId","getAttribute","dispatch","getElement","Error","indexcollapsed","_expandSectionNode","forceValue","collapsibleId","replace","collapsible","undefined","togglerValue","collapse","pageItem","_element$pageItem","isStatic","type","get","setTimeout","_this$cms$element$pag","scrollIntoView","block","pageCmInfo","getPageAnchorCmInfo","sectionid","fakeelement","createElement","add","innerHTML","data","getExporter","newelement","renderComponent","parentNode","replaceChild","cmlist","listparent","_fixOrder","sectionlist","listedSectionIds","container","neworder","allitems","length","remove","itemid","index","item","currentitem","children","insertBefore","append","removeChild","lastChild"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"courseindex.min.js","sources":["../../../src/local/courseindex/courseindex.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Course index main component.\n *\n * @module core_courseformat/local/courseindex/courseindex\n * @class core_courseformat/local/courseindex/courseindex\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\nimport jQuery from 'jquery';\nimport ContentTree from 'core_courseformat/local/courseeditor/contenttree';\n\nexport default class Component extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex';\n // Default query selectors.\n this.selectors = {\n SECTION: `[data-for='section']`,\n SECTION_CMLIST: `[data-for='cmlist']`,\n CM: `[data-for='cm']`,\n TOGGLER: `[data-action=\"togglecourseindexsection\"]`,\n COLLAPSE: `[data-toggle=\"collapse\"]`,\n DRAWER: `.drawer`,\n };\n // Default classes to toggle on refresh.\n this.classes = {\n SECTIONHIDDEN: 'dimmed',\n CMHIDDEN: 'dimmed',\n SECTIONCURRENT: 'current',\n COLLAPSED: `collapsed`,\n SHOW: `show`,\n };\n // Arrays to keep cms and sections elements.\n this.sections = {};\n this.cms = {};\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.getElementById(target),\n reactive: getCurrentCourseEditor(),\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the state data\n */\n stateReady(state) {\n // Activate section togglers.\n this.addEventListener(this.element, 'click', this._sectionTogglers);\n\n // Get cms and sections elements.\n const sections = this.getElements(this.selectors.SECTION);\n sections.forEach((section) => {\n this.sections[section.dataset.id] = section;\n });\n const cms = this.getElements(this.selectors.CM);\n cms.forEach((cm) => {\n this.cms[cm.dataset.id] = cm;\n });\n\n this._expandPageCmSectionIfNecessary(state);\n this._refreshPageItem({element: state.course, state});\n\n // Configure Aria Tree.\n this.contentTree = new ContentTree(this.element, this.selectors, this.reactive.isEditing);\n }\n\n getWatchers() {\n return [\n {watch: `section.indexcollapsed:updated`, handler: this._refreshSectionCollapsed},\n {watch: `cm:created`, handler: this._createCm},\n {watch: `cm:deleted`, handler: this._deleteCm},\n {watch: `section:created`, handler: this._createSection},\n {watch: `section:deleted`, handler: this._deleteSection},\n {watch: `course.pageItem:created`, handler: this._refreshPageItem},\n {watch: `course.pageItem:updated`, handler: this._refreshPageItem},\n // Sections and cm sorting.\n {watch: `course.sectionlist:updated`, handler: this._refreshCourseSectionlist},\n {watch: `section.cmlist:updated`, handler: this._refreshSectionCmlist},\n ];\n }\n\n /**\n * Setup sections toggler.\n *\n * Toggler click is delegated to the main course index element because new sections can\n * appear at any moment and this way we prevent accidental double bindings.\n *\n * @param {Event} event the triggered event\n */\n _sectionTogglers(event) {\n const sectionlink = event.target.closest(this.selectors.TOGGLER);\n const isChevron = event.target.closest(this.selectors.COLLAPSE);\n\n if (sectionlink || isChevron) {\n\n const section = event.target.closest(this.selectors.SECTION);\n const toggler = section.querySelector(this.selectors.COLLAPSE);\n const isCollapsed = toggler?.classList.contains(this.classes.COLLAPSED) ?? false;\n\n // Update the state.\n const sectionId = section.getAttribute('data-id');\n if (!sectionlink || isCollapsed) {\n this.reactive.dispatch(\n 'sectionIndexCollapsed',\n [sectionId],\n !isCollapsed\n );\n }\n }\n }\n\n /**\n * Update section collapsed.\n *\n * @param {object} args\n * @param {object} args.element The leement to be expanded\n */\n _refreshSectionCollapsed({element}) {\n const target = this.getElement(this.selectors.SECTION, element.id);\n if (!target) {\n throw new Error(`Unkown section with ID ${element.id}`);\n }\n // Check if it is already done.\n const toggler = target.querySelector(this.selectors.COLLAPSE);\n const isCollapsed = toggler?.classList.contains(this.classes.COLLAPSED) ?? false;\n\n if (element.indexcollapsed !== isCollapsed) {\n this._expandSectionNode(element);\n }\n }\n\n /**\n * Expand a section node.\n *\n * By default the method will use element.indexcollapsed to decide if the\n * section is opened or closed. However, using forceValue it is possible\n * to open or close a section independant from the indexcollapsed attribute.\n *\n * @param {Object} element the course module state element\n * @param {boolean} forceValue optional forced expanded value\n */\n _expandSectionNode(element, forceValue) {\n const target = this.getElement(this.selectors.SECTION, element.id);\n const toggler = target.querySelector(this.selectors.COLLAPSE);\n let collapsibleId = toggler.dataset.target ?? toggler.getAttribute(\"href\");\n if (!collapsibleId) {\n return;\n }\n collapsibleId = collapsibleId.replace('#', '');\n const collapsible = document.getElementById(collapsibleId);\n if (!collapsible) {\n return;\n }\n\n if (forceValue === undefined) {\n forceValue = (element.indexcollapsed) ? false : true;\n }\n\n // Course index is based on Bootstrap 4 collapsibles. To collapse them we need jQuery to\n // interact with collapsibles methods. Hopefully, this will change in Bootstrap 5 because\n // it does not require jQuery anymore (when MDL-71979 is integrated).\n const togglerValue = (forceValue) ? 'show' : 'hide';\n jQuery(collapsible).collapse(togglerValue);\n }\n\n /**\n * Handle a page item update.\n *\n * @param {Object} details the update details\n * @param {Object} details.state the state data.\n * @param {Object} details.element the course state data.\n */\n _refreshPageItem({element, state}) {\n if (!element?.pageItem?.isStatic || element.pageItem.type != 'cm') {\n return;\n }\n // Check if we need to uncollapse the section and scroll to the element.\n const section = state.section.get(element.pageItem.sectionId);\n if (section.indexcollapsed) {\n this._expandSectionNode(section, true);\n setTimeout(\n () => this.cms[element.pageItem.id]?.scrollIntoView({block: \"nearest\"}),\n 250\n );\n }\n }\n\n /**\n * Expand a section if the current page is a section's cm.\n *\n * @private\n * @param {Object} state the course state.\n */\n _expandPageCmSectionIfNecessary(state) {\n const pageCmInfo = this.reactive.getPageAnchorCmInfo();\n if (!pageCmInfo) {\n return;\n }\n this._expandSectionNode(state.section.get(pageCmInfo.sectionid), true);\n }\n\n /**\n * Create a newcm instance.\n *\n * @param {object} param\n * @param {Object} param.state\n * @param {Object} param.element\n */\n async _createCm({state, element}) {\n // Create a fake node while the component is loading.\n const fakeelement = document.createElement('li');\n fakeelement.classList.add('bg-pulse-grey', 'w-100');\n fakeelement.innerHTML = ' ';\n this.cms[element.id] = fakeelement;\n // Place the fake node on the correct position.\n this._refreshSectionCmlist({\n state,\n element: state.section.get(element.sectionid),\n });\n // Collect render data.\n const exporter = this.reactive.getExporter();\n const data = exporter.cm(state, element);\n // Create the new content.\n const newcomponent = await this.renderComponent(fakeelement, 'core_courseformat/local/courseindex/cm', data);\n // Replace the fake node with the real content.\n const newelement = newcomponent.getElement();\n this.cms[element.id] = newelement;\n fakeelement.parentNode.replaceChild(newelement, fakeelement);\n }\n\n /**\n * Create a new section instance.\n *\n * @param {Object} details the update details.\n * @param {Object} details.state the state data.\n * @param {Object} details.element the element data.\n */\n async _createSection({state, element}) {\n // Create a fake node while the component is loading.\n const fakeelement = document.createElement('div');\n fakeelement.classList.add('bg-pulse-grey', 'w-100');\n fakeelement.innerHTML = ' ';\n this.sections[element.id] = fakeelement;\n // Place the fake node on the correct position.\n this._refreshCourseSectionlist({\n state,\n element: state.course,\n });\n // Collect render data.\n const exporter = this.reactive.getExporter();\n const data = exporter.section(state, element);\n // Create the new content.\n const newcomponent = await this.renderComponent(fakeelement, 'core_courseformat/local/courseindex/section', data);\n // Replace the fake node with the real content.\n const newelement = newcomponent.getElement();\n this.sections[element.id] = newelement;\n fakeelement.parentNode.replaceChild(newelement, fakeelement);\n }\n\n /**\n * Refresh a section cm list.\n *\n * @param {object} param\n * @param {Object} param.element\n */\n _refreshSectionCmlist({element}) {\n const cmlist = element.cmlist ?? [];\n const listparent = this.getElement(this.selectors.SECTION_CMLIST, element.id);\n this._fixOrder(listparent, cmlist, this.cms);\n }\n\n /**\n * Refresh the section list.\n *\n * @param {object} param\n * @param {Object} param.state\n */\n _refreshCourseSectionlist({state}) {\n const sectionlist = this.reactive.getExporter().listedSectionIds(state);\n this._fixOrder(this.element, sectionlist, this.sections);\n }\n\n /**\n * Fix/reorder the section or cms order.\n *\n * @param {Element} container the HTML element to reorder.\n * @param {Array} neworder an array with the ids order\n * @param {Array} allitems the list of html elements that can be placed in the container\n */\n _fixOrder(container, neworder, allitems) {\n\n // Empty lists should not be visible.\n if (!neworder.length) {\n container.classList.add('hidden');\n container.innerHTML = '';\n return;\n }\n\n // Grant the list is visible (in case it was empty).\n container.classList.remove('hidden');\n\n // Move the elements in order at the beginning of the list.\n neworder.forEach((itemid, index) => {\n const item = allitems[itemid];\n // Get the current element at that position.\n const currentitem = container.children[index];\n if (currentitem === undefined && item != undefined) {\n container.append(item);\n return;\n }\n if (currentitem !== item && item) {\n container.insertBefore(item, currentitem);\n }\n });\n // Remove the remaining elements.\n while (container.children.length > neworder.length) {\n container.removeChild(container.lastChild);\n }\n }\n\n /**\n * Remove a cm from the list.\n *\n * The actual DOM element removal is delegated to the cm component.\n *\n * @param {object} param\n * @param {Object} param.element\n */\n _deleteCm({element}) {\n delete this.cms[element.id];\n }\n\n /**\n * Remove a section from the list.\n *\n * The actual DOM element removal is delegated to the section component.\n *\n * @param {Object} details the update details.\n * @param {Object} details.element the element data.\n */\n _deleteSection({element}) {\n delete this.sections[element.id];\n }\n}\n"],"names":["Component","BaseComponent","create","name","selectors","SECTION","SECTION_CMLIST","CM","TOGGLER","COLLAPSE","DRAWER","classes","SECTIONHIDDEN","CMHIDDEN","SECTIONCURRENT","COLLAPSED","SHOW","sections","cms","target","this","element","document","getElementById","reactive","stateReady","state","addEventListener","_sectionTogglers","getElements","forEach","section","dataset","id","cm","_expandPageCmSectionIfNecessary","_refreshPageItem","course","contentTree","ContentTree","isEditing","getWatchers","watch","handler","_refreshSectionCollapsed","_createCm","_deleteCm","_createSection","_deleteSection","_refreshCourseSectionlist","_refreshSectionCmlist","event","sectionlink","closest","isChevron","toggler","querySelector","isCollapsed","classList","contains","sectionId","getAttribute","dispatch","getElement","Error","indexcollapsed","_expandSectionNode","forceValue","collapsibleId","replace","collapsible","undefined","togglerValue","collapse","pageItem","_element$pageItem","isStatic","type","get","setTimeout","_this$cms$element$pag","scrollIntoView","block","pageCmInfo","getPageAnchorCmInfo","sectionid","fakeelement","createElement","add","innerHTML","data","getExporter","newelement","renderComponent","parentNode","replaceChild","cmlist","listparent","_fixOrder","sectionlist","listedSectionIds","container","neworder","allitems","length","remove","itemid","index","item","currentitem","children","insertBefore","append","removeChild","lastChild"],"mappings":"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"} \ No newline at end of file diff --git a/course/format/amd/build/local/courseindex/section.min.js b/course/format/amd/build/local/courseindex/section.min.js index d84ee14128877..fbc40ea1f17aa 100644 --- a/course/format/amd/build/local/courseindex/section.min.js +++ b/course/format/amd/build/local/courseindex/section.min.js @@ -8,6 +8,6 @@ define("core_courseformat/local/courseindex/section",["exports","core_courseform * @class core_courseformat/local/courseindex/section * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_sectiontitle=_interopRequireDefault(_sectiontitle),_dndsection=_interopRequireDefault(_dndsection);class Component extends _dndsection.default{create(){this.name="courseindex_section",this.selectors={SECTION_ITEM:"[data-for='section_item']",SECTION_TITLE:"[data-for='section_title']",CM_LAST:'[data-for="cm"]:last-child'},this.classes={SECTIONHIDDEN:"dimmed",SECTIONCURRENT:"current",LOCKED:"editinprogress",RESTRICTIONS:"restrictions",PAGEITEM:"pageitem",OVERLAYBORDERS:"overlay-preview-borders"},this.id=this.element.dataset.id,this.isPageItem=!1}static init(target,selectors){return new this({element:document.getElementById(target),selectors:selectors})}stateReady(state){this.configState(state);const sectionItem=this.getElement(this.selectors.SECTION_ITEM);if(this.reactive.isEditing&&this.reactive.supportComponents){const titleitem=new _sectiontitle.default({...this,element:sectionItem,fullregion:this.element});this.configDragDrop(titleitem)}const section=state.section.get(this.id);window.location.href==section.sectionurl.replace(/&/g,"&")&&(this.reactive.dispatch("setPageItem","section",this.id),sectionItem.scrollIntoView())}getWatchers(){return[{watch:"section[".concat(this.id,"]:deleted"),handler:this.remove},{watch:"section[".concat(this.id,"]:updated"),handler:this._refreshSection},{watch:"course.pageItem:updated",handler:this._refreshPageItem}]}getLastCm(){return this.getElement(this.selectors.CM_LAST)}_refreshSection(_ref){var _element$hasrestricti,_element$dragging,_element$locked;let{element:element}=_ref;const sectionItem=this.getElement(this.selectors.SECTION_ITEM);sectionItem.classList.toggle(this.classes.SECTIONHIDDEN,!element.visible),sectionItem.classList.toggle(this.classes.RESTRICTIONS,null!==(_element$hasrestricti=element.hasrestrictions)&&void 0!==_element$hasrestricti&&_element$hasrestricti),this.element.classList.toggle(this.classes.SECTIONCURRENT,element.current),this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.locked=element.locked,this.getElement(this.selectors.SECTION_TITLE).innerHTML=element.title}_refreshPageItem(_ref2){var _element$pageItem,_this$pageItem;let{element:element,state:state}=_ref2;if(!element.pageItem)return;if(element.pageItem.sectionId!==this.id&&this.isPageItem||"section"!==element.pageItem.type)return this.pageItem=!1,void this.getElement(this.selectors.SECTION_ITEM).classList.remove(this.classes.PAGEITEM);var _element$pageItem2;!state.section.get(this.id).indexcollapsed||null!==(_element$pageItem=element.pageItem)&&void 0!==_element$pageItem&&_element$pageItem.isStatic?this.pageItem="section"==element.pageItem.type&&element.pageItem.id==this.id:this.pageItem=(null===(_element$pageItem2=element.pageItem)||void 0===_element$pageItem2?void 0:_element$pageItem2.sectionId)==this.id;this.getElement(this.selectors.SECTION_ITEM).classList.toggle(this.classes.PAGEITEM,null!==(_this$pageItem=this.pageItem)&&void 0!==_this$pageItem&&_this$pageItem),this.pageItem&&!this.reactive.isEditing&&this.element.scrollIntoView({block:"nearest"})}async addOverlay(){this.element.classList.add(this.classes.OVERLAYBORDERS)}removeOverlay(){this.element.classList.remove(this.classes.OVERLAYBORDERS)}}return _exports.default=Component,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_sectiontitle=_interopRequireDefault(_sectiontitle),_dndsection=_interopRequireDefault(_dndsection);class Component extends _dndsection.default{create(){this.name="courseindex_section",this.selectors={SECTION_ITEM:"[data-for='section_item']",SECTION_TITLE:"[data-for='section_title']",CM_LAST:'[data-for="cm"]:last-child'},this.classes={SECTIONHIDDEN:"dimmed",SECTIONCURRENT:"current",LOCKED:"editinprogress",RESTRICTIONS:"restrictions",PAGEITEM:"pageitem",OVERLAYBORDERS:"overlay-preview-borders"},this.id=this.element.dataset.id,this.isPageItem=!1}static init(target,selectors){return new this({element:document.getElementById(target),selectors:selectors})}stateReady(state){this.configState(state);const sectionItem=this.getElement(this.selectors.SECTION_ITEM);if(this.reactive.isEditing&&this.reactive.supportComponents){const titleitem=new _sectiontitle.default({...this,element:sectionItem,fullregion:this.element});this.configDragDrop(titleitem)}const section=state.section.get(this.id);window.location.href==section.sectionurl.replace(/&/g,"&")&&(this.reactive.dispatch("setPageItem","section",this.id),sectionItem.scrollIntoView())}getWatchers(){return[{watch:"section[".concat(this.id,"]:deleted"),handler:this.remove},{watch:"section[".concat(this.id,"]:updated"),handler:this._refreshSection},{watch:"course.pageItem:updated",handler:this._refreshPageItem}]}getLastCm(){return this.getElement(this.selectors.CM_LAST)}_refreshSection(_ref){var _element$hasrestricti,_element$dragging,_element$locked;let{element:element}=_ref;const sectionItem=this.getElement(this.selectors.SECTION_ITEM);sectionItem.classList.toggle(this.classes.SECTIONHIDDEN,!element.visible),sectionItem.classList.toggle(this.classes.RESTRICTIONS,null!==(_element$hasrestricti=element.hasrestrictions)&&void 0!==_element$hasrestricti&&_element$hasrestricti),this.element.classList.toggle(this.classes.SECTIONCURRENT,element.current),this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.locked=element.locked,this.getElement(this.selectors.SECTION_TITLE).innerHTML=element.title}_refreshPageItem(_ref2){var _element$pageItem,_this$pageItem;let{element:element,state:state}=_ref2;if(!element.pageItem)return;const section=state.section.get(this.id),isRelevantPageItem=element.pageItem.sectionId===this.id||!this.isPageItem,isSectionOrCollapsed="section"===element.pageItem.type||section.indexcollapsed;if(!isRelevantPageItem||!isSectionOrCollapsed)return this.pageItem=!1,void this.getElement(this.selectors.SECTION_ITEM).classList.remove(this.classes.PAGEITEM);var _element$pageItem2;!section.indexcollapsed||null!==(_element$pageItem=element.pageItem)&&void 0!==_element$pageItem&&_element$pageItem.isStatic?this.pageItem="section"==element.pageItem.type&&element.pageItem.id==this.id:this.pageItem=(null===(_element$pageItem2=element.pageItem)||void 0===_element$pageItem2?void 0:_element$pageItem2.sectionId)==this.id;this.getElement(this.selectors.SECTION_ITEM).classList.toggle(this.classes.PAGEITEM,null!==(_this$pageItem=this.pageItem)&&void 0!==_this$pageItem&&_this$pageItem),this.pageItem&&!this.reactive.isEditing&&this.element.scrollIntoView({block:"nearest"})}async addOverlay(){this.element.classList.add(this.classes.OVERLAYBORDERS)}removeOverlay(){this.element.classList.remove(this.classes.OVERLAYBORDERS)}}return _exports.default=Component,_exports.default})); //# sourceMappingURL=section.min.js.map \ No newline at end of file diff --git a/course/format/amd/build/local/courseindex/section.min.js.map b/course/format/amd/build/local/courseindex/section.min.js.map index b2303b156557c..f273c8412a121 100644 --- a/course/format/amd/build/local/courseindex/section.min.js.map +++ b/course/format/amd/build/local/courseindex/section.min.js.map @@ -1 +1 @@ -{"version":3,"file":"section.min.js","sources":["../../../src/local/courseindex/section.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Course index section component.\n *\n * This component is used to control specific course section interactions like drag and drop.\n *\n * @module core_courseformat/local/courseindex/section\n * @class core_courseformat/local/courseindex/section\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport SectionTitle from 'core_courseformat/local/courseindex/sectiontitle';\nimport DndSection from 'core_courseformat/local/courseeditor/dndsection';\n\nexport default class Component extends DndSection {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex_section';\n // Default query selectors.\n this.selectors = {\n SECTION_ITEM: `[data-for='section_item']`,\n SECTION_TITLE: `[data-for='section_title']`,\n CM_LAST: `[data-for=\"cm\"]:last-child`,\n };\n // Default classes to toggle on refresh.\n this.classes = {\n SECTIONHIDDEN: 'dimmed',\n SECTIONCURRENT: 'current',\n LOCKED: 'editinprogress',\n RESTRICTIONS: 'restrictions',\n PAGEITEM: 'pageitem',\n OVERLAYBORDERS: 'overlay-preview-borders',\n };\n\n // We need our id to watch specific events.\n this.id = this.element.dataset.id;\n this.isPageItem = false;\n }\n\n /**\n * Static method to create a component instance form the mustahce template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.getElementById(target),\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the initial state\n */\n stateReady(state) {\n this.configState(state);\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n // Drag and drop is only available for components compatible course formats.\n if (this.reactive.isEditing && this.reactive.supportComponents) {\n // Init the inner dragable element passing the full section as affected region.\n const titleitem = new SectionTitle({\n ...this,\n element: sectionItem,\n fullregion: this.element,\n });\n this.configDragDrop(titleitem);\n }\n // Check if the current url is the section url.\n const section = state.section.get(this.id);\n if (window.location.href == section.sectionurl.replace(/&/g, \"&\")) {\n this.reactive.dispatch('setPageItem', 'section', this.id);\n sectionItem.scrollIntoView();\n }\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `section[${this.id}]:deleted`, handler: this.remove},\n {watch: `section[${this.id}]:updated`, handler: this._refreshSection},\n {watch: `course.pageItem:updated`, handler: this._refreshPageItem},\n ];\n }\n\n /**\n * Get the last CM element of that section.\n *\n * @returns {element|null}\n */\n getLastCm() {\n return this.getElement(this.selectors.CM_LAST);\n }\n\n /**\n * Update a course index section using the state information.\n *\n * @param {Object} param details the update details.\n * @param {Object} param.element the section element\n */\n _refreshSection({element}) {\n // Update classes.\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n sectionItem.classList.toggle(this.classes.SECTIONHIDDEN, !element.visible);\n sectionItem.classList.toggle(this.classes.RESTRICTIONS, element.hasrestrictions ?? false);\n this.element.classList.toggle(this.classes.SECTIONCURRENT, element.current);\n this.element.classList.toggle(this.classes.DRAGGING, element.dragging ?? false);\n this.element.classList.toggle(this.classes.LOCKED, element.locked ?? false);\n this.locked = element.locked;\n // Update title.\n this.getElement(this.selectors.SECTION_TITLE).innerHTML = element.title;\n }\n\n /**\n * Handle a page item update.\n *\n * @param {Object} details the update details\n * @param {Object} details.state the state data.\n * @param {Object} details.element the course state data.\n */\n _refreshPageItem({element, state}) {\n if (!element.pageItem) {\n return;\n }\n if (element.pageItem.sectionId !== this.id && this.isPageItem || element.pageItem.type !== 'section') {\n this.pageItem = false;\n this.getElement(this.selectors.SECTION_ITEM).classList.remove(this.classes.PAGEITEM);\n return;\n }\n const section = state.section.get(this.id);\n if (section.indexcollapsed && !element.pageItem?.isStatic) {\n this.pageItem = (element.pageItem?.sectionId == this.id);\n } else {\n this.pageItem = (element.pageItem.type == 'section' && element.pageItem.id == this.id);\n }\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n sectionItem.classList.toggle(this.classes.PAGEITEM, this.pageItem ?? false);\n if (this.pageItem && !this.reactive.isEditing) {\n this.element.scrollIntoView({block: \"nearest\"});\n }\n }\n\n /**\n * Overridden version of the component addOverlay async method.\n *\n * The course index is not compatible with overlay elements.\n */\n async addOverlay() {\n this.element.classList.add(this.classes.OVERLAYBORDERS);\n }\n\n /**\n * Overridden version of the component removeOverlay.\n *\n * The course index is not compatible with overlay elements.\n */\n removeOverlay() {\n this.element.classList.remove(this.classes.OVERLAYBORDERS);\n }\n}\n"],"names":["Component","DndSection","create","name","selectors","SECTION_ITEM","SECTION_TITLE","CM_LAST","classes","SECTIONHIDDEN","SECTIONCURRENT","LOCKED","RESTRICTIONS","PAGEITEM","OVERLAYBORDERS","id","this","element","dataset","isPageItem","target","document","getElementById","stateReady","state","configState","sectionItem","getElement","reactive","isEditing","supportComponents","titleitem","SectionTitle","fullregion","configDragDrop","section","get","window","location","href","sectionurl","replace","dispatch","scrollIntoView","getWatchers","watch","handler","remove","_refreshSection","_refreshPageItem","getLastCm","classList","toggle","visible","hasrestrictions","current","DRAGGING","dragging","locked","innerHTML","title","pageItem","sectionId","type","indexcollapsed","_element$pageItem","isStatic","block","add","removeOverlay"],"mappings":";;;;;;;;;;+LA6BqBA,kBAAkBC,oBAKnCC,cAESC,KAAO,2BAEPC,UAAY,CACbC,yCACAC,2CACAC,2CAGCC,QAAU,CACXC,cAAe,SACfC,eAAgB,UAChBC,OAAQ,iBACRC,aAAc,eACdC,SAAU,WACVC,eAAgB,gCAIfC,GAAKC,KAAKC,QAAQC,QAAQH,QAC1BI,YAAa,cAUVC,OAAQhB,kBACT,IAAIY,KAAK,CACZC,QAASI,SAASC,eAAeF,QACjChB,UAAAA,YASRmB,WAAWC,YACFC,YAAYD,aACXE,YAAcV,KAAKW,WAAWX,KAAKZ,UAAUC,iBAE/CW,KAAKY,SAASC,WAAab,KAAKY,SAASE,kBAAmB,OAEtDC,UAAY,IAAIC,sBAAa,IAC5BhB,KACHC,QAASS,YACTO,WAAYjB,KAAKC,eAEhBiB,eAAeH,iBAGlBI,QAAUX,MAAMW,QAAQC,IAAIpB,KAAKD,IACnCsB,OAAOC,SAASC,MAAQJ,QAAQK,WAAWC,QAAQ,SAAU,YACxDb,SAASc,SAAS,cAAe,UAAW1B,KAAKD,IACtDW,YAAYiB,kBASpBC,oBACW,CACH,CAACC,wBAAkB7B,KAAKD,gBAAe+B,QAAS9B,KAAK+B,QACrD,CAACF,wBAAkB7B,KAAKD,gBAAe+B,QAAS9B,KAAKgC,iBACrD,CAACH,gCAAkCC,QAAS9B,KAAKiC,mBASzDC,mBACWlC,KAAKW,WAAWX,KAAKZ,UAAUG,SAS1CyC,sFAAgB/B,QAACA,oBAEPS,YAAcV,KAAKW,WAAWX,KAAKZ,UAAUC,cACnDqB,YAAYyB,UAAUC,OAAOpC,KAAKR,QAAQC,eAAgBQ,QAAQoC,SAClE3B,YAAYyB,UAAUC,OAAOpC,KAAKR,QAAQI,2CAAcK,QAAQqC,8EAC3DrC,QAAQkC,UAAUC,OAAOpC,KAAKR,QAAQE,eAAgBO,QAAQsC,cAC9DtC,QAAQkC,UAAUC,OAAOpC,KAAKR,QAAQgD,mCAAUvC,QAAQwC,+DACxDxC,QAAQkC,UAAUC,OAAOpC,KAAKR,QAAQG,+BAAQM,QAAQyC,yDACtDA,OAASzC,QAAQyC,YAEjB/B,WAAWX,KAAKZ,UAAUE,eAAeqD,UAAY1C,QAAQ2C,MAUtEX,iEAAiBhC,QAACA,QAADO,MAAUA,iBAClBP,QAAQ4C,mBAGT5C,QAAQ4C,SAASC,YAAc9C,KAAKD,IAAMC,KAAKG,YAAwC,YAA1BF,QAAQ4C,SAASE,iBACzEF,UAAW,YACXlC,WAAWX,KAAKZ,UAAUC,cAAc8C,UAAUJ,OAAO/B,KAAKR,QAAQK,kCAG/DW,MAAMW,QAAQC,IAAIpB,KAAKD,IAC3BiD,0CAAmB/C,QAAQ4C,uCAARI,kBAAkBC,cAGxCL,SAAqC,WAAzB5C,QAAQ4C,SAASE,MAAqB9C,QAAQ4C,SAAS9C,IAAMC,KAAKD,QAF9E8C,qCAAY5C,QAAQ4C,iEAAUC,YAAa9C,KAAKD,GAIrCC,KAAKW,WAAWX,KAAKZ,UAAUC,cACvC8C,UAAUC,OAAOpC,KAAKR,QAAQK,gCAAUG,KAAK6C,oDACrD7C,KAAK6C,WAAa7C,KAAKY,SAASC,gBAC3BZ,QAAQ0B,eAAe,CAACwB,MAAO,oCAUnClD,QAAQkC,UAAUiB,IAAIpD,KAAKR,QAAQM,gBAQ5CuD,qBACSpD,QAAQkC,UAAUJ,OAAO/B,KAAKR,QAAQM"} \ No newline at end of file +{"version":3,"file":"section.min.js","sources":["../../../src/local/courseindex/section.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Course index section component.\n *\n * This component is used to control specific course section interactions like drag and drop.\n *\n * @module core_courseformat/local/courseindex/section\n * @class core_courseformat/local/courseindex/section\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport SectionTitle from 'core_courseformat/local/courseindex/sectiontitle';\nimport DndSection from 'core_courseformat/local/courseeditor/dndsection';\n\nexport default class Component extends DndSection {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex_section';\n // Default query selectors.\n this.selectors = {\n SECTION_ITEM: `[data-for='section_item']`,\n SECTION_TITLE: `[data-for='section_title']`,\n CM_LAST: `[data-for=\"cm\"]:last-child`,\n };\n // Default classes to toggle on refresh.\n this.classes = {\n SECTIONHIDDEN: 'dimmed',\n SECTIONCURRENT: 'current',\n LOCKED: 'editinprogress',\n RESTRICTIONS: 'restrictions',\n PAGEITEM: 'pageitem',\n OVERLAYBORDERS: 'overlay-preview-borders',\n };\n\n // We need our id to watch specific events.\n this.id = this.element.dataset.id;\n this.isPageItem = false;\n }\n\n /**\n * Static method to create a component instance form the mustahce template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.getElementById(target),\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the initial state\n */\n stateReady(state) {\n this.configState(state);\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n // Drag and drop is only available for components compatible course formats.\n if (this.reactive.isEditing && this.reactive.supportComponents) {\n // Init the inner dragable element passing the full section as affected region.\n const titleitem = new SectionTitle({\n ...this,\n element: sectionItem,\n fullregion: this.element,\n });\n this.configDragDrop(titleitem);\n }\n // Check if the current url is the section url.\n const section = state.section.get(this.id);\n if (window.location.href == section.sectionurl.replace(/&/g, \"&\")) {\n this.reactive.dispatch('setPageItem', 'section', this.id);\n sectionItem.scrollIntoView();\n }\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `section[${this.id}]:deleted`, handler: this.remove},\n {watch: `section[${this.id}]:updated`, handler: this._refreshSection},\n {watch: `course.pageItem:updated`, handler: this._refreshPageItem},\n ];\n }\n\n /**\n * Get the last CM element of that section.\n *\n * @returns {element|null}\n */\n getLastCm() {\n return this.getElement(this.selectors.CM_LAST);\n }\n\n /**\n * Update a course index section using the state information.\n *\n * @param {Object} param details the update details.\n * @param {Object} param.element the section element\n */\n _refreshSection({element}) {\n // Update classes.\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n sectionItem.classList.toggle(this.classes.SECTIONHIDDEN, !element.visible);\n sectionItem.classList.toggle(this.classes.RESTRICTIONS, element.hasrestrictions ?? false);\n this.element.classList.toggle(this.classes.SECTIONCURRENT, element.current);\n this.element.classList.toggle(this.classes.DRAGGING, element.dragging ?? false);\n this.element.classList.toggle(this.classes.LOCKED, element.locked ?? false);\n this.locked = element.locked;\n // Update title.\n this.getElement(this.selectors.SECTION_TITLE).innerHTML = element.title;\n }\n\n /**\n * Handle a page item update.\n *\n * @param {Object} details the update details\n * @param {Object} details.state the state data.\n * @param {Object} details.element the course state data.\n */\n _refreshPageItem({element, state}) {\n if (!element.pageItem) {\n return;\n }\n const section = state.section.get(this.id);\n const isRelevantPageItem = element.pageItem.sectionId === this.id || !this.isPageItem;\n const isSectionOrCollapsed = element.pageItem.type === 'section' || section.indexcollapsed;\n\n if (!(isRelevantPageItem && isSectionOrCollapsed)) {\n this.pageItem = false;\n this.getElement(this.selectors.SECTION_ITEM).classList.remove(this.classes.PAGEITEM);\n return;\n }\n if (section.indexcollapsed && !element.pageItem?.isStatic) {\n this.pageItem = (element.pageItem?.sectionId == this.id);\n } else {\n this.pageItem = (element.pageItem.type == 'section' && element.pageItem.id == this.id);\n }\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n sectionItem.classList.toggle(this.classes.PAGEITEM, this.pageItem ?? false);\n if (this.pageItem && !this.reactive.isEditing) {\n this.element.scrollIntoView({block: \"nearest\"});\n }\n }\n\n /**\n * Overridden version of the component addOverlay async method.\n *\n * The course index is not compatible with overlay elements.\n */\n async addOverlay() {\n this.element.classList.add(this.classes.OVERLAYBORDERS);\n }\n\n /**\n * Overridden version of the component removeOverlay.\n *\n * The course index is not compatible with overlay elements.\n */\n removeOverlay() {\n this.element.classList.remove(this.classes.OVERLAYBORDERS);\n }\n}\n"],"names":["Component","DndSection","create","name","selectors","SECTION_ITEM","SECTION_TITLE","CM_LAST","classes","SECTIONHIDDEN","SECTIONCURRENT","LOCKED","RESTRICTIONS","PAGEITEM","OVERLAYBORDERS","id","this","element","dataset","isPageItem","target","document","getElementById","stateReady","state","configState","sectionItem","getElement","reactive","isEditing","supportComponents","titleitem","SectionTitle","fullregion","configDragDrop","section","get","window","location","href","sectionurl","replace","dispatch","scrollIntoView","getWatchers","watch","handler","remove","_refreshSection","_refreshPageItem","getLastCm","classList","toggle","visible","hasrestrictions","current","DRAGGING","dragging","locked","innerHTML","title","pageItem","isRelevantPageItem","sectionId","isSectionOrCollapsed","type","indexcollapsed","_element$pageItem","isStatic","block","add","removeOverlay"],"mappings":";;;;;;;;;;+LA6BqBA,kBAAkBC,oBAKnCC,cAESC,KAAO,2BAEPC,UAAY,CACbC,yCACAC,2CACAC,2CAGCC,QAAU,CACXC,cAAe,SACfC,eAAgB,UAChBC,OAAQ,iBACRC,aAAc,eACdC,SAAU,WACVC,eAAgB,gCAIfC,GAAKC,KAAKC,QAAQC,QAAQH,QAC1BI,YAAa,cAUVC,OAAQhB,kBACT,IAAIY,KAAK,CACZC,QAASI,SAASC,eAAeF,QACjChB,UAAAA,YASRmB,WAAWC,YACFC,YAAYD,aACXE,YAAcV,KAAKW,WAAWX,KAAKZ,UAAUC,iBAE/CW,KAAKY,SAASC,WAAab,KAAKY,SAASE,kBAAmB,OAEtDC,UAAY,IAAIC,sBAAa,IAC5BhB,KACHC,QAASS,YACTO,WAAYjB,KAAKC,eAEhBiB,eAAeH,iBAGlBI,QAAUX,MAAMW,QAAQC,IAAIpB,KAAKD,IACnCsB,OAAOC,SAASC,MAAQJ,QAAQK,WAAWC,QAAQ,SAAU,YACxDb,SAASc,SAAS,cAAe,UAAW1B,KAAKD,IACtDW,YAAYiB,kBASpBC,oBACW,CACH,CAACC,wBAAkB7B,KAAKD,gBAAe+B,QAAS9B,KAAK+B,QACrD,CAACF,wBAAkB7B,KAAKD,gBAAe+B,QAAS9B,KAAKgC,iBACrD,CAACH,gCAAkCC,QAAS9B,KAAKiC,mBASzDC,mBACWlC,KAAKW,WAAWX,KAAKZ,UAAUG,SAS1CyC,sFAAgB/B,QAACA,oBAEPS,YAAcV,KAAKW,WAAWX,KAAKZ,UAAUC,cACnDqB,YAAYyB,UAAUC,OAAOpC,KAAKR,QAAQC,eAAgBQ,QAAQoC,SAClE3B,YAAYyB,UAAUC,OAAOpC,KAAKR,QAAQI,2CAAcK,QAAQqC,8EAC3DrC,QAAQkC,UAAUC,OAAOpC,KAAKR,QAAQE,eAAgBO,QAAQsC,cAC9DtC,QAAQkC,UAAUC,OAAOpC,KAAKR,QAAQgD,mCAAUvC,QAAQwC,+DACxDxC,QAAQkC,UAAUC,OAAOpC,KAAKR,QAAQG,+BAAQM,QAAQyC,yDACtDA,OAASzC,QAAQyC,YAEjB/B,WAAWX,KAAKZ,UAAUE,eAAeqD,UAAY1C,QAAQ2C,MAUtEX,iEAAiBhC,QAACA,QAADO,MAAUA,iBAClBP,QAAQ4C,sBAGP1B,QAAUX,MAAMW,QAAQC,IAAIpB,KAAKD,IACjC+C,mBAAqB7C,QAAQ4C,SAASE,YAAc/C,KAAKD,KAAOC,KAAKG,WACrE6C,qBAAiD,YAA1B/C,QAAQ4C,SAASI,MAAsB9B,QAAQ+B,mBAEtEJ,qBAAsBE,iCACnBH,UAAW,YACXlC,WAAWX,KAAKZ,UAAUC,cAAc8C,UAAUJ,OAAO/B,KAAKR,QAAQK,kCAG3EsB,QAAQ+B,0CAAmBjD,QAAQ4C,uCAARM,kBAAkBC,cAGxCP,SAAqC,WAAzB5C,QAAQ4C,SAASI,MAAqBhD,QAAQ4C,SAAS9C,IAAMC,KAAKD,QAF9E8C,qCAAY5C,QAAQ4C,iEAAUE,YAAa/C,KAAKD,GAIrCC,KAAKW,WAAWX,KAAKZ,UAAUC,cACvC8C,UAAUC,OAAOpC,KAAKR,QAAQK,gCAAUG,KAAK6C,oDACrD7C,KAAK6C,WAAa7C,KAAKY,SAASC,gBAC3BZ,QAAQ0B,eAAe,CAAC0B,MAAO,oCAUnCpD,QAAQkC,UAAUmB,IAAItD,KAAKR,QAAQM,gBAQ5CyD,qBACStD,QAAQkC,UAAUJ,OAAO/B,KAAKR,QAAQM"} \ No newline at end of file diff --git a/course/format/amd/src/local/courseeditor/fileuploader.js b/course/format/amd/src/local/courseeditor/fileuploader.js index 3b67d35005d5f..cc36ec827ee22 100644 --- a/course/format/amd/src/local/courseeditor/fileuploader.js +++ b/course/format/amd/src/local/courseeditor/fileuploader.js @@ -345,10 +345,11 @@ class HandlerManager { let hasDefault = false; fileHandlers.forEach((handler, index) => { const isDefault = (defaultModule == handler.module); + const optionNumber = index + 1; data.handlers.push({ ...handler, selected: isDefault, - labelid: `fileuploader_${data.uploadid}`, + labelid: `fileuploader_${data.uploadid}_${optionNumber}`, value: index, }); hasDefault = hasDefault || isDefault; diff --git a/course/format/amd/src/local/courseindex/cm.js b/course/format/amd/src/local/courseindex/cm.js index 22ffe7aeb14ed..0a7e6c1a5b239 100644 --- a/course/format/amd/src/local/courseindex/cm.js +++ b/course/format/amd/src/local/courseindex/cm.js @@ -102,11 +102,16 @@ export default class Component extends DndCmItem { } // Add anchor logic if the element is not user visible or the element hasn't URL. if (!cm.uservisible || !cm.url) { + const element = this.getElement(this.selectors.CM_NAME); this.addEventListener( - this.getElement(this.selectors.CM_NAME), + element, 'click', this._activityAnchor, ); + // If the element is not user visible we also need to update the anchor link including the section page. + if (!document.getElementById(cm.anchor)) { + element.setAttribute('href', this._getActivitySectionURL(cm)); + } } } @@ -206,13 +211,22 @@ export default class Component extends DndCmItem { return; } // If the element is not present in the page we need to go to the specific section. - const course = this.reactive.get('course'); + event.preventDefault(); + window.location = this._getActivitySectionURL(cm); + } + + /** + * Get the anchor link in section page for the cm. + * + * @param {Object} cm the course module data. + * @return {String} the anchor link. + */ + _getActivitySectionURL(cm) { const section = this.reactive.get('section', cm.sectionid); if (!section) { - return; + return '#'; } - const url = `${course.baseurl}§ion=${section.number}#${cm.anchor}`; - event.preventDefault(); - window.location = url; + + return `${section.sectionurl}#${cm.anchor}`; } } diff --git a/course/format/amd/src/local/courseindex/courseindex.js b/course/format/amd/src/local/courseindex/courseindex.js index e070de2eed93c..de5d53742f2bb 100644 --- a/course/format/amd/src/local/courseindex/courseindex.js +++ b/course/format/amd/src/local/courseindex/courseindex.js @@ -338,7 +338,7 @@ export default class Component extends BaseComponent { const item = allitems[itemid]; // Get the current element at that position. const currentitem = container.children[index]; - if (currentitem === undefined) { + if (currentitem === undefined && item != undefined) { container.append(item); return; } diff --git a/course/format/amd/src/local/courseindex/section.js b/course/format/amd/src/local/courseindex/section.js index 8e743666e8bf3..92e06dff6d6df 100644 --- a/course/format/amd/src/local/courseindex/section.js +++ b/course/format/amd/src/local/courseindex/section.js @@ -148,12 +148,15 @@ export default class Component extends DndSection { if (!element.pageItem) { return; } - if (element.pageItem.sectionId !== this.id && this.isPageItem || element.pageItem.type !== 'section') { + const section = state.section.get(this.id); + const isRelevantPageItem = element.pageItem.sectionId === this.id || !this.isPageItem; + const isSectionOrCollapsed = element.pageItem.type === 'section' || section.indexcollapsed; + + if (!(isRelevantPageItem && isSectionOrCollapsed)) { this.pageItem = false; this.getElement(this.selectors.SECTION_ITEM).classList.remove(this.classes.PAGEITEM); return; } - const section = state.section.get(this.id); if (section.indexcollapsed && !element.pageItem?.isStatic) { this.pageItem = (element.pageItem?.sectionId == this.id); } else { diff --git a/course/format/classes/base.php b/course/format/classes/base.php index b0f8bc35ce9a8..e6890695c91a1 100644 --- a/course/format/classes/base.php +++ b/course/format/classes/base.php @@ -283,6 +283,20 @@ public static function session_cache(stdClass $course): string { return self::session_cache_reset($course); } + /** + * Prune a course state cache for all open sessions. + * + * Most course edits does not require to invalidate the cache for all users + * because the cache relies on the course cacherev value. However, there are + * actions like editing the groups that do not change the course cacherev. + * + * @param \stdClass $course + * @return void + */ + public static function invalidate_all_session_caches_for_course(stdClass $course): void { + \cache_helper::invalidate_by_event('changesincoursestate', [$course->id]); + } + /** * Returns the format name used by this course * diff --git a/course/format/classes/hook_listener.php b/course/format/classes/hook_listener.php new file mode 100644 index 0000000000000..13fbf57f3768b --- /dev/null +++ b/course/format/classes/hook_listener.php @@ -0,0 +1,55 @@ +. + +namespace core_courseformat; + +use core_group\hook\after_group_membership_added; +use core_group\hook\after_group_membership_removed; + +/** + * Hook listener for course format + * + * @package core_courseformat + * @copyright 2024 Laurent David + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class hook_listener { + /** + * Add members to group room when a new member is added to the group. + * + * @param after_group_membership_added $hook The group membership added hook. + */ + public static function add_members_to_group( + after_group_membership_added $hook, + ): void { + $group = $hook->groupinstance; + $course = get_course($group->courseid); + base::invalidate_all_session_caches_for_course($course); + } + + /** + * Remove members from the room when a member is removed from group room. + * + * @param after_group_membership_removed $hook The group membership removed hook. + */ + public static function remove_members_from_group( + after_group_membership_removed $hook, + ): void { + $group = $hook->groupinstance; + $course = get_course($group->courseid); + base::invalidate_all_session_caches_for_course($course); + } +} diff --git a/course/format/classes/local/sectionactions.php b/course/format/classes/local/sectionactions.php index f0e5657a9e137..5793df46cac5b 100644 --- a/course/format/classes/local/sectionactions.php +++ b/course/format/classes/local/sectionactions.php @@ -162,11 +162,11 @@ public function create_delegated( * * @param int $position The position to add to, 0 means to the end. * @param bool $skipcheck the check has already been made and we know that the section with this position does not exist - * @return stdClass created section object) + * @return stdClass created section object */ public function create(int $position = 0, bool $skipcheck = false): stdClass { $record = (object) [ - 'section' => $position, + 'section' => ($position == 0 && !$skipcheck) ? null : $position, ]; return $this->create_from_object($record, $skipcheck); } diff --git a/course/format/templates/local/content/cm/completion_dialog.mustache b/course/format/templates/local/content/cm/completion_dialog.mustache index 7dabcddc436d8..2cfa52ccc494c 100644 --- a/course/format/templates/local/content/cm/completion_dialog.mustache +++ b/course/format/templates/local/content/cm/completion_dialog.mustache @@ -110,7 +110,7 @@ {{! Show edit link to editing teachers. }} {{#editing}} {{#editurl}} -
    +
    {{#hasconditions}}{{#pix}} i/edit, core {{/pix}}{{#str}}editconditions, completion{{/str}}{{/hasconditions}} {{^hasconditions}}{{#pix}} t/add, core {{/pix}}{{#str}}addconditions, completion{{/str}}{{/hasconditions}} diff --git a/course/format/templates/local/content/movesection.mustache b/course/format/templates/local/content/movesection.mustache index ff09d36b3b1cb..f7141a6e546b0 100644 --- a/course/format/templates/local/content/movesection.mustache +++ b/course/format/templates/local/content/movesection.mustache @@ -44,7 +44,7 @@ }} {{#information}}

    - {{information}}: + {{{information}}}:

    {{/information}}