diff --git a/.upgradenotes/MDL-43938-2024070407235835.yml b/.upgradenotes/MDL-43938-2024070407235835.yml new file mode 100644 index 0000000000000..b9ccf22c748d1 --- /dev/null +++ b/.upgradenotes/MDL-43938-2024070407235835.yml @@ -0,0 +1,7 @@ +issueNumber: MDL-43938 +notes: + core_badges: + - message: >- + The badges/newbadge.php page has been deprecated and merged with + badges/edit.php. Please, use badges/edit.php instead. + type: deprecated diff --git a/.upgradenotes/MDL-66903-2024070501430894.yml b/.upgradenotes/MDL-66903-2024070501430894.yml new file mode 100644 index 0000000000000..ce397c67d00ed --- /dev/null +++ b/.upgradenotes/MDL-66903-2024070501430894.yml @@ -0,0 +1,11 @@ +issueNumber: MDL-66903 +notes: + core: + - message: | + The following classes have been renamed and now support autoloading. + Existing classes are currently unaffected. + + | Old class name | New class name | + | --- | --- | + | `\core_component` | `\core\component` | + type: improved diff --git a/.upgradenotes/MDL-66903-2024070502035383.yml b/.upgradenotes/MDL-66903-2024070502035383.yml new file mode 100644 index 0000000000000..0c749fbef709a --- /dev/null +++ b/.upgradenotes/MDL-66903-2024070502035383.yml @@ -0,0 +1,20 @@ +issueNumber: MDL-66903 +notes: + core: + - message: > + Added the ability for unit tests to autoload classes in the + `\[component]\tests\` + + namespace from the `[path/to/component]/tests/classes` directory. + type: improved + - message: > + Added a helper to load fixtures from a components `tests/fixtures/` + folder: + + + ```php + + advanced_testcase::load_fixture(string $component, string $fixture): void; + + ``` + type: improved diff --git a/.upgradenotes/MDL-67554-2024071602392599.yml b/.upgradenotes/MDL-67554-2024071602392599.yml new file mode 100644 index 0000000000000..8caa9da7f466c --- /dev/null +++ b/.upgradenotes/MDL-67554-2024071602392599.yml @@ -0,0 +1,8 @@ +issueNumber: MDL-67554 +notes: + tool_oauth2: + - message: > + The `get_additional_login_parameters()` method now supports adding the + language code to the authentication request so that the OAuth2 login + page matches the language in Moodle. + type: improved diff --git a/.upgradenotes/MDL-70854-2024071306574741.yml b/.upgradenotes/MDL-70854-2024071306574741.yml new file mode 100644 index 0000000000000..42b111e37150e --- /dev/null +++ b/.upgradenotes/MDL-70854-2024071306574741.yml @@ -0,0 +1,5 @@ +issueNumber: MDL-70854 +notes: + core: + - message: Added stored progress bars + type: improved diff --git a/.upgradenotes/MDL-70983-2024062810515930.yml b/.upgradenotes/MDL-70983-2024062810515930.yml new file mode 100644 index 0000000000000..df52839ac011b --- /dev/null +++ b/.upgradenotes/MDL-70983-2024062810515930.yml @@ -0,0 +1,7 @@ +issueNumber: MDL-70983 +notes: + core_badges: + - message: Final removal of BADGE_BACKPACKAPIURL and BADGE_BACKPACKWEBURL. + type: removed + - message: OPEN_BADGES_V1 is deprecated and should not be used anymore. + type: deprecated diff --git a/.upgradenotes/MDL-72424-2024062113465122.yml b/.upgradenotes/MDL-72424-2024062113465122.yml new file mode 100644 index 0000000000000..6061a35c82ed9 --- /dev/null +++ b/.upgradenotes/MDL-72424-2024062113465122.yml @@ -0,0 +1,7 @@ +issueNumber: MDL-72424 +notes: + mod_feedback: + - message: >- + The `feedback_check_is_switchrole` method has been deprecated as it + didn't work + type: deprecated diff --git a/.upgradenotes/MDL-73284-2024061908343534.yml b/.upgradenotes/MDL-73284-2024061908343534.yml new file mode 100644 index 0000000000000..91c6aac556321 --- /dev/null +++ b/.upgradenotes/MDL-73284-2024061908343534.yml @@ -0,0 +1,5 @@ +issueNumber: MDL-73284 +notes: + core_message: + - message: Final deprecation MESSAGE_DEFAULT_LOGGEDOFF / MESSAGE_DEFAULT_LOGGEDIN. + type: removed diff --git a/.upgradenotes/MDL-75025-2024062015422824.yml b/.upgradenotes/MDL-75025-2024062015422824.yml new file mode 100644 index 0000000000000..5dffed60c5fe2 --- /dev/null +++ b/.upgradenotes/MDL-75025-2024062015422824.yml @@ -0,0 +1,7 @@ +issueNumber: MDL-75025 +notes: + core_backup: + - message: >- + Final deprecation of base_controller::get_copy(). Please use + restore_controller::get_copy() instead. + type: removed diff --git a/.upgradenotes/MDL-75025-2024062015430029.yml b/.upgradenotes/MDL-75025-2024062015430029.yml new file mode 100644 index 0000000000000..9736c0f2f2dab --- /dev/null +++ b/.upgradenotes/MDL-75025-2024062015430029.yml @@ -0,0 +1,6 @@ +issueNumber: MDL-75025 +notes: + core_backup: + - message: 'Final deprecation of base_controller::set_copy(). Please use a restore + controller for storing copy information instead.' + type: removed diff --git a/.upgradenotes/MDL-77167-2024062111140275.yml b/.upgradenotes/MDL-77167-2024062111140275.yml new file mode 100644 index 0000000000000..3c59dfa89e6fb --- /dev/null +++ b/.upgradenotes/MDL-77167-2024062111140275.yml @@ -0,0 +1,5 @@ +issueNumber: MDL-77167 +notes: + core: + - message: Remove deprecation layer for YUI Events. The deprecation layer was introduced with MDL-70990 and MDL-72291. + type: removed diff --git a/.upgradenotes/MDL-78293-2024071102223792.yml b/.upgradenotes/MDL-78293-2024071102223792.yml new file mode 100644 index 0000000000000..44a5deed1642c --- /dev/null +++ b/.upgradenotes/MDL-78293-2024071102223792.yml @@ -0,0 +1,7 @@ +issueNumber: MDL-78293 +notes: + core_user: + - message: > + The visibility of the methods: check_access_for_dynamic_submission() and get_options() + in core_user\form\private_files has been changed from protected to public. + type: changed diff --git a/.upgradenotes/MDL-80744-2024062407163290.yml b/.upgradenotes/MDL-80744-2024062407163290.yml new file mode 100644 index 0000000000000..c92a3760e8009 --- /dev/null +++ b/.upgradenotes/MDL-80744-2024062407163290.yml @@ -0,0 +1,20 @@ +issueNumber: MDL-80744 +notes: + core_grades: + - message: >- + The behat step definition + behat_grade::i_confirm_in_search_within_the_gradebook_widget_exists has + been deprecated. Please use + behat_general::i_confirm_in_search_combobox_exists instead. + type: deprecated + - message: >- + The behat step definition + behat_grade::i_confirm_in_search_within_the_gradebook_widget_does_not_exist + has been deprecated. Please use + behat_general::i_confirm_in_search_combobox_does_not_exist instead. + type: deprecated + - message: >- + The behat step definition behat_grade::i_click_on_in_search_widget has + been deprecated. Please use behat_general::i_click_on_in_search_combobox + instead. + type: deprecated diff --git a/.upgradenotes/MDL-81533-2024070307500488.yml b/.upgradenotes/MDL-81533-2024070307500488.yml new file mode 100644 index 0000000000000..eab4262db9be8 --- /dev/null +++ b/.upgradenotes/MDL-81533-2024070307500488.yml @@ -0,0 +1,15 @@ +issueNumber: MDL-81533 +notes: + core: + - message: > + The function update_display_mode will update the eye icon (enabled/disabled) in the availability. + The $pluginname is represented to the plugin need to update. + The $displaymode is represented to the eye icon. Whether it enabled or disabled. + + type: improved + core_admin: + - message: > + Add availability_management_table is a table which extends from plugin_management_table. + Create the availability_management_table can reusable the toggle button for enabled column. + + type: improved diff --git a/.upgradenotes/MDL-81699-2024071803025011.yml b/.upgradenotes/MDL-81699-2024071803025011.yml new file mode 100644 index 0000000000000..285d972bbc834 --- /dev/null +++ b/.upgradenotes/MDL-81699-2024071803025011.yml @@ -0,0 +1,8 @@ +issueNumber: MDL-81699 +notes: + core_course: + - message: > + Webservices `core_course_get_courses_by_field` now accepts a new + parameter `sectionid` to be able to retrieve the course that has the + indicated section + type: improved diff --git a/.upgradenotes/MDL-82066-2024070911064670.yml b/.upgradenotes/MDL-82066-2024070911064670.yml new file mode 100644 index 0000000000000..1fc208b62acef --- /dev/null +++ b/.upgradenotes/MDL-82066-2024070911064670.yml @@ -0,0 +1,7 @@ +issueNumber: MDL-82066 +notes: + core_user: + - message: >- + New `\core_user\hook\extend_default_homepage` hook added to allow + third-party plugins to extend the default homepage options for the site + type: improved diff --git a/.upgradenotes/MDL-82066-2024070915322412.yml b/.upgradenotes/MDL-82066-2024070915322412.yml new file mode 100644 index 0000000000000..8f3e5450fb29f --- /dev/null +++ b/.upgradenotes/MDL-82066-2024070915322412.yml @@ -0,0 +1,12 @@ +issueNumber: MDL-82066 +notes: + core: + - message: >- + The `get_home_page()` method can now return new constant `HOMEPAGE_URL`, + applicable when a third-party hook has extended the default homepage + options for the site + + + A new method, `get_default_home_page_url()` has been added which will + return the correct URL when this constant is returned + type: changed diff --git a/.upgradenotes/MDL-82146-2024070508361756.yml b/.upgradenotes/MDL-82146-2024070508361756.yml new file mode 100644 index 0000000000000..4943c526397dd --- /dev/null +++ b/.upgradenotes/MDL-82146-2024070508361756.yml @@ -0,0 +1,7 @@ +issueNumber: MDL-82146 +notes: + core_courseformat: + - message: >- + New $disabled parameter has been added to select, select_optgroup and + select_option html_writers to create disabled option elements. + type: improved diff --git a/.upgradenotes/MDL-82146-2024071006310652.yml b/.upgradenotes/MDL-82146-2024071006310652.yml new file mode 100644 index 0000000000000..1f6ad98bcabb1 --- /dev/null +++ b/.upgradenotes/MDL-82146-2024071006310652.yml @@ -0,0 +1,7 @@ +issueNumber: MDL-82146 +notes: + report_log: + - message: >- + get_activities_list() function returns also an array of disabled + elements, apart from the array of activities. + type: improved diff --git a/.upgradenotes/MDL-82168-2024062815382042.yml b/.upgradenotes/MDL-82168-2024062815382042.yml new file mode 100644 index 0000000000000..43a78574eee27 --- /dev/null +++ b/.upgradenotes/MDL-82168-2024062815382042.yml @@ -0,0 +1,5 @@ +issueNumber: MDL-82168 +notes: + core_badges: + - message: New webservices enable_badges and disable_badges have been added. + type: improved diff --git a/.upgradenotes/MDL-82210-2024070210360255.yml b/.upgradenotes/MDL-82210-2024070210360255.yml new file mode 100644 index 0000000000000..bb73a470dc213 --- /dev/null +++ b/.upgradenotes/MDL-82210-2024070210360255.yml @@ -0,0 +1,11 @@ +issueNumber: MDL-82210 +notes: + theme_boost: + - message: >- + Upon upgrading Font Awesome from version 4 to 6, the solid family was + selected by default. However, FA6 includes additional families such as + regular and brands. Support for these families has now been integrated, + allowing icons defined with icon_system::FONTAWESOME to use them. + Icons can add the FontAwesome family (fa-regular, fa-brands, fa-solid) + near the icon name to display it using this styling. + type: improved diff --git a/.upgradenotes/MDL-82287-2024062501330649.yml b/.upgradenotes/MDL-82287-2024062501330649.yml new file mode 100644 index 0000000000000..e1bfa14b87265 --- /dev/null +++ b/.upgradenotes/MDL-82287-2024062501330649.yml @@ -0,0 +1,40 @@ +issueNumber: MDL-82287 +notes: + core: + - message: > + The following methods have been formally deprecated: + + + - `get_core_subsystems` + + - `get_plugin_types` + + - `get_plugin_list` + + - `get_plugin_list_with_class` + + - `get_plugin_directory` + + - `normalize_component` + + - `get_component_directory` + + - `get_context_instance` + + + Note: These methods have been deprecated for a long time, but previously + did not emit any deprecation notice. + type: deprecated + - message: > + The following methods have been finally deprecated and will now throw an + exception if called: + + + - `get_context_instance` + + - `can_use_rotated_text` + + - `get_system_context` + + - `print_arrow` + type: deprecated diff --git a/.upgradenotes/MDL-82292-2024070102564148.yml b/.upgradenotes/MDL-82292-2024070102564148.yml new file mode 100644 index 0000000000000..ff57856474841 --- /dev/null +++ b/.upgradenotes/MDL-82292-2024070102564148.yml @@ -0,0 +1,6 @@ +issueNumber: MDL-82292 +notes: + mod_assign: + - message: | + Method assign_grading_table::col_picture has been deprecated. + type: deprecated diff --git a/.upgradenotes/MDL-82466-2024071510553022.yml b/.upgradenotes/MDL-82466-2024071510553022.yml new file mode 100644 index 0000000000000..cd219f229ed93 --- /dev/null +++ b/.upgradenotes/MDL-82466-2024071510553022.yml @@ -0,0 +1,7 @@ +issueNumber: MDL-82466 +notes: + core_reportbuilder: + - message: > + New format helper `format_time` method, for use in column callbacks that + represent a duration of time (e.g. "3 days 4 hours") + type: improved diff --git a/.upgradenotes/MDL-82481-2024071516341176.yml b/.upgradenotes/MDL-82481-2024071516341176.yml new file mode 100644 index 0000000000000..12afa7f87bbf1 --- /dev/null +++ b/.upgradenotes/MDL-82481-2024071516341176.yml @@ -0,0 +1,7 @@ +issueNumber: MDL-82481 +notes: + customfield_select: + - message: > + The field controller `get_options` method now returns each option + pre-formatted + type: changed diff --git a/.upgradenotes/MDL-82510-2024071711550004.yml b/.upgradenotes/MDL-82510-2024071711550004.yml new file mode 100644 index 0000000000000..6ab0d13868916 --- /dev/null +++ b/.upgradenotes/MDL-82510-2024071711550004.yml @@ -0,0 +1,9 @@ +issueNumber: MDL-82510 +notes: + core_courseformat: + - message: >- + New \core_courseformat\output\local\content\basecontrolmenu class + has been created. Existing \core_courseformat\output\local\content\cm\controlmenu + and \core_courseformat\output\local\content\section\controlmenu classes extend + the new \core_courseformat\output\local\content\basecontrolmenu class. + type: improved diff --git a/.upgradenotes/MDL-82510-2024071722441237.yml b/.upgradenotes/MDL-82510-2024071722441237.yml new file mode 100644 index 0000000000000..398baac2a3b89 --- /dev/null +++ b/.upgradenotes/MDL-82510-2024071722441237.yml @@ -0,0 +1,9 @@ +issueNumber: MDL-82510 +notes: + core_courseformat: + - message: >- + New \core_courseformat\output\local\content\cm\delegatedcontrolmenu class + has been created extending + \core_courseformat\output\local\content\basecontrolmenu class to render + delegated section action menu combining section and module action menu. + type: improved diff --git a/.upgradenotes/MDL-82529-2024071803142477.yml b/.upgradenotes/MDL-82529-2024071803142477.yml new file mode 100644 index 0000000000000..d8aee82025453 --- /dev/null +++ b/.upgradenotes/MDL-82529-2024071803142477.yml @@ -0,0 +1,9 @@ +issueNumber: MDL-82529 +notes: + core_reportbuilder: + - message: >- + Methods add_columns_from_entity(), add_filters_from_entity() and + report_element_search() have been moved from + \core_reportbuilder\datasource class to \core_reportbuilder\base class + in order to be available also for system reports + type: improved diff --git a/UPGRADING.md b/UPGRADING.md index f98a25070cc3c..2f68b79f41cf0 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -8,6 +8,29 @@ The format of this change log follows the advice given at [Keep a CHANGELOG](htt ## 4.5dev +### core_badges + +#### Deprecated + +- The badges/newbadge.php page has been deprecated and merged with badges/edit.php. Please, use badges/edit.php instead. + + For more information see [MDL-43938](https://tracker.moodle.org/browse/MDL-43938) +- OPEN_BADGES_V1 is deprecated and should not be used anymore. + + For more information see [MDL-70983](https://tracker.moodle.org/browse/MDL-70983) + +#### Removed + +- Final removal of BADGE_BACKPACKAPIURL and BADGE_BACKPACKWEBURL. + + For more information see [MDL-70983](https://tracker.moodle.org/browse/MDL-70983) + +#### Added + +- New webservices enable_badges and disable_badges have been added. + + For more information see [MDL-82168](https://tracker.moodle.org/browse/MDL-82168) + ### core #### Removed @@ -28,22 +51,38 @@ The format of this change log follows the advice given at [Keep a CHANGELOG](htt - Final deprecation of \core\task\manager::ensure_adhoc_task_qos() For more information see [MDL-74843](https://tracker.moodle.org/browse/MDL-74843) +- Remove deprecation layer for YUI Events. The deprecation layer was introduced with MDL-70990 and MDL-72291. -#### Changed + For more information see [MDL-77167](https://tracker.moodle.org/browse/MDL-77167) -- The class autoloader has been moved to an earlier point in the Moodle bootstrap. - Autoloaded classes are now available to scripts using the `ABORT_AFTER_CONFIG` constant. +#### Added - For more information see [MDL-80275](https://tracker.moodle.org/browse/MDL-80275) -- The `\core\dataformat::get_format_instance` method is now public, and can be used to retrieve a writer instance for a given dataformat +- The following classes have been renamed and now support autoloading. + Existing classes are currently unaffected. + | Old class name | New class name | + | --- | --- | + | `\core_component` | `\core\component` | - For more information see [MDL-81781](https://tracker.moodle.org/browse/MDL-81781) + For more information see [MDL-66903](https://tracker.moodle.org/browse/MDL-66903) +- Added the ability for unit tests to autoload classes in the `\[component]\tests\` + namespace from the `[path/to/component]/tests/classes` directory. -#### Added + For more information see [MDL-66903](https://tracker.moodle.org/browse/MDL-66903) +- Added a helper to load fixtures from a components `tests/fixtures/` folder: + ```php + advanced_testcase::load_fixture(string $component, string $fixture): void; + ``` + For more information see [MDL-66903](https://tracker.moodle.org/browse/MDL-66903) +- Added stored progress bars + + For more information see [MDL-70854](https://tracker.moodle.org/browse/MDL-70854) - New DML constant `SQL_INT_MAX` to define the size of a large integer with cross database platform support For more information see [MDL-81282](https://tracker.moodle.org/browse/MDL-81282) +- The function update_display_mode will update the eye icon (enabled/disabled) in the availability. The $pluginname is represented to the plugin need to update. The $displaymode is represented to the eye icon. Whether it enabled or disabled. + + For more information see [MDL-81533](https://tracker.moodle.org/browse/MDL-81533) - Added an `exception` L2 Namespace to APIs For more information see [MDL-81903](https://tracker.moodle.org/browse/MDL-81903) @@ -149,6 +188,20 @@ The format of this change log follows the advice given at [Keep a CHANGELOG](htt For more information see [MDL-82183](https://tracker.moodle.org/browse/MDL-82183) +#### Changed + +- The class autoloader has been moved to an earlier point in the Moodle bootstrap. + Autoloaded classes are now available to scripts using the `ABORT_AFTER_CONFIG` constant. + + For more information see [MDL-80275](https://tracker.moodle.org/browse/MDL-80275) +- The `\core\dataformat::get_format_instance` method is now public, and can be used to retrieve a writer instance for a given dataformat + + For more information see [MDL-81781](https://tracker.moodle.org/browse/MDL-81781) +- The `get_home_page()` method can now return new constant `HOMEPAGE_URL`, applicable when a third-party hook has extended the default homepage options for the site + A new method, `get_default_home_page_url()` has been added which will return the correct URL when this constant is returned + + For more information see [MDL-82066](https://tracker.moodle.org/browse/MDL-82066) + #### Fixed - All the setup and tear down methods of `PHPUnit` now are required to, always, call to their parent counterparts. This is a good practice to avoid future problems, especially when updating to PHPUnit >= 10. @@ -179,6 +232,25 @@ The format of this change log follows the advice given at [Keep a CHANGELOG](htt - `lib/tokeniserlib.php` For more information see [MDL-82191](https://tracker.moodle.org/browse/MDL-82191) +- The following methods have been formally deprecated: + - `get_core_subsystems` + - `get_plugin_types` + - `get_plugin_list` + - `get_plugin_list_with_class` + - `get_plugin_directory` + - `normalize_component` + - `get_component_directory` + - `get_context_instance` + Note: These methods have been deprecated for a long time, but previously did not emit any deprecation notice. + + For more information see [MDL-82287](https://tracker.moodle.org/browse/MDL-82287) +- The following methods have been finally deprecated and will now throw an exception if called: + - `get_context_instance` + - `can_use_rotated_text` + - `get_system_context` + - `print_arrow` + + For more information see [MDL-82287](https://tracker.moodle.org/browse/MDL-82287) ### core_reportbuilder @@ -198,6 +270,12 @@ The format of this change log follows the advice given at [Keep a CHANGELOG](htt - Added a new database helper method `sql_replace_parameters` to help ensure uniqueness of parameters within a SQL expression For more information see [MDL-81434](https://tracker.moodle.org/browse/MDL-81434) +- New format helper `format_time` method, for use in column callbacks that represent a duration of time (e.g. "3 days 4 hours") + + For more information see [MDL-82466](https://tracker.moodle.org/browse/MDL-82466) +- Methods add_columns_from_entity(), add_filters_from_entity() and report_element_search() have been moved from \core_reportbuilder\datasource class to \core_reportbuilder\base class in order to be available also for system reports + + For more information see [MDL-82529](https://tracker.moodle.org/browse/MDL-82529) #### Removed @@ -255,6 +333,17 @@ The format of this change log follows the advice given at [Keep a CHANGELOG](htt - The constant `ASSIGN_ATTEMPT_REOPEN_METHOD_NONE` has been deprecated, and a new default value for `attemptreopenmethod` has been set to "Automatically until pass". For more information see [MDL-80741](https://tracker.moodle.org/browse/MDL-80741) +- Method assign_grading_table::col_picture has been deprecated. + + For more information see [MDL-82292](https://tracker.moodle.org/browse/MDL-82292) + +### tool_oauth2 + +#### Added + +- The `get_additional_login_parameters()` method now supports adding the language code to the authentication request so that the OAuth2 login page matches the language in Moodle. + + For more information see [MDL-67554](https://tracker.moodle.org/browse/MDL-67554) ### report @@ -270,6 +359,17 @@ The format of this change log follows the advice given at [Keep a CHANGELOG](htt For more information see [MDL-78773](https://tracker.moodle.org/browse/MDL-78773) +### mod_feedback + +#### Deprecated + +- The `feedback_check_is_switchrole` method has been deprecated as it didn't work + + For more information see [MDL-72424](https://tracker.moodle.org/browse/MDL-72424) +- The method `mod_feedback\output\renderer::create_template_form()` has been deprecated. It is not used anymore. + + For more information see [MDL-81742](https://tracker.moodle.org/browse/MDL-81742) + ### report_eventlist #### Deprecated @@ -280,6 +380,26 @@ The format of this change log follows the advice given at [Keep a CHANGELOG](htt For more information see [MDL-72786](https://tracker.moodle.org/browse/MDL-72786) +### core_message + +#### Removed + +- Final deprecation MESSAGE_DEFAULT_LOGGEDOFF / MESSAGE_DEFAULT_LOGGEDIN. + + For more information see [MDL-73284](https://tracker.moodle.org/browse/MDL-73284) + +#### Changed + +- The `\core_message\helper::togglecontact_link_params` now accepts a new optional param called `isrequested` to indicate the status of the contact request + + For more information see [MDL-81428](https://tracker.moodle.org/browse/MDL-81428) + +#### Deprecated + +- The `core_message/remove_contact_button` template is deprecated and will be removed in the future version + + For more information see [MDL-81428](https://tracker.moodle.org/browse/MDL-81428) + ### theme #### Removed @@ -312,6 +432,15 @@ The format of this change log follows the advice given at [Keep a CHANGELOG](htt #### Deprecated +- The behat step definition behat_grade::i_confirm_in_search_within_the_gradebook_widget_exists has been deprecated. Please use behat_general::i_confirm_in_search_combobox_exists instead. + + For more information see [MDL-80744](https://tracker.moodle.org/browse/MDL-80744) +- The behat step definition behat_grade::i_confirm_in_search_within_the_gradebook_widget_does_not_exist has been deprecated. Please use behat_general::i_confirm_in_search_combobox_does_not_exist instead. + + For more information see [MDL-80744](https://tracker.moodle.org/browse/MDL-80744) +- The behat step definition behat_grade::i_click_on_in_search_widget has been deprecated. Please use behat_general::i_click_on_in_search_combobox instead. + + For more information see [MDL-80744](https://tracker.moodle.org/browse/MDL-80744) - The `core_grades_renderer::group_selector()` method has been deprecated. Please use `\core_course\output\actionbar\renderer` to render a `group_selector` renderable instead. For more information see [MDL-80745](https://tracker.moodle.org/browse/MDL-80745) @@ -323,6 +452,26 @@ The format of this change log follows the advice given at [Keep a CHANGELOG](htt - Final deprecation and removal of core_backup\copy\copy in backup/util/ui/classes/copy.php. Please use copy_helper from backup/util/helper/copy_helper.class.php instead. For more information see [MDL-75022](https://tracker.moodle.org/browse/MDL-75022) +- Final deprecation of base_controller::get_copy(). Please use restore_controller::get_copy() instead. + + For more information see [MDL-75025](https://tracker.moodle.org/browse/MDL-75025) +- Final deprecation of base_controller::set_copy(). Please use a restore controller for storing copy information instead. + + For more information see [MDL-75025](https://tracker.moodle.org/browse/MDL-75025) + +### core_user + +#### Changed + +- The visibility of the methods: check_access_for_dynamic_submission() and get_options() in core_user\form\private_files has been changed from protected to public. + + For more information see [MDL-78293](https://tracker.moodle.org/browse/MDL-78293) + +#### Added + +- New `\core_user\hook\extend_default_homepage` hook added to allow third-party plugins to extend the default homepage options for the site + + For more information see [MDL-82066](https://tracker.moodle.org/browse/MDL-82066) ### core_question @@ -401,19 +550,13 @@ The format of this change log follows the advice given at [Keep a CHANGELOG](htt For more information see [MDL-81321](https://tracker.moodle.org/browse/MDL-81321) -### core_message - -#### Changed - -- The `\core_message\helper::togglecontact_link_params` now accepts a new optional param called `isrequested` to indicate the status of the contact request - - For more information see [MDL-81428](https://tracker.moodle.org/browse/MDL-81428) +### core_admin -#### Deprecated +#### Added -- The `core_message/remove_contact_button` template is deprecated and will be removed in the future version +- Add availability_management_table is a table which extends from plugin_management_table. Create the availability_management_table can reusable the toggle button for enabled column. - For more information see [MDL-81428](https://tracker.moodle.org/browse/MDL-81428) + For more information see [MDL-81533](https://tracker.moodle.org/browse/MDL-81533) ### editor_tiny @@ -439,6 +582,15 @@ The format of this change log follows the advice given at [Keep a CHANGELOG](htt If `istrackeduser` is pre-computed for the course module's course, it can be provided here to avoid an additional function call. For more information see [MDL-81610](https://tracker.moodle.org/browse/MDL-81610) +- New $disabled parameter has been added to select, select_optgroup and select_option html_writers to create disabled option elements. + + For more information see [MDL-82146](https://tracker.moodle.org/browse/MDL-82146) +- New \core_courseformat\output\local\content\basecontrolmenu class has been created. Existing \core_courseformat\output\local\content\cm\controlmenu and \core_courseformat\output\local\content\section\controlmenu classes extend the new \core_courseformat\output\local\content\basecontrolmenu class. + + For more information see [MDL-82510](https://tracker.moodle.org/browse/MDL-82510) +- New \core_courseformat\output\local\content\cm\delegatedcontrolmenu class has been created extending \core_courseformat\output\local\content\basecontrolmenu class to render delegated section action menu combining section and module action menu. + + For more information see [MDL-82510](https://tracker.moodle.org/browse/MDL-82510) ### core_course @@ -447,6 +599,9 @@ The format of this change log follows the advice given at [Keep a CHANGELOG](htt - - New optional sectionNum parameter has been added to activitychooser AMD module initializer. - New option sectionnum parameter has been added to get_course_content_items() external function. - New optional sectionnum parameter has been added to get_content_items_for_user_in_course() function. For more information see [MDL-81675](https://tracker.moodle.org/browse/MDL-81675) +- Webservices `core_course_get_courses_by_field` now accepts a new parameter `sectionid` to be able to retrieve the course that has the indicated section + + For more information see [MDL-81699](https://tracker.moodle.org/browse/MDL-81699) #### Deprecated @@ -463,14 +618,6 @@ The format of this change log follows the advice given at [Keep a CHANGELOG](htt For more information see [MDL-81872](https://tracker.moodle.org/browse/MDL-81872) -### mod_feedback - -#### Deprecated - -- The method `mod_feedback\output\renderer::create_template_form()` has been deprecated. It is not used anymore. - - For more information see [MDL-81742](https://tracker.moodle.org/browse/MDL-81742) - ### core_completion #### Changed @@ -498,6 +645,12 @@ The format of this change log follows the advice given at [Keep a CHANGELOG](htt For more information see [MDL-81818](https://tracker.moodle.org/browse/MDL-81818) +#### Added + +- Upon upgrading Font Awesome from version 4 to 6, the solid family was selected by default. However, FA6 includes additional families such as regular and brands. Support for these families has now been integrated, allowing icons defined with icon_system::FONTAWESOME to use them. Icons can add the FontAwesome family (fa-regular, fa-brands, fa-solid) near the icon name to display it using this styling. + + For more information see [MDL-82210](https://tracker.moodle.org/browse/MDL-82210) + ### availability #### Changed @@ -505,3 +658,19 @@ The format of this change log follows the advice given at [Keep a CHANGELOG](htt - The base class `info::get_groups` method has a `$userid` parameter to specify for which user you want to retrieve course groups (defaults to current user) For more information see [MDL-81850](https://tracker.moodle.org/browse/MDL-81850) + +### report_log + +#### Added + +- get_activities_list() function returns also an array of disabled elements, apart from the array of activities. + + For more information see [MDL-82146](https://tracker.moodle.org/browse/MDL-82146) + +### customfield_select + +#### Changed + +- The field controller `get_options` method now returns each option pre-formatted + + For more information see [MDL-82481](https://tracker.moodle.org/browse/MDL-82481) diff --git a/admin/UPGRADING.md b/admin/UPGRADING.md new file mode 100644 index 0000000000000..8c65af633ddcc --- /dev/null +++ b/admin/UPGRADING.md @@ -0,0 +1,9 @@ +# core_admin (subsystem) Upgrade notes + +## 4.5dev + +### Added + +- Add availability_management_table is a table which extends from plugin_management_table. Create the availability_management_table can reusable the toggle button for enabled column. + + For more information see [MDL-81533](https://tracker.moodle.org/browse/MDL-81533) diff --git a/admin/classes/reportbuilder/local/entities/task_log.php b/admin/classes/reportbuilder/local/entities/task_log.php index 8cc6cdf2bfc20..4922822744a6f 100644 --- a/admin/classes/reportbuilder/local/entities/task_log.php +++ b/admin/classes/reportbuilder/local/entities/task_log.php @@ -183,15 +183,7 @@ protected function get_all_columns(): array { ->set_type(column::TYPE_FLOAT) ->add_field("{$tablealias}.timeend - {$tablealias}.timestart", 'duration') ->set_is_sortable(true) - ->add_callback(static function(float $value): string { - $duration = round($value, 2); - if (empty($duration)) { - // The format_time function returns 'now' when the difference is exactly 0. - // Note: format_time performs concatenation in exactly this fashion so we should do this for consistency. - return '0 ' . get_string('secs', 'moodle'); - } - return format_time($duration); - }); + ->add_callback([format::class, 'format_time'], 2); // Hostname column. $columns[] = (new column( @@ -211,11 +203,8 @@ protected function get_all_columns(): array { $this->get_entity_name() )) ->add_joins($this->get_joins()) - ->set_type(column::TYPE_INTEGER) ->add_field("{$tablealias}.pid") - ->set_is_sortable(true) - // Although this is an integer column, it doesn't make sense to perform numeric aggregation on it. - ->set_disabled_aggregation(['avg', 'count', 'countdistinct', 'max', 'min', 'sum']); + ->set_is_sortable(true); // Database column. $columns[] = (new column( @@ -224,17 +213,14 @@ protected function get_all_columns(): array { $this->get_entity_name() )) ->add_joins($this->get_joins()) - ->set_type(column::TYPE_INTEGER) ->add_fields("{$tablealias}.dbreads, {$tablealias}.dbwrites") ->set_is_sortable(true, ["{$tablealias}.dbreads", "{$tablealias}.dbwrites"]) - ->add_callback(static function(int $value, stdClass $row): string { + ->add_callback(static function($value, stdClass $row): string { $output = ''; $output .= \html_writer::div(get_string('task_stats:dbreads', 'admin', $row->dbreads)); $output .= \html_writer::div(get_string('task_stats:dbwrites', 'admin', $row->dbwrites)); return $output; - }) - // Although this is an integer column, it doesn't make sense to perform numeric aggregation on it. - ->set_disabled_aggregation(['avg', 'count', 'countdistinct', 'max', 'min', 'sum']); + }); // Database reads column. $columns[] = (new column( diff --git a/admin/classes/table/availability_management_table.php b/admin/classes/table/availability_management_table.php new file mode 100644 index 0000000000000..834d6c7b2aea3 --- /dev/null +++ b/admin/classes/table/availability_management_table.php @@ -0,0 +1,85 @@ +. + +namespace core_admin\table; + +use moodle_url; +use stdClass; +use html_writer; +use get_string_manager; + +/** + * Availability admin settings. + * + * @package core_admin + * @copyright 2024 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class availability_management_table extends plugin_management_table { + + protected function get_table_id(): string { + return 'availabilityconditions_administration_table'; + } + + protected function get_plugintype(): string { + return 'availability'; + } + + protected function get_action_url(array $params = []): moodle_url { + return new moodle_url('/admin/tool/availabilityconditions/', $params); + } + + public function guess_base_url(): void { + $this->define_baseurl( + new moodle_url('/admin/tool/availabilityconditions/') + ); + } + + protected function get_column_list(): array { + return [ + 'name' => get_string('plugin'), + 'version' => get_string('version'), + 'enabled' => get_string('enabled', 'admin'), + 'defaultdisplaymode' => get_string('defaultdisplaymode', 'tool_availabilityconditions'), + ]; + } + + public function setup(): void { + $this->set_attribute('id', 'availabilityconditions_administration_table'); + $this->set_attribute('class', 'admintable generaltable'); + parent::setup(); + } + + protected function col_name(stdClass $row): string { + return html_writer::span( + get_string('pluginname', 'availability_' . $row->plugininfo->name) + ); + } + + protected function col_defaultdisplaymode(stdClass $row): string { + global $OUTPUT, $CFG; + $displaymode = get_config('availability_' . $row->plugininfo->name, 'defaultdisplaymode') ? 'show' : 'hide'; + $paramsdisplaymode = [ + 'sesskey' => sesskey(), + 'plugin' => $row->plugininfo->name, + 'displaymode' => $displaymode, + ]; + $urldisplaymode = new moodle_url('/' . $CFG->admin . '/tool/availabilityconditions/', $paramsdisplaymode); + + return html_writer::link($urldisplaymode, $OUTPUT->pix_icon('t/' . $displaymode, + get_string($displaymode)), ['class' => 'display-mode-' . $row->plugininfo->name]); + } +} diff --git a/admin/roles/classes/reportbuilder/local/entities/role_assignment.php b/admin/roles/classes/reportbuilder/local/entities/role_assignment.php index fda44a9c73a5b..55eda7c9f611f 100644 --- a/admin/roles/classes/reportbuilder/local/entities/role_assignment.php +++ b/admin/roles/classes/reportbuilder/local/entities/role_assignment.php @@ -102,7 +102,6 @@ protected function get_all_columns(): array { $this->get_entity_name() )) ->add_joins($this->get_joins()) - ->set_type(column::TYPE_TEXT) ->add_field("{$raalias}.component") ->set_is_sortable(true); @@ -113,7 +112,6 @@ protected function get_all_columns(): array { $this->get_entity_name() )) ->add_joins($this->get_joins()) - ->set_type(column::TYPE_INTEGER) ->add_field("{$raalias}.itemid") ->set_is_sortable(true); diff --git a/admin/settings/appearance.php b/admin/settings/appearance.php index 56b1c0724c8c5..1dce8ecc55941 100644 --- a/admin/settings/appearance.php +++ b/admin/settings/appearance.php @@ -152,6 +152,12 @@ } $choices[HOMEPAGE_MYCOURSES] = new lang_string('mycourses', 'admin'); $choices[HOMEPAGE_USER] = new lang_string('userpreference', 'admin'); + + // Allow hook callbacks to extend options. + $hook = new \core_user\hook\extend_default_homepage(); + \core\di::get(\core\hook\manager::class)->dispatch($hook); + $choices += $hook->get_options(); + $temp->add(new admin_setting_configselect('defaulthomepage', new lang_string('defaulthomepage', 'admin'), new lang_string('configdefaulthomepage', 'admin'), get_default_home_page(), $choices)); if (!isset($CFG->enabledashboard) || $CFG->enabledashboard) { diff --git a/admin/settings/badges.php b/admin/settings/badges.php index 997808e9e530b..e992f57c3aa47 100644 --- a/admin/settings/badges.php +++ b/admin/settings/badges.php @@ -87,7 +87,7 @@ $ADMIN->add('badges', new admin_externalpage('newbadge', new lang_string('newbadge', 'badges'), - new moodle_url('/badges/newbadge.php', array('type' => BADGE_TYPE_SITE)), + new moodle_url('/badges/edit.php', ['action' => 'new']), array('moodle/badges:createbadge'), empty($CFG->enablebadges) ) ); diff --git a/admin/settings/plugins.php b/admin/settings/plugins.php index 1a4761308fd9d..9604e436a5417 100644 --- a/admin/settings/plugins.php +++ b/admin/settings/plugins.php @@ -544,6 +544,18 @@ $settings->add(new admin_setting_configselect('question_preview/history', get_string('responsehistory', 'question'), '', 0, $hiddenofvisible)); + // Question editing settings. + $settings = new admin_settingpage('qediting', + get_string('questionediting', 'question'), + 'moodle/question:config'); + $ADMIN->add('qtypesettings', $settings); + + $settings->add(new admin_setting_heading('qediting_options', + '', get_string('questionediting_desc', 'question'))); + + $settings->add(new admin_setting_configcheckbox('questiondefaultssave', + get_string('questiondefaultssave', 'question'), get_string('questiondefaultssave_desc', 'question'), 1)); + // Settings for particular question types. $plugins = core_plugin_manager::instance()->get_plugins_of_type('qtype'); core_collator::asort_objects_by_property($plugins, 'displayname'); diff --git a/admin/settings/security.php b/admin/settings/security.php index 081da82d56ce3..f069b6584891f 100644 --- a/admin/settings/security.php +++ b/admin/settings/security.php @@ -126,7 +126,7 @@ $temp->add($adminsetting); $temp->add(new admin_setting_configcheckbox('passwordchangelogout', new lang_string('passwordchangelogout', 'admin'), - new lang_string('passwordchangelogout_desc', 'admin'), 0)); + new lang_string('passwordchangelogout_desc', 'admin'), 1)); $temp->add(new admin_setting_configcheckbox('passwordchangetokendeletion', new lang_string('passwordchangetokendeletion', 'admin'), diff --git a/admin/tool/admin_presets/tests/behat/apply_presets.feature b/admin/tool/admin_presets/tests/behat/apply_presets.feature index e8e933d0a4670..04c27d21a8a0b 100644 --- a/admin/tool/admin_presets/tests/behat/apply_presets.feature +++ b/admin/tool/admin_presets/tests/behat/apply_presets.feature @@ -14,7 +14,7 @@ Feature: I can apply presets Given I navigate to "Plugins > Activity modules > Manage activities" in site administration And I should see "Disable Database" And I navigate to "Plugins > Availability restrictions > Manage restrictions" in site administration - And "Hide" "icon" should exist in the "Restriction by grouping" "table_row" + And I should see "Disable Restriction by grouping" And I navigate to "Plugins > Blocks > Manage blocks" in site administration And I should see "Disable Logged in user" And I navigate to "Plugins > Course formats > Manage course formats" in site administration @@ -107,7 +107,7 @@ Feature: I can apply presets And I navigate to "Plugins > Activity modules > Manage activities" in site administration And I should see "Enable Database" And I navigate to "Plugins > Availability restrictions > Manage restrictions" in site administration - And "Hide" "icon" should not exist in the "Restriction by grouping" "table_row" + And I should see "Enable Restriction by grouping" And I navigate to "Plugins > Blocks > Manage blocks" in site administration And I should see "Enable Logged in user" And I navigate to "Plugins > Course formats > Manage course formats" in site administration diff --git a/admin/tool/admin_presets/tests/behat/revert_changes.feature b/admin/tool/admin_presets/tests/behat/revert_changes.feature index 2f4a1595632ba..a3da9a4c3013f 100644 --- a/admin/tool/admin_presets/tests/behat/revert_changes.feature +++ b/admin/tool/admin_presets/tests/behat/revert_changes.feature @@ -21,7 +21,7 @@ Feature: I can revert changes after a load And I navigate to "Plugins > Activity modules > Manage activities" in site administration And I should see "Enable Database" And I navigate to "Plugins > Availability restrictions > Manage restrictions" in site administration - And "Hide" "icon" should not exist in the "Restriction by grouping" "table_row" + And I should see "Enable Restriction by grouping" And I navigate to "Plugins > Blocks > Manage blocks" in site administration And "Disable Logged in user" "icon" should not exist in the "Logged in user" "table_row" And I navigate to "Plugins > Course formats > Manage course formats" in site administration @@ -39,7 +39,7 @@ Feature: I can revert changes after a load And I navigate to "Plugins > Activity modules > Manage activities" in site administration And I should see "Disable Database" And I navigate to "Plugins > Availability restrictions > Manage restrictions" in site administration - And "Hide" "icon" should exist in the "Restriction by grouping" "table_row" + And I should see "Disable Restriction by grouping" And I navigate to "Plugins > Blocks > Manage blocks" in site administration And I should see "Disable Logged in user" And I navigate to "Plugins > Course formats > Manage course formats" in site administration diff --git a/admin/tool/availabilityconditions/index.php b/admin/tool/availabilityconditions/index.php index 7ddb049f93a0c..f2acb4e880a81 100644 --- a/admin/tool/availabilityconditions/index.php +++ b/admin/tool/availabilityconditions/index.php @@ -31,7 +31,7 @@ admin_externalpage_setup('manageavailability'); // Get sorted list of all availability condition plugins. -$plugins = array(); +$plugins = []; foreach (core_component::get_plugin_list('availability') as $plugin => $plugindir) { if (get_string_manager()->string_exists('pluginname', 'availability_' . $plugin)) { $strpluginname = get_string('pluginname', 'availability_' . $plugin); @@ -44,20 +44,30 @@ // Do plugin actions. $pageurl = new moodle_url('/' . $CFG->admin . '/tool/availabilityconditions/'); +$classavailability = \core_plugin_manager::resolve_plugininfo_class('availability'); if (($plugin = optional_param('plugin', '', PARAM_PLUGIN))) { require_sesskey(); if (!array_key_exists($plugin, $plugins)) { throw new \moodle_exception('invalidcomponent', 'error', $pageurl); } - $action = required_param('action', PARAM_ALPHA); - switch ($action) { + $action = optional_param('action', '', PARAM_ALPHA); + if ($action === 'hide' && $classavailability::enable_plugin($plugin, 0)) { + \core\notification::add( + \core\notification::SUCCESS + ); + } else if ($action === 'show' && $classavailability::enable_plugin($plugin, 1)) { + \core\notification::add( + \core\notification::SUCCESS + ); + } + + $displaymode = optional_param('displaymode', '', PARAM_ALPHA); + switch ($displaymode) { case 'hide' : - $class = \core_plugin_manager::resolve_plugininfo_class('availability'); - $class::enable_plugin($plugin, false); + $classavailability::update_display_mode($plugin, false); break; case 'show' : - $class = \core_plugin_manager::resolve_plugininfo_class('availability'); - $class::enable_plugin($plugin, true); + $classavailability::update_display_mode($plugin, true); break; } @@ -68,48 +78,6 @@ echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('manageplugins', 'availability')); -// Show a table of installed availability conditions. -$table = new flexible_table('availabilityconditions_administration_table'); -$table->define_columns(array('name', 'version', 'enable')); -$table->define_headers(array(get_string('plugin'), - get_string('version'), get_string('hide') . '/' . get_string('show'))); -$table->define_baseurl($PAGE->url); -$table->set_attribute('id', 'availabilityconditions'); -$table->set_attribute('class', 'admintable generaltable'); -$table->setup(); - -$enabledlist = core\plugininfo\availability::get_enabled_plugins(); -foreach ($plugins as $plugin => $name) { - - // Get version or ? if unknown. - $version = get_config('availability_' . $plugin); - if (!empty($version->version)) { - $version = $version->version; - } else { - $version = '?'; - } - - // Get enabled status and use to grey out name if necessary. - $enabled = in_array($plugin, $enabledlist); - if ($enabled) { - $enabledaction = 'hide'; - $enabledstr = get_string('hide'); - $class = ''; - } else { - $enabledaction = 'show'; - $enabledstr = get_string('show'); - $class = 'dimmed_text'; - } - - // Make enable control. This is a POST request (using a form control rather - // than just a link) because it makes a database change. - $params = array('sesskey' => sesskey(), 'plugin' => $plugin, 'action' => $enabledaction); - $url = new moodle_url('/' . $CFG->admin . '/tool/availabilityconditions/', $params); - $enablecontrol = html_writer::link($url, $OUTPUT->pix_icon('t/' . $enabledaction, $enabledstr)); - - $table->add_data([$name, $version, $enablecontrol], $class); -} - -$table->print_html(); - +$table = new \core_admin\table\availability_management_table(); +$table->out(); echo $OUTPUT->footer(); diff --git a/admin/tool/availabilityconditions/lang/en/tool_availabilityconditions.php b/admin/tool/availabilityconditions/lang/en/tool_availabilityconditions.php index 651e576d38d5d..2cd0424321f75 100644 --- a/admin/tool/availabilityconditions/lang/en/tool_availabilityconditions.php +++ b/admin/tool/availabilityconditions/lang/en/tool_availabilityconditions.php @@ -22,6 +22,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['defaultdisplaymode'] = 'Default display mode'; $string['manageplugins'] = 'Manage restrictions'; $string['pluginname'] = 'Availability condition management'; $string['privacy:metadata'] = 'The Availability condition management plugin does not store any personal data.'; diff --git a/admin/tool/availabilityconditions/tests/behat/manage_conditions.feature b/admin/tool/availabilityconditions/tests/behat/manage_conditions.feature index cb3a459e48d33..0e1303fd6323a 100644 --- a/admin/tool/availabilityconditions/tests/behat/manage_conditions.feature +++ b/admin/tool/availabilityconditions/tests/behat/manage_conditions.feature @@ -44,16 +44,16 @@ Feature: Manage availability conditions # Check the icon is there (it should be a Hide icon, meaning is currently visible). Then "Hide" "icon" should exist in the "Restriction by date" "table_row" - # Click the icon. It should toggle to hidden (title=Show). - And I click on "Hide" "icon" in the "Restriction by date" "table_row" - And "Show" "icon" should exist in the "Restriction by date" "table_row" + # Click the icon. It should toggle to disabled. + And I toggle the "Disable Restriction by date" admin switch "off" + And I should see "Restriction by date disabled." # Toggle it back to visible (title=Hide). - And I click on "Show" "icon" in the "Restriction by date" "table_row" - And "Hide" "icon" should exist in the "Restriction by date" "table_row" + And I toggle the "Enable Restriction by date" admin switch "on" + And I should see "Restriction by date enabled." # OK, toggling works. Set the grade one to Hide and we'll go see if it actually worked. - And I click on "Hide" "icon" in the "Restriction by grade" "table_row" + And I toggle the "Disable Restriction by grade" admin switch "off" And I am on the "P1" "page activity editing" page And I expand all fieldsets And I click on "Add restriction..." "button" diff --git a/admin/tool/componentlibrary/content/library/moodle-javascript.md b/admin/tool/componentlibrary/content/library/moodle-javascript.md index cc3f178aa7716..dae1c335b3d35 100644 --- a/admin/tool/componentlibrary/content/library/moodle-javascript.md +++ b/admin/tool/componentlibrary/content/library/moodle-javascript.md @@ -19,7 +19,7 @@ In order for this to work you need to use the JavaScript syntax used in core Mus PlaceKitten Bootstrap 11 mins ago - diff --git a/admin/tool/componentlibrary/content/moodle/components/moodle-icons.md b/admin/tool/componentlibrary/content/moodle/components/moodle-icons.md index 269be9340febf..23e5ca4031e9d 100644 --- a/admin/tool/componentlibrary/content/moodle/components/moodle-icons.md +++ b/admin/tool/componentlibrary/content/moodle/components/moodle-icons.md @@ -11,7 +11,7 @@ tags: ## Description -Most Moodle icons are rendered using the 6.5.1 versions of [Fontawesome](https://fontawesome.com/v6/search). Iconnames are mapped from the Moodle icon name to the Font Awesome icon names in `/lib/classes/output/icon_system_fontawesome.php` +Most Moodle icons are rendered using the 6.5.2 versions of [Fontawesome](https://fontawesome.com/v6/search). Iconnames are mapped from the Moodle icon name to the Font Awesome icon names in `/lib/classes/output/icon_system_fontawesome.php` If needed a theme can override this map and provide its own mapping. diff --git a/admin/tool/componentlibrary/content/moodle/components/notifications.md b/admin/tool/componentlibrary/content/moodle/components/notifications.md index d323dd9488acb..518a62f7b585c 100644 --- a/admin/tool/componentlibrary/content/moodle/components/notifications.md +++ b/admin/tool/componentlibrary/content/moodle/components/notifications.md @@ -18,7 +18,7 @@ Notifications are coupled with actions and provide instant feedback to the actio {{< example show_markup="false">}} {{< /example >}} diff --git a/admin/tool/componentlibrary/hugo/site/data/fontawesomeicons.json b/admin/tool/componentlibrary/hugo/site/data/fontawesomeicons.json index d8da8e95d2772..8bc87fe1b2db4 100644 --- a/admin/tool/componentlibrary/hugo/site/data/fontawesomeicons.json +++ b/admin/tool/componentlibrary/hugo/site/data/fontawesomeicons.json @@ -1 +1 @@ -[{"name":"core:docs","icon":"fa-info-circle","standardicon":"\"core:docs\""},{"name":"core:book","icon":"fa-book","standardicon":"\"core:book\""},{"name":"core:help","icon":"fa-question-circle text-info","standardicon":"\"core:help\""},{"name":"core:req","icon":"fa-exclamation-circle text-danger","standardicon":"\"core:req\""},{"name":"core:a\/add_file","icon":"fa-file-o","standardicon":"\"core:a\/add_file\""},{"name":"core:a\/create_folder","icon":"fa-folder-o","standardicon":"\"core:a\/create_folder\""},{"name":"core:a\/download_all","icon":"fa-download","standardicon":"\"core:a\/download_all\""},{"name":"core:a\/help","icon":"fa-question-circle text-info","standardicon":"\"core:a\/help\""},{"name":"core:a\/logout","icon":"fa-sign-out","standardicon":"\"core:a\/logout\""},{"name":"core:a\/refresh","icon":"fa-refresh","standardicon":"\"core:a\/refresh\""},{"name":"core:a\/search","icon":"fa-search","standardicon":"\"core:a\/search\""},{"name":"core:a\/setting","icon":"fa-cog","standardicon":"\"core:a\/setting\""},{"name":"core:a\/view_icon_active","icon":"fa-th","standardicon":"\"core:a\/view_icon_active\""},{"name":"core:a\/view_list_active","icon":"fa-list","standardicon":"\"core:a\/view_list_active\""},{"name":"core:a\/view_tree_active","icon":"fa-folder","standardicon":"\"core:a\/view_tree_active\""},{"name":"core:b\/bookmark-new","icon":"fa-bookmark","standardicon":"\"core:b\/bookmark-new\""},{"name":"core:b\/document-edit","icon":"fa-pencil","standardicon":"\"core:b\/document-edit\""},{"name":"core:b\/document-new","icon":"fa-file-o","standardicon":"\"core:b\/document-new\""},{"name":"core:b\/document-properties","icon":"fa-info","standardicon":"\"core:b\/document-properties\""},{"name":"core:b\/edit-copy","icon":"fa-files-o","standardicon":"\"core:b\/edit-copy\""},{"name":"core:b\/edit-delete","icon":"fa-trash","standardicon":"\"core:b\/edit-delete\""},{"name":"core:e\/abbr","icon":"fa-comment","standardicon":"\"core:e\/abbr\""},{"name":"core:e\/absolute","icon":"fa-crosshairs","standardicon":"\"core:e\/absolute\""},{"name":"core:e\/accessibility_checker","icon":"fa-universal-access","standardicon":"\"core:e\/accessibility_checker\""},{"name":"core:e\/acronym","icon":"fa-comment","standardicon":"\"core:e\/acronym\""},{"name":"core:e\/advance_hr","icon":"fa-arrows-h","standardicon":"\"core:e\/advance_hr\""},{"name":"core:e\/align_center","icon":"fa-align-center","standardicon":"\"core:e\/align_center\""},{"name":"core:e\/align_left","icon":"fa-align-left","standardicon":"\"core:e\/align_left\""},{"name":"core:e\/align_right","icon":"fa-align-right","standardicon":"\"core:e\/align_right\""},{"name":"core:e\/anchor","icon":"fa-chain","standardicon":"\"core:e\/anchor\""},{"name":"core:e\/backward","icon":"fa-undo","standardicon":"\"core:e\/backward\""},{"name":"core:e\/bold","icon":"fa-bold","standardicon":"\"core:e\/bold\""},{"name":"core:e\/bullet_list","icon":"fa-list-ul","standardicon":"\"core:e\/bullet_list\""},{"name":"core:e\/cancel","icon":"fa-times","standardicon":"\"core:e\/cancel\""},{"name":"core:e\/cancel_solid_circle","icon":"fas fa-times-circle","standardicon":"\"core:e\/cancel_solid_circle\""},{"name":"core:e\/cell_props","icon":"fa-info","standardicon":"\"core:e\/cell_props\""},{"name":"core:e\/cite","icon":"fa-quote-right","standardicon":"\"core:e\/cite\""},{"name":"core:e\/cleanup_messy_code","icon":"fa-eraser","standardicon":"\"core:e\/cleanup_messy_code\""},{"name":"core:e\/clear_formatting","icon":"fa-i-cursor","standardicon":"\"core:e\/clear_formatting\""},{"name":"core:e\/copy","icon":"fa-clone","standardicon":"\"core:e\/copy\""},{"name":"core:e\/cut","icon":"fa-scissors","standardicon":"\"core:e\/cut\""},{"name":"core:e\/decrease_indent","icon":"fa-outdent","standardicon":"\"core:e\/decrease_indent\""},{"name":"core:e\/delete_col","icon":"fa-minus","standardicon":"\"core:e\/delete_col\""},{"name":"core:e\/delete_row","icon":"fa-minus","standardicon":"\"core:e\/delete_row\""},{"name":"core:e\/delete","icon":"fa-minus","standardicon":"\"core:e\/delete\""},{"name":"core:e\/delete_table","icon":"fa-minus","standardicon":"\"core:e\/delete_table\""},{"name":"core:e\/document_properties","icon":"fa-info","standardicon":"\"core:e\/document_properties\""},{"name":"core:e\/emoticons","icon":"fa-smile-o","standardicon":"\"core:e\/emoticons\""},{"name":"core:e\/find_replace","icon":"fa-search-plus","standardicon":"\"core:e\/find_replace\""},{"name":"core:e\/file-text","icon":"fa-file-text","standardicon":"\"core:e\/file-text\""},{"name":"core:e\/forward","icon":"fa-arrow-right","standardicon":"\"core:e\/forward\""},{"name":"core:e\/fullpage","icon":"fa-arrows-alt","standardicon":"\"core:e\/fullpage\""},{"name":"core:e\/fullscreen","icon":"fa-arrows-alt","standardicon":"\"core:e\/fullscreen\""},{"name":"core:e\/help","icon":"fa-question-circle","standardicon":"\"core:e\/help\""},{"name":"core:e\/increase_indent","icon":"fa-indent","standardicon":"\"core:e\/increase_indent\""},{"name":"core:e\/insert_col_after","icon":"fa-columns","standardicon":"\"core:e\/insert_col_after\""},{"name":"core:e\/insert_col_before","icon":"fa-columns","standardicon":"\"core:e\/insert_col_before\""},{"name":"core:e\/insert_date","icon":"fa-calendar","standardicon":"\"core:e\/insert_date\""},{"name":"core:e\/insert_edit_image","icon":"fa-picture-o","standardicon":"\"core:e\/insert_edit_image\""},{"name":"core:e\/insert_edit_link","icon":"fa-link","standardicon":"\"core:e\/insert_edit_link\""},{"name":"core:e\/insert_edit_video","icon":"fa-file-video-o","standardicon":"\"core:e\/insert_edit_video\""},{"name":"core:e\/insert_file","icon":"fa-file","standardicon":"\"core:e\/insert_file\""},{"name":"core:e\/insert_horizontal_ruler","icon":"fa-arrows-h","standardicon":"\"core:e\/insert_horizontal_ruler\""},{"name":"core:e\/insert_nonbreaking_space","icon":"fa-square-o","standardicon":"\"core:e\/insert_nonbreaking_space\""},{"name":"core:e\/insert_page_break","icon":"fa-level-down","standardicon":"\"core:e\/insert_page_break\""},{"name":"core:e\/insert_row_after","icon":"fa-plus","standardicon":"\"core:e\/insert_row_after\""},{"name":"core:e\/insert_row_before","icon":"fa-plus","standardicon":"\"core:e\/insert_row_before\""},{"name":"core:e\/insert","icon":"fa-plus","standardicon":"\"core:e\/insert\""},{"name":"core:e\/insert_time","icon":"fa-clock-o","standardicon":"\"core:e\/insert_time\""},{"name":"core:e\/italic","icon":"fa-italic","standardicon":"\"core:e\/italic\""},{"name":"core:e\/justify","icon":"fa-align-justify","standardicon":"\"core:e\/justify\""},{"name":"core:e\/layers_over","icon":"fa-level-up","standardicon":"\"core:e\/layers_over\""},{"name":"core:e\/layers","icon":"fa-window-restore","standardicon":"\"core:e\/layers\""},{"name":"core:e\/layers_under","icon":"fa-level-down","standardicon":"\"core:e\/layers_under\""},{"name":"core:e\/left_to_right","icon":"fa-chevron-right","standardicon":"\"core:e\/left_to_right\""},{"name":"core:e\/manage_files","icon":"fa-files-o","standardicon":"\"core:e\/manage_files\""},{"name":"core:e\/math","icon":"fa-calculator","standardicon":"\"core:e\/math\""},{"name":"core:e\/merge_cells","icon":"fa-compress","standardicon":"\"core:e\/merge_cells\""},{"name":"core:e\/new_document","icon":"fa-file-o","standardicon":"\"core:e\/new_document\""},{"name":"core:e\/numbered_list","icon":"fa-list-ol","standardicon":"\"core:e\/numbered_list\""},{"name":"core:e\/page_break","icon":"fa-level-down","standardicon":"\"core:e\/page_break\""},{"name":"core:e\/paste","icon":"fa-clipboard","standardicon":"\"core:e\/paste\""},{"name":"core:e\/paste_text","icon":"fa-clipboard","standardicon":"\"core:e\/paste_text\""},{"name":"core:e\/paste_word","icon":"fa-clipboard","standardicon":"\"core:e\/paste_word\""},{"name":"core:e\/prevent_autolink","icon":"fa-exclamation","standardicon":"\"core:e\/prevent_autolink\""},{"name":"core:e\/preview","icon":"fa-search-plus","standardicon":"\"core:e\/preview\""},{"name":"core:e\/print","icon":"fa-print","standardicon":"\"core:e\/print\""},{"name":"core:e\/question","icon":"fa-question","standardicon":"\"core:e\/question\""},{"name":"core:e\/redo","icon":"fa-repeat","standardicon":"\"core:e\/redo\""},{"name":"core:e\/remove_link","icon":"fa-chain-broken","standardicon":"\"core:e\/remove_link\""},{"name":"core:e\/remove_page_break","icon":"fa-remove","standardicon":"\"core:e\/remove_page_break\""},{"name":"core:e\/resize","icon":"fa-expand","standardicon":"\"core:e\/resize\""},{"name":"core:e\/restore_draft","icon":"fa-undo","standardicon":"\"core:e\/restore_draft\""},{"name":"core:e\/restore_last_draft","icon":"fa-undo","standardicon":"\"core:e\/restore_last_draft\""},{"name":"core:e\/right_to_left","icon":"fa-chevron-left","standardicon":"\"core:e\/right_to_left\""},{"name":"core:e\/row_props","icon":"fa-info","standardicon":"\"core:e\/row_props\""},{"name":"core:e\/save","icon":"fa-floppy-o","standardicon":"\"core:e\/save\""},{"name":"core:e\/screenreader_helper","icon":"fa-braille","standardicon":"\"core:e\/screenreader_helper\""},{"name":"core:e\/search","icon":"fa-search","standardicon":"\"core:e\/search\""},{"name":"core:e\/select_all","icon":"fa-arrows-h","standardicon":"\"core:e\/select_all\""},{"name":"core:e\/show_invisible_characters","icon":"fa-eye-slash","standardicon":"\"core:e\/show_invisible_characters\""},{"name":"core:e\/source_code","icon":"fa-code","standardicon":"\"core:e\/source_code\""},{"name":"core:e\/special_character","icon":"fa-pencil-square-o","standardicon":"\"core:e\/special_character\""},{"name":"core:e\/spellcheck","icon":"fa-check","standardicon":"\"core:e\/spellcheck\""},{"name":"core:e\/split_cells","icon":"fa-columns","standardicon":"\"core:e\/split_cells\""},{"name":"core:e\/strikethrough","icon":"fa-strikethrough","standardicon":"\"core:e\/strikethrough\""},{"name":"core:e\/styleparagraph","icon":"fa-font","standardicon":"\"core:e\/styleparagraph\""},{"name":"core:e\/subscript","icon":"fa-subscript","standardicon":"\"core:e\/subscript\""},{"name":"core:e\/superscript","icon":"fa-superscript","standardicon":"\"core:e\/superscript\""},{"name":"core:e\/table_props","icon":"fa-table","standardicon":"\"core:e\/table_props\""},{"name":"core:e\/table","icon":"fa-table","standardicon":"\"core:e\/table\""},{"name":"core:e\/template","icon":"fa-sticky-note","standardicon":"\"core:e\/template\""},{"name":"core:e\/text_color_picker","icon":"fa-paint-brush","standardicon":"\"core:e\/text_color_picker\""},{"name":"core:e\/text_color","icon":"fa-paint-brush","standardicon":"\"core:e\/text_color\""},{"name":"core:e\/text_highlight_picker","icon":"fa-lightbulb-o","standardicon":"\"core:e\/text_highlight_picker\""},{"name":"core:e\/text_highlight","icon":"fa-lightbulb-o","standardicon":"\"core:e\/text_highlight\""},{"name":"core:e\/tick","icon":"fa-check","standardicon":"\"core:e\/tick\""},{"name":"core:e\/toggle_blockquote","icon":"fa-quote-left","standardicon":"\"core:e\/toggle_blockquote\""},{"name":"core:e\/underline","icon":"fa-underline","standardicon":"\"core:e\/underline\""},{"name":"core:e\/undo","icon":"fa-undo","standardicon":"\"core:e\/undo\""},{"name":"core:e\/visual_aid","icon":"fa-universal-access","standardicon":"\"core:e\/visual_aid\""},{"name":"core:e\/visual_blocks","icon":"fa-audio-description","standardicon":"\"core:e\/visual_blocks\""},{"name":"core:i\/activities","icon":"fa-file-pen","standardicon":"\"core:i\/activities\""},{"name":"core:i\/addblock","icon":"fa-plus-square","standardicon":"\"core:i\/addblock\""},{"name":"core:i\/assignroles","icon":"fa-user-plus","standardicon":"\"core:i\/assignroles\""},{"name":"core:i\/asterisk","icon":"fa-asterisk","standardicon":"\"core:i\/asterisk\""},{"name":"core:i\/backup","icon":"fa-file-zip-o","standardicon":"\"core:i\/backup\""},{"name":"core:i\/badge","icon":"fa-shield","standardicon":"\"core:i\/badge\""},{"name":"core:i\/breadcrumbdivider","icon":"fa-angle-right","standardicon":"\"core:i\/breadcrumbdivider\""},{"name":"core:i\/bullhorn","icon":"fa-bullhorn","standardicon":"\"core:i\/bullhorn\""},{"name":"core:i\/calc","icon":"fa-calculator","standardicon":"\"core:i\/calc\""},{"name":"core:i\/calendar","icon":"fa-calendar","standardicon":"\"core:i\/calendar\""},{"name":"core:i\/calendareventdescription","icon":"fa-align-left","standardicon":"\"core:i\/calendareventdescription\""},{"name":"core:i\/calendareventtime","icon":"fa-clock-o","standardicon":"\"core:i\/calendareventtime\""},{"name":"core:i\/caution","icon":"fa-exclamation text-warning","standardicon":"\"core:i\/caution\""},{"name":"core:i\/checked","icon":"fa-check","standardicon":"\"core:i\/checked\""},{"name":"core:i\/checkedcircle","icon":"fa-check-circle","standardicon":"\"core:i\/checkedcircle\""},{"name":"core:i\/checkpermissions","icon":"fa-unlock-alt","standardicon":"\"core:i\/checkpermissions\""},{"name":"core:i\/cohort","icon":"fa-users","standardicon":"\"core:i\/cohort\""},{"name":"core:i\/competencies","icon":"fa-check-square-o","standardicon":"\"core:i\/competencies\""},{"name":"core:i\/completion_self","icon":"fa-user-o","standardicon":"\"core:i\/completion_self\""},{"name":"core:i\/contentbank","icon":"fa-paint-brush","standardicon":"\"core:i\/contentbank\""},{"name":"core:i\/dashboard","icon":"fa-tachometer","standardicon":"\"core:i\/dashboard\""},{"name":"core:i\/categoryevent","icon":"fa-cubes","standardicon":"\"core:i\/categoryevent\""},{"name":"core:i\/chartbar","icon":"fa-chart-bar","standardicon":"\"core:i\/chartbar\""},{"name":"core:i\/course","icon":"fa-graduation-cap","standardicon":"\"core:i\/course\""},{"name":"core:i\/courseevent","icon":"fa-graduation-cap","standardicon":"\"core:i\/courseevent\""},{"name":"core:i\/customfield","icon":"fa-hand-o-right","standardicon":"\"core:i\/customfield\""},{"name":"core:i\/db","icon":"fa-database","standardicon":"\"core:i\/db\""},{"name":"core:i\/delete","icon":"fa-trash","standardicon":"\"core:i\/delete\""},{"name":"core:i\/down","icon":"fa-arrow-down","standardicon":"\"core:i\/down\""},{"name":"core:i\/dragdrop","icon":"fa-arrows","standardicon":"\"core:i\/dragdrop\""},{"name":"core:i\/duration","icon":"fa-clock-o","standardicon":"\"core:i\/duration\""},{"name":"core:i\/emojicategoryactivities","icon":"fa-futbol-o","standardicon":"\"core:i\/emojicategoryactivities\""},{"name":"core:i\/emojicategoryanimalsnature","icon":"fa-leaf","standardicon":"\"core:i\/emojicategoryanimalsnature\""},{"name":"core:i\/emojicategoryflags","icon":"fa-flag","standardicon":"\"core:i\/emojicategoryflags\""},{"name":"core:i\/emojicategoryfooddrink","icon":"fa-cutlery","standardicon":"\"core:i\/emojicategoryfooddrink\""},{"name":"core:i\/emojicategoryobjects","icon":"fa-lightbulb-o","standardicon":"\"core:i\/emojicategoryobjects\""},{"name":"core:i\/emojicategorypeoplebody","icon":"fa-male","standardicon":"\"core:i\/emojicategorypeoplebody\""},{"name":"core:i\/emojicategoryrecent","icon":"fa-clock-o","standardicon":"\"core:i\/emojicategoryrecent\""},{"name":"core:i\/emojicategorysmileysemotion","icon":"fa-smile-o","standardicon":"\"core:i\/emojicategorysmileysemotion\""},{"name":"core:i\/emojicategorysymbols","icon":"fa-heart","standardicon":"\"core:i\/emojicategorysymbols\""},{"name":"core:i\/emojicategorytravelplaces","icon":"fa-plane","standardicon":"\"core:i\/emojicategorytravelplaces\""},{"name":"core:i\/edit","icon":"fa-pencil","standardicon":"\"core:i\/edit\""},{"name":"core:i\/email","icon":"fa-envelope","standardicon":"\"core:i\/email\""},{"name":"core:i\/empty","icon":"fa-fw","standardicon":"\"core:i\/empty\""},{"name":"core:i\/enrolmentsuspended","icon":"fa-pause","standardicon":"\"core:i\/enrolmentsuspended\""},{"name":"core:i\/enrolusers","icon":"fa-user-plus","standardicon":"\"core:i\/enrolusers\""},{"name":"core:i\/excluded","icon":"fa-minus-circle","standardicon":"\"core:i\/excluded\""},{"name":"core:i\/expired","icon":"fa-exclamation text-warning","standardicon":"\"core:i\/expired\""},{"name":"core:i\/export","icon":"fa-download","standardicon":"\"core:i\/export\""},{"name":"core:i\/link","icon":"fa-link","standardicon":"\"core:i\/link\""},{"name":"core:i\/externallink","icon":"fa-external-link","standardicon":"\"core:i\/externallink\""},{"name":"core:i\/files","icon":"fa-file","standardicon":"\"core:i\/files\""},{"name":"core:i\/filter","icon":"fa-filter","standardicon":"\"core:i\/filter\""},{"name":"core:i\/flagged","icon":"fa-flag","standardicon":"\"core:i\/flagged\""},{"name":"core:i\/folder","icon":"fa-folder","standardicon":"\"core:i\/folder\""},{"name":"core:i\/grade_correct","icon":"fa-check text-success","standardicon":"\"core:i\/grade_correct\""},{"name":"core:i\/grade_incorrect","icon":"fa-remove text-danger","standardicon":"\"core:i\/grade_incorrect\""},{"name":"core:i\/grade_partiallycorrect","icon":"fa-check-square","standardicon":"\"core:i\/grade_partiallycorrect\""},{"name":"core:i\/grades","icon":"fa-table","standardicon":"\"core:i\/grades\""},{"name":"core:i\/grading","icon":"fa-magic","standardicon":"\"core:i\/grading\""},{"name":"core:i\/gradingnotifications","icon":"fa-bell-o","standardicon":"\"core:i\/gradingnotifications\""},{"name":"core:i\/groupevent","icon":"fa-group","standardicon":"\"core:i\/groupevent\""},{"name":"core:i\/group","icon":"fa-users","standardicon":"\"core:i\/group\""},{"name":"core:i\/home","icon":"fa-home","standardicon":"\"core:i\/home\""},{"name":"core:i\/hide","icon":"fa-eye","standardicon":"\"core:i\/hide\""},{"name":"core:i\/hierarchylock","icon":"fa-lock","standardicon":"\"core:i\/hierarchylock\""},{"name":"core:i\/import","icon":"fa-level-up","standardicon":"\"core:i\/import\""},{"name":"core:i\/incorrect","icon":"fa-exclamation","standardicon":"\"core:i\/incorrect\""},{"name":"core:i\/info","icon":"fa-info","standardicon":"\"core:i\/info\""},{"name":"core:i\/invalid","icon":"fa-times text-danger","standardicon":"\"core:i\/invalid\""},{"name":"core:i\/item","icon":"fa-circle","standardicon":"\"core:i\/item\""},{"name":"core:i\/loading","icon":"fa-circle-o-notch fa-spin","standardicon":"\"core:i\/loading\""},{"name":"core:i\/loading_small","icon":"fa-circle-o-notch fa-spin","standardicon":"\"core:i\/loading_small\""},{"name":"core:i\/location","icon":"fa-map-marker","standardicon":"\"core:i\/location\""},{"name":"core:i\/lock","icon":"fa-lock","standardicon":"\"core:i\/lock\""},{"name":"core:i\/log","icon":"fa-list-alt","standardicon":"\"core:i\/log\""},{"name":"core:i\/mahara_host","icon":"fa-id-badge","standardicon":"\"core:i\/mahara_host\""},{"name":"core:i\/manual_item","icon":"fa-pencil-square-o","standardicon":"\"core:i\/manual_item\""},{"name":"core:i\/marked","icon":"fa-circle","standardicon":"\"core:i\/marked\""},{"name":"core:i\/marker","icon":"fa-circle-o","standardicon":"\"core:i\/marker\""},{"name":"core:i\/mean","icon":"fa-calculator","standardicon":"\"core:i\/mean\""},{"name":"core:i\/menu","icon":"fa-ellipsis-v","standardicon":"\"core:i\/menu\""},{"name":"core:i\/menubars","icon":"fa-bars","standardicon":"\"core:i\/menubars\""},{"name":"core:i\/messagecontentaudio","icon":"fa-headphones","standardicon":"\"core:i\/messagecontentaudio\""},{"name":"core:i\/messagecontentimage","icon":"fa-image","standardicon":"\"core:i\/messagecontentimage\""},{"name":"core:i\/messagecontentvideo","icon":"fa-film","standardicon":"\"core:i\/messagecontentvideo\""},{"name":"core:i\/messagecontentmultimediageneral","icon":"fa-file-video-o","standardicon":"\"core:i\/messagecontentmultimediageneral\""},{"name":"core:i\/mnethost","icon":"fa-external-link","standardicon":"\"core:i\/mnethost\""},{"name":"core:i\/moodle_host","icon":"fa-graduation-cap","standardicon":"\"core:i\/moodle_host\""},{"name":"core:i\/moremenu","icon":"fa-ellipsis-h","standardicon":"\"core:i\/moremenu\""},{"name":"core:i\/move_2d","icon":"fa-arrows","standardicon":"\"core:i\/move_2d\""},{"name":"core:i\/muted","icon":"fa-microphone-slash","standardicon":"\"core:i\/muted\""},{"name":"core:i\/navigationitem","icon":"fa-fw","standardicon":"\"core:i\/navigationitem\""},{"name":"core:i\/ne_red_mark","icon":"fa-remove","standardicon":"\"core:i\/ne_red_mark\""},{"name":"core:i\/new","icon":"fa-bolt","standardicon":"\"core:i\/new\""},{"name":"core:i\/news","icon":"fa-newspaper-o","standardicon":"\"core:i\/news\""},{"name":"core:i\/next","icon":"fa-chevron-right","standardicon":"\"core:i\/next\""},{"name":"core:i\/nosubcat","icon":"fa-plus-square-o","standardicon":"\"core:i\/nosubcat\""},{"name":"core:i\/notifications","icon":"fa-bell-o","standardicon":"\"core:i\/notifications\""},{"name":"core:i\/open","icon":"fa-folder-open","standardicon":"\"core:i\/open\""},{"name":"core:i\/otherevent","icon":"fa-calendar","standardicon":"\"core:i\/otherevent\""},{"name":"core:i\/outcomes","icon":"fa-tasks","standardicon":"\"core:i\/outcomes\""},{"name":"core:i\/overriden_grade","icon":"fa-edit","standardicon":"\"core:i\/overriden_grade\""},{"name":"core:i\/payment","icon":"fa-money","standardicon":"\"core:i\/payment\""},{"name":"core:i\/permissionlock","icon":"fa-lock","standardicon":"\"core:i\/permissionlock\""},{"name":"core:i\/permissions","icon":"fa-pencil-square-o","standardicon":"\"core:i\/permissions\""},{"name":"core:i\/persona_sign_in_black","icon":"fa-male","standardicon":"\"core:i\/persona_sign_in_black\""},{"name":"core:i\/portfolio","icon":"fa-id-badge","standardicon":"\"core:i\/portfolio\""},{"name":"core:i\/preview","icon":"fa-search-plus","standardicon":"\"core:i\/preview\""},{"name":"core:i\/previous","icon":"fa-chevron-left","standardicon":"\"core:i\/previous\""},{"name":"core:i\/privatefiles","icon":"fa-file-o","standardicon":"\"core:i\/privatefiles\""},{"name":"core:i\/progressbar","icon":"fa-spinner fa-spin","standardicon":"\"core:i\/progressbar\""},{"name":"core:i\/publish","icon":"fa-share","standardicon":"\"core:i\/publish\""},{"name":"core:i\/questions","icon":"fa-question","standardicon":"\"core:i\/questions\""},{"name":"core:i\/reload","icon":"fa-refresh","standardicon":"\"core:i\/reload\""},{"name":"core:i\/report","icon":"fa-area-chart","standardicon":"\"core:i\/report\""},{"name":"core:i\/repository","icon":"fa-hdd-o","standardicon":"\"core:i\/repository\""},{"name":"core:i\/restore","icon":"fa-level-up","standardicon":"\"core:i\/restore\""},{"name":"core:i\/return","icon":"fa-arrow-left","standardicon":"\"core:i\/return\""},{"name":"core:i\/risk_config","icon":"fa-exclamation text-muted","standardicon":"\"core:i\/risk_config\""},{"name":"core:i\/risk_managetrust","icon":"fa-exclamation-triangle text-warning","standardicon":"\"core:i\/risk_managetrust\""},{"name":"core:i\/risk_personal","icon":"fa-exclamation-circle text-info","standardicon":"\"core:i\/risk_personal\""},{"name":"core:i\/risk_spam","icon":"fa-exclamation text-primary","standardicon":"\"core:i\/risk_spam\""},{"name":"core:i\/risk_xss","icon":"fa-exclamation-triangle text-danger","standardicon":"\"core:i\/risk_xss\""},{"name":"core:i\/role","icon":"fa-user-md","standardicon":"\"core:i\/role\""},{"name":"core:i\/rss","icon":"fa-rss","standardicon":"\"core:i\/rss\""},{"name":"core:i\/rsssitelogo","icon":"fa-graduation-cap","standardicon":"\"core:i\/rsssitelogo\""},{"name":"core:i\/scales","icon":"fa-balance-scale","standardicon":"\"core:i\/scales\""},{"name":"core:i\/scheduled","icon":"fa-calendar-check-o","standardicon":"\"core:i\/scheduled\""},{"name":"core:i\/search","icon":"fa-search","standardicon":"\"core:i\/search\""},{"name":"core:i\/section","icon":"fa-folder-o","standardicon":"\"core:i\/section\""},{"name":"core:i\/sendmessage","icon":"fa-paper-plane","standardicon":"\"core:i\/sendmessage\""},{"name":"core:i\/settings","icon":"fa-cog","standardicon":"\"core:i\/settings\""},{"name":"core:i\/share","icon":"fa-share-square-o","standardicon":"\"core:i\/share\""},{"name":"core:i\/show","icon":"fa-eye-slash","standardicon":"\"core:i\/show\""},{"name":"core:i\/siteevent","icon":"fa-globe","standardicon":"\"core:i\/siteevent\""},{"name":"core:i\/star","icon":"fa-star","standardicon":"\"core:i\/star\""},{"name":"core:i\/star-o","icon":"fa-star-o","standardicon":"\"core:i\/star-o\""},{"name":"core:i\/star-rating","icon":"fa-star","standardicon":"\"core:i\/star-rating\""},{"name":"core:i\/stats","icon":"fa-line-chart","standardicon":"\"core:i\/stats\""},{"name":"core:i\/switch","icon":"fa-exchange","standardicon":"\"core:i\/switch\""},{"name":"core:i\/switchrole","icon":"fa-user-secret","standardicon":"\"core:i\/switchrole\""},{"name":"core:i\/trash","icon":"fa-trash","standardicon":"\"core:i\/trash\""},{"name":"core:i\/twoway","icon":"fa-arrows-h","standardicon":"\"core:i\/twoway\""},{"name":"core:i\/unchecked","icon":"fa-square-o","standardicon":"\"core:i\/unchecked\""},{"name":"core:i\/uncheckedcircle","icon":"fa-circle-o","standardicon":"\"core:i\/uncheckedcircle\""},{"name":"core:i\/unflagged","icon":"fa-flag-o","standardicon":"\"core:i\/unflagged\""},{"name":"core:i\/unlock","icon":"fa-unlock","standardicon":"\"core:i\/unlock\""},{"name":"core:i\/up","icon":"fa-arrow-up","standardicon":"\"core:i\/up\""},{"name":"core:i\/upload","icon":"fa-upload","standardicon":"\"core:i\/upload\""},{"name":"core:i\/userevent","icon":"fa-user","standardicon":"\"core:i\/userevent\""},{"name":"core:i\/user","icon":"fa-user","standardicon":"\"core:i\/user\""},{"name":"core:i\/users","icon":"fa-users","standardicon":"\"core:i\/users\""},{"name":"core:i\/valid","icon":"fa-check text-success","standardicon":"\"core:i\/valid\""},{"name":"core:i\/viewsection","icon":"fa-pager","standardicon":"\"core:i\/viewsection\""},{"name":"core:i\/warning","icon":"fa-exclamation text-warning","standardicon":"\"core:i\/warning\""},{"name":"core:i\/window_close","icon":"fa-window-close","standardicon":"\"core:i\/window_close\""},{"name":"core:i\/withsubcat","icon":"fa-plus-square","standardicon":"\"core:i\/withsubcat\""},{"name":"core:i\/language","icon":"fa-language","standardicon":"\"core:i\/language\""},{"name":"core:m\/USD","icon":"fa-usd","standardicon":"\"core:m\/USD\""},{"name":"core:t\/addcontact","icon":"fa-address-card","standardicon":"\"core:t\/addcontact\""},{"name":"core:t\/add","icon":"fa-plus","standardicon":"\"core:t\/add\""},{"name":"core:t\/angles-down","icon":"fa-angles-down","standardicon":"\"core:t\/angles-down\""},{"name":"core:t\/angles-left","icon":"fa-angles-left","standardicon":"\"core:t\/angles-left\""},{"name":"core:t\/angles-right","icon":"fa-angles-right","standardicon":"\"core:t\/angles-right\""},{"name":"core:t\/angles-up","icon":"fa-angles-up","standardicon":"\"core:t\/angles-up\""},{"name":"core:t\/approve","icon":"fa-thumbs-up","standardicon":"\"core:t\/approve\""},{"name":"core:t\/assignroles","icon":"fa-user-circle","standardicon":"\"core:t\/assignroles\""},{"name":"core:t\/award","icon":"fa-trophy","standardicon":"\"core:t\/award\""},{"name":"core:t\/backpack","icon":"fa-shopping-bag","standardicon":"\"core:t\/backpack\""},{"name":"core:t\/backup","icon":"fa-arrow-circle-down","standardicon":"\"core:t\/backup\""},{"name":"core:t\/block","icon":"fa-ban","standardicon":"\"core:t\/block\""},{"name":"core:t\/block_to_dock_rtl","icon":"fa-chevron-right","standardicon":"\"core:t\/block_to_dock_rtl\""},{"name":"core:t\/block_to_dock","icon":"fa-chevron-left","standardicon":"\"core:t\/block_to_dock\""},{"name":"core:t\/blocks_drawer","icon":"fa-chevron-left","standardicon":"\"core:t\/blocks_drawer\""},{"name":"core:t\/blocks_drawer_rtl","icon":"fa-chevron-right","standardicon":"\"core:t\/blocks_drawer_rtl\""},{"name":"core:t\/calc_off","icon":"fa-calculator","standardicon":"\"core:t\/calc_off\""},{"name":"core:t\/calc","icon":"fa-calculator","standardicon":"\"core:t\/calc\""},{"name":"core:t\/check","icon":"fa-check","standardicon":"\"core:t\/check\""},{"name":"core:t\/clipboard","icon":"fa-clipboard","standardicon":"\"core:t\/clipboard\""},{"name":"core:t\/cohort","icon":"fa-users","standardicon":"\"core:t\/cohort\""},{"name":"core:t\/collapsed_empty_rtl","icon":"fa-caret-square-o-left","standardicon":"\"core:t\/collapsed_empty_rtl\""},{"name":"core:t\/collapsed_empty","icon":"fa-caret-square-o-right","standardicon":"\"core:t\/collapsed_empty\""},{"name":"core:t\/collapsed_rtl","icon":"fa-caret-left","standardicon":"\"core:t\/collapsed_rtl\""},{"name":"core:t\/collapsed","icon":"fa-caret-right","standardicon":"\"core:t\/collapsed\""},{"name":"core:t\/collapsedcaret","icon":"fa-caret-right","standardicon":"\"core:t\/collapsedcaret\""},{"name":"core:t\/collapsedchevron","icon":"fa-chevron-right","standardicon":"\"core:t\/collapsedchevron\""},{"name":"core:t\/collapsedchevron_rtl","icon":"fa-chevron-left","standardicon":"\"core:t\/collapsedchevron_rtl\""},{"name":"core:t\/collapsedchevron_up","icon":"fa-chevron-up","standardicon":"\"core:t\/collapsedchevron_up\""},{"name":"core:t\/completion_complete","icon":"fa-circle","standardicon":"\"core:t\/completion_complete\""},{"name":"core:t\/completion_fail","icon":"fa-times","standardicon":"\"core:t\/completion_fail\""},{"name":"core:t\/completion_incomplete","icon":"fa-circle-thin","standardicon":"\"core:t\/completion_incomplete\""},{"name":"core:t\/contextmenu","icon":"fa-cog","standardicon":"\"core:t\/contextmenu\""},{"name":"core:t\/copy","icon":"fa-copy","standardicon":"\"core:t\/copy\""},{"name":"core:t\/delete","icon":"fa-trash","standardicon":"\"core:t\/delete\""},{"name":"core:t\/dockclose","icon":"fa-window-close","standardicon":"\"core:t\/dockclose\""},{"name":"core:t\/dock_to_block_rtl","icon":"fa-chevron-right","standardicon":"\"core:t\/dock_to_block_rtl\""},{"name":"core:t\/dock_to_block","icon":"fa-chevron-left","standardicon":"\"core:t\/dock_to_block\""},{"name":"core:t\/download","icon":"fa-download","standardicon":"\"core:t\/download\""},{"name":"core:t\/down","icon":"fa-arrow-down","standardicon":"\"core:t\/down\""},{"name":"core:t\/downlong","icon":"fa-long-arrow-down","standardicon":"\"core:t\/downlong\""},{"name":"core:t\/dropdown","icon":"fa-cog","standardicon":"\"core:t\/dropdown\""},{"name":"core:t\/editinline","icon":"fa-pencil","standardicon":"\"core:t\/editinline\""},{"name":"core:t\/edit_menu","icon":"fa-cog","standardicon":"\"core:t\/edit_menu\""},{"name":"core:t\/editstring","icon":"fa-pencil","standardicon":"\"core:t\/editstring\""},{"name":"core:t\/edit","icon":"fa-cog","standardicon":"\"core:t\/edit\""},{"name":"core:t\/emailno","icon":"fa-ban","standardicon":"\"core:t\/emailno\""},{"name":"core:t\/email","icon":"fa-envelope-o","standardicon":"\"core:t\/email\""},{"name":"core:t\/emptystar","icon":"fa-star-o","standardicon":"\"core:t\/emptystar\""},{"name":"core:t\/enrolusers","icon":"fa-user-plus","standardicon":"\"core:t\/enrolusers\""},{"name":"core:t\/expanded","icon":"fa-caret-down","standardicon":"\"core:t\/expanded\""},{"name":"core:t\/expandedchevron","icon":"fa-chevron-down","standardicon":"\"core:t\/expandedchevron\""},{"name":"core:t\/go","icon":"fa-play","standardicon":"\"core:t\/go\""},{"name":"core:t\/grades","icon":"fa-table","standardicon":"\"core:t\/grades\""},{"name":"core:t\/groupn","icon":"fa-user","standardicon":"\"core:t\/groupn\""},{"name":"core:t\/groups","icon":"fa-user-circle","standardicon":"\"core:t\/groups\""},{"name":"core:t\/groupv","icon":"fa-user-circle-o","standardicon":"\"core:t\/groupv\""},{"name":"core:t\/hide","icon":"fa-eye","standardicon":"\"core:t\/hide\""},{"name":"core:t\/index_drawer","icon":"fa-list","standardicon":"\"core:t\/index_drawer\""},{"name":"core:t\/left","icon":"fa-arrow-left","standardicon":"\"core:t\/left\""},{"name":"core:t\/less","icon":"fa-caret-up","standardicon":"\"core:t\/less\""},{"name":"core:t\/life-ring","icon":"fa-life-ring","standardicon":"\"core:t\/life-ring\""},{"name":"core:t\/locked","icon":"fa-lock","standardicon":"\"core:t\/locked\""},{"name":"core:t\/lock","icon":"fa-unlock","standardicon":"\"core:t\/lock\""},{"name":"core:t\/locktime","icon":"fa-lock","standardicon":"\"core:t\/locktime\""},{"name":"core:t\/markasread","icon":"fa-check","standardicon":"\"core:t\/markasread\""},{"name":"core:t\/messages","icon":"fa-comments","standardicon":"\"core:t\/messages\""},{"name":"core:t\/messages-o","icon":"fa-comments-o","standardicon":"\"core:t\/messages-o\""},{"name":"core:t\/message","icon":"fa-comment-o","standardicon":"\"core:t\/message\""},{"name":"core:t\/more","icon":"fa-caret-down","standardicon":"\"core:t\/more\""},{"name":"core:t\/move","icon":"fa-arrows-v","standardicon":"\"core:t\/move\""},{"name":"core:t\/online","icon":"fa-circle","standardicon":"\"core:t\/online\""},{"name":"core:t\/passwordunmask-edit","icon":"fa-pencil","standardicon":"\"core:t\/passwordunmask-edit\""},{"name":"core:t\/passwordunmask-reveal","icon":"fa-eye","standardicon":"\"core:t\/passwordunmask-reveal\""},{"name":"core:t\/play","icon":"fa-play","standardicon":"\"core:t\/play\""},{"name":"core:t\/portfolioadd","icon":"fa-plus","standardicon":"\"core:t\/portfolioadd\""},{"name":"core:t\/preferences","icon":"fa-wrench","standardicon":"\"core:t\/preferences\""},{"name":"core:t\/preview","icon":"fa-search-plus","standardicon":"\"core:t\/preview\""},{"name":"core:t\/print","icon":"fa-print","standardicon":"\"core:t\/print\""},{"name":"core:t\/removecontact","icon":"fa-user-times","standardicon":"\"core:t\/removecontact\""},{"name":"core:t\/reload","icon":"fa-refresh","standardicon":"\"core:t\/reload\""},{"name":"core:t\/reset","icon":"fa-repeat","standardicon":"\"core:t\/reset\""},{"name":"core:t\/restore","icon":"fa-arrow-circle-up","standardicon":"\"core:t\/restore\""},{"name":"core:t\/right","icon":"fa-arrow-right","standardicon":"\"core:t\/right\""},{"name":"core:t\/sendmessage","icon":"fa-paper-plane","standardicon":"\"core:t\/sendmessage\""},{"name":"core:t\/show","icon":"fa-eye-slash","standardicon":"\"core:t\/show\""},{"name":"core:t\/sort_by","icon":"fa-sort-amount-asc","standardicon":"\"core:t\/sort_by\""},{"name":"core:t\/sort_asc","icon":"fa-sort-asc","standardicon":"\"core:t\/sort_asc\""},{"name":"core:t\/sort_desc","icon":"fa-sort-desc","standardicon":"\"core:t\/sort_desc\""},{"name":"core:t\/sort","icon":"fa-sort","standardicon":"\"core:t\/sort\""},{"name":"core:t\/stealth","icon":"fa-low-vision","standardicon":"\"core:t\/stealth\""},{"name":"core:t\/stop","icon":"fa-stop","standardicon":"\"core:t\/stop\""},{"name":"core:t\/switch_minus","icon":"fa-minus","standardicon":"\"core:t\/switch_minus\""},{"name":"core:t\/switch_plus","icon":"fa-plus","standardicon":"\"core:t\/switch_plus\""},{"name":"core:t\/switch_whole","icon":"fa-square-o","standardicon":"\"core:t\/switch_whole\""},{"name":"core:t\/tags","icon":"fa-tags","standardicon":"\"core:t\/tags\""},{"name":"core:t\/unblock","icon":"fa-commenting","standardicon":"\"core:t\/unblock\""},{"name":"core:t\/unlocked","icon":"fa-unlock-alt","standardicon":"\"core:t\/unlocked\""},{"name":"core:t\/unlock","icon":"fa-lock","standardicon":"\"core:t\/unlock\""},{"name":"core:t\/up","icon":"fa-arrow-up","standardicon":"\"core:t\/up\""},{"name":"core:t\/uplong","icon":"fa-long-arrow-up","standardicon":"\"core:t\/uplong\""},{"name":"core:t\/user","icon":"fa-user","standardicon":"\"core:t\/user\""},{"name":"core:t\/viewdetails","icon":"fa-list","standardicon":"\"core:t\/viewdetails\""},{"name":"qtype_ddmarker:crosshairs","icon":"fa-crosshairs","standardicon":"\"qtype_ddmarker:crosshairs\""},{"name":"qtype_ddmarker:grid","icon":"fa-th","standardicon":"\"qtype_ddmarker:grid\""},{"name":"mod_book:chapter","icon":"fa-bookmark-o","standardicon":"\"mod_book:chapter\""},{"name":"mod_book:nav_prev","icon":"fa-arrow-left","standardicon":"\"mod_book:nav_prev\""},{"name":"mod_book:nav_sep","icon":"fa-minus","standardicon":"\"mod_book:nav_sep\""},{"name":"mod_book:add","icon":"fa-plus","standardicon":"\"mod_book:add\""},{"name":"mod_book:nav_next","icon":"fa-arrow-right","standardicon":"\"mod_book:nav_next\""},{"name":"mod_book:nav_exit","icon":"fa-arrow-up","standardicon":"\"mod_book:nav_exit\""},{"name":"mod_choice:row","icon":"fa-info","standardicon":"\"mod_choice:row\""},{"name":"mod_choice:column","icon":"fa-columns","standardicon":"\"mod_choice:column\""},{"name":"mod_data:field\/checkbox","icon":"fa-check-square-o","standardicon":"\"mod_data:field\/checkbox\""},{"name":"mod_data:field\/date","icon":"fa-calendar-o","standardicon":"\"mod_data:field\/date\""},{"name":"mod_data:field\/file","icon":"fa-file","standardicon":"\"mod_data:field\/file\""},{"name":"mod_data:field\/latlong","icon":"fa-globe","standardicon":"\"mod_data:field\/latlong\""},{"name":"mod_data:field\/menu","icon":"fa-bars","standardicon":"\"mod_data:field\/menu\""},{"name":"mod_data:field\/multimenu","icon":"fa-bars","standardicon":"\"mod_data:field\/multimenu\""},{"name":"mod_data:field\/number","icon":"fa-hashtag","standardicon":"\"mod_data:field\/number\""},{"name":"mod_data:field\/picture","icon":"fa-picture-o","standardicon":"\"mod_data:field\/picture\""},{"name":"mod_data:field\/radiobutton","icon":"fa-circle-o","standardicon":"\"mod_data:field\/radiobutton\""},{"name":"mod_data:field\/textarea","icon":"fa-font","standardicon":"\"mod_data:field\/textarea\""},{"name":"mod_data:field\/text","icon":"fa-i-cursor","standardicon":"\"mod_data:field\/text\""},{"name":"mod_data:field\/url","icon":"fa-link","standardicon":"\"mod_data:field\/url\""},{"name":"mod_feedback:required","icon":"fa-exclamation-circle","standardicon":"\"mod_feedback:required\""},{"name":"mod_feedback:notrequired","icon":"fa-question-circle-o","standardicon":"\"mod_feedback:notrequired\""},{"name":"mod_forum:i\/pinned","icon":"fa-map-pin","standardicon":"\"mod_forum:i\/pinned\""},{"name":"mod_forum:t\/selected","icon":"fa-check","standardicon":"\"mod_forum:t\/selected\""},{"name":"mod_forum:t\/subscribed","icon":"fa-envelope-o","standardicon":"\"mod_forum:t\/subscribed\""},{"name":"mod_forum:t\/unsubscribed","icon":"fa-envelope-open-o","standardicon":"\"mod_forum:t\/unsubscribed\""},{"name":"mod_forum:t\/star","icon":"fa-star","standardicon":"\"mod_forum:t\/star\""},{"name":"mod_glossary:export","icon":"fa-download","standardicon":"\"mod_glossary:export\""},{"name":"mod_glossary:minus","icon":"fa-minus","standardicon":"\"mod_glossary:minus\""},{"name":"mod_lesson:e\/copy","icon":"fa-clone","standardicon":"\"mod_lesson:e\/copy\""},{"name":"mod_lti:warning","icon":"fa-exclamation text-warning","standardicon":"\"mod_lti:warning\""},{"name":"mod_quiz:navflagged","icon":"fa-flag","standardicon":"\"mod_quiz:navflagged\""},{"name":"mod_scorm:assetc","icon":"fa-file-archive-o","standardicon":"\"mod_scorm:assetc\""},{"name":"mod_scorm:asset","icon":"fa-file-archive-o","standardicon":"\"mod_scorm:asset\""},{"name":"mod_scorm:browsed","icon":"fa-book","standardicon":"\"mod_scorm:browsed\""},{"name":"mod_scorm:completed","icon":"fa-check-square-o","standardicon":"\"mod_scorm:completed\""},{"name":"mod_scorm:failed","icon":"fa-times","standardicon":"\"mod_scorm:failed\""},{"name":"mod_scorm:incomplete","icon":"fa-pencil-square-o","standardicon":"\"mod_scorm:incomplete\""},{"name":"mod_scorm:minus","icon":"fa-minus","standardicon":"\"mod_scorm:minus\""},{"name":"mod_scorm:notattempted","icon":"fa-square-o","standardicon":"\"mod_scorm:notattempted\""},{"name":"mod_scorm:passed","icon":"fa-check","standardicon":"\"mod_scorm:passed\""},{"name":"mod_scorm:plus","icon":"fa-plus","standardicon":"\"mod_scorm:plus\""},{"name":"mod_scorm:popdown","icon":"fa-window-close-o","standardicon":"\"mod_scorm:popdown\""},{"name":"mod_scorm:popup","icon":"fa-window-restore","standardicon":"\"mod_scorm:popup\""},{"name":"mod_scorm:suspend","icon":"fa-pause","standardicon":"\"mod_scorm:suspend\""},{"name":"mod_scorm:wait","icon":"fa-clock-o","standardicon":"\"mod_scorm:wait\""},{"name":"mod_wiki:attachment","icon":"fa-paperclip","standardicon":"\"mod_wiki:attachment\""},{"name":"mod_workshop:userplan\/task-info","icon":"fa-info text-info","standardicon":"\"mod_workshop:userplan\/task-info\""},{"name":"mod_workshop:userplan\/task-todo","icon":"fa-square-o","standardicon":"\"mod_workshop:userplan\/task-todo\""},{"name":"mod_workshop:userplan\/task-done","icon":"fa-check text-success","standardicon":"\"mod_workshop:userplan\/task-done\""},{"name":"mod_workshop:userplan\/task-fail","icon":"fa-remove text-danger","standardicon":"\"mod_workshop:userplan\/task-fail\""},{"name":"customfield_date:checked","icon":"fa-check-square-o","standardicon":"\"customfield_date:checked\""},{"name":"customfield_date:notchecked","icon":"fa-square-o","standardicon":"\"customfield_date:notchecked\""},{"name":"enrol_guest:withpassword","icon":"fa-key","standardicon":"\"enrol_guest:withpassword\""},{"name":"enrol_guest:withoutpassword","icon":"fa-unlock-alt","standardicon":"\"enrol_guest:withoutpassword\""},{"name":"enrol_lti:managedeployments","icon":"fa-sitemap","standardicon":"\"enrol_lti:managedeployments\""},{"name":"enrol_lti:platformdetails","icon":"fa-pencil-square-o","standardicon":"\"enrol_lti:platformdetails\""},{"name":"enrol_lti:enrolinstancewarning","icon":"fa-exclamation-circle text-danger","standardicon":"\"enrol_lti:enrolinstancewarning\""},{"name":"enrol_self:withkey","icon":"fa-key","standardicon":"\"enrol_self:withkey\""},{"name":"enrol_self:withoutkey","icon":"fa-sign-in","standardicon":"\"enrol_self:withoutkey\""},{"name":"block_accessreview:smile","icon":"fa-smile-o","standardicon":"\"block_accessreview:smile\""},{"name":"block_accessreview:frown","icon":"fa-frown-o","standardicon":"\"block_accessreview:frown\""},{"name":"block_accessreview:errorsfound","icon":"fa-ban","standardicon":"\"block_accessreview:errorsfound\""},{"name":"block_accessreview:f\/pdf","icon":"fa-file-pdf-o","standardicon":"\"block_accessreview:f\/pdf\""},{"name":"block_accessreview:f\/video","icon":"fa-file-video-o","standardicon":"\"block_accessreview:f\/video\""},{"name":"block_accessreview:f\/find","icon":"fa-bar-chart","standardicon":"\"block_accessreview:f\/find\""},{"name":"block_accessreview:f\/form","icon":"fa-pencil-square-o","standardicon":"\"block_accessreview:f\/form\""},{"name":"block_accessreview:f\/image","icon":"fa-image","standardicon":"\"block_accessreview:f\/image\""},{"name":"block_accessreview:f\/layout","icon":"fa-th-large","standardicon":"\"block_accessreview:f\/layout\""},{"name":"block_accessreview:f\/link","icon":"fa-link","standardicon":"\"block_accessreview:f\/link\""},{"name":"block_accessreview:f\/media","icon":"fa-play-circle-o","standardicon":"\"block_accessreview:f\/media\""},{"name":"block_accessreview:f\/table","icon":"fa-table","standardicon":"\"block_accessreview:f\/table\""},{"name":"block_accessreview:f\/text","icon":"fa-font","standardicon":"\"block_accessreview:f\/text\""},{"name":"block_accessreview:t\/fail","icon":"fa-ban","standardicon":"\"block_accessreview:t\/fail\""},{"name":"block_accessreview:t\/pass","icon":"fa-check","standardicon":"\"block_accessreview:t\/pass\""},{"name":"gradingform_guide:info","icon":"fa-info-circle","standardicon":"\"gradingform_guide:info\""},{"name":"gradingform_guide:plus","icon":"fa-plus","standardicon":"\"gradingform_guide:plus\""},{"name":"tool_brickfield:f\/award","icon":"fa-tachometer","standardicon":"\"tool_brickfield:f\/award\""},{"name":"tool_brickfield:f\/done","icon":"fa-check-circle-o","standardicon":"\"tool_brickfield:f\/done\""},{"name":"tool_brickfield:f\/done2","icon":"fa-check-square-o","standardicon":"\"tool_brickfield:f\/done2\""},{"name":"tool_brickfield:f\/error","icon":"fa-times-circle-o","standardicon":"\"tool_brickfield:f\/error\""},{"name":"tool_brickfield:f\/find","icon":"fa-bar-chart","standardicon":"\"tool_brickfield:f\/find\""},{"name":"tool_brickfield:f\/total","icon":"fa-calculator","standardicon":"\"tool_brickfield:f\/total\""},{"name":"tool_brickfield:f\/form","icon":"fa-pencil-square-o","standardicon":"\"tool_brickfield:f\/form\""},{"name":"tool_brickfield:f\/image","icon":"fa-image","standardicon":"\"tool_brickfield:f\/image\""},{"name":"tool_brickfield:f\/layout","icon":"fa-th-large","standardicon":"\"tool_brickfield:f\/layout\""},{"name":"tool_brickfield:f\/link","icon":"fa-link","standardicon":"\"tool_brickfield:f\/link\""},{"name":"tool_brickfield:f\/media","icon":"fa-play-circle-o","standardicon":"\"tool_brickfield:f\/media\""},{"name":"tool_brickfield:f\/table","icon":"fa-table","standardicon":"\"tool_brickfield:f\/table\""},{"name":"tool_brickfield:f\/text","icon":"fa-font","standardicon":"\"tool_brickfield:f\/text\""},{"name":"tool_lp:url","icon":"fa-external-link","standardicon":"\"tool_lp:url\""},{"name":"tool_policy:agreed","icon":"fa-check text-success","standardicon":"\"tool_policy:agreed\""},{"name":"tool_policy:declined","icon":"fa-times text-danger","standardicon":"\"tool_policy:declined\""},{"name":"tool_policy:pending","icon":"fa-clock-o text-warning","standardicon":"\"tool_policy:pending\""},{"name":"tool_policy:partial","icon":"fa-exclamation-triangle text-warning","standardicon":"\"tool_policy:partial\""},{"name":"tool_policy:level","icon":"fa-level-up fa-rotate-90 text-muted","standardicon":"\"tool_policy:level\""},{"name":"tool_recyclebin:trash","icon":"fa-trash","standardicon":"\"tool_recyclebin:trash\""},{"name":"tool_usertours:t\/export","icon":"fa-download","standardicon":"\"tool_usertours:t\/export\""},{"name":"tool_usertours:i\/reload","icon":"fa-refresh","standardicon":"\"tool_usertours:i\/reload\""},{"name":"tool_usertours:t\/filler","icon":"fa-spacer","standardicon":"\"tool_usertours:t\/filler\""},{"name":"atto_collapse:icon","icon":"fa-level-down","standardicon":"\"atto_collapse:icon\""},{"name":"atto_recordrtc:i\/audiortc","icon":"fa-microphone","standardicon":"\"atto_recordrtc:i\/audiortc\""},{"name":"atto_recordrtc:i\/videortc","icon":"fa-video-camera","standardicon":"\"atto_recordrtc:i\/videortc\""}] \ No newline at end of file +[{"name":"core:docs","icon":"fa-info-circle","standardicon":"\"core:docs\""},{"name":"core:book","icon":"fa-book","standardicon":"\"core:book\""},{"name":"core:help","icon":"fa-question-circle text-info","standardicon":"\"core:help\""},{"name":"core:req","icon":"fa-exclamation-circle text-danger","standardicon":"\"core:req\""},{"name":"core:a\/add_file","icon":"fa-file-o","standardicon":"\"core:a\/add_file\""},{"name":"core:a\/create_folder","icon":"fa-folder-o","standardicon":"\"core:a\/create_folder\""},{"name":"core:a\/download_all","icon":"fa-download","standardicon":"\"core:a\/download_all\""},{"name":"core:a\/help","icon":"fa-question-circle text-info","standardicon":"\"core:a\/help\""},{"name":"core:a\/logout","icon":"fa-sign-out","standardicon":"\"core:a\/logout\""},{"name":"core:a\/refresh","icon":"fa-refresh","standardicon":"\"core:a\/refresh\""},{"name":"core:a\/search","icon":"fa-search","standardicon":"\"core:a\/search\""},{"name":"core:a\/setting","icon":"fa-cog","standardicon":"\"core:a\/setting\""},{"name":"core:a\/view_icon_active","icon":"fa-th","standardicon":"\"core:a\/view_icon_active\""},{"name":"core:a\/view_list_active","icon":"fa-list","standardicon":"\"core:a\/view_list_active\""},{"name":"core:a\/view_tree_active","icon":"fa-folder","standardicon":"\"core:a\/view_tree_active\""},{"name":"core:b\/bookmark-new","icon":"fa-bookmark","standardicon":"\"core:b\/bookmark-new\""},{"name":"core:b\/document-edit","icon":"fa-pencil","standardicon":"\"core:b\/document-edit\""},{"name":"core:b\/document-new","icon":"fa-file-o","standardicon":"\"core:b\/document-new\""},{"name":"core:b\/document-properties","icon":"fa-info","standardicon":"\"core:b\/document-properties\""},{"name":"core:b\/edit-copy","icon":"fa-files-o","standardicon":"\"core:b\/edit-copy\""},{"name":"core:b\/edit-delete","icon":"fa-trash","standardicon":"\"core:b\/edit-delete\""},{"name":"core:e\/abbr","icon":"fa-comment","standardicon":"\"core:e\/abbr\""},{"name":"core:e\/absolute","icon":"fa-crosshairs","standardicon":"\"core:e\/absolute\""},{"name":"core:e\/accessibility_checker","icon":"fa-universal-access","standardicon":"\"core:e\/accessibility_checker\""},{"name":"core:e\/acronym","icon":"fa-comment","standardicon":"\"core:e\/acronym\""},{"name":"core:e\/advance_hr","icon":"fa-arrows-h","standardicon":"\"core:e\/advance_hr\""},{"name":"core:e\/align_center","icon":"fa-align-center","standardicon":"\"core:e\/align_center\""},{"name":"core:e\/align_left","icon":"fa-align-left","standardicon":"\"core:e\/align_left\""},{"name":"core:e\/align_right","icon":"fa-align-right","standardicon":"\"core:e\/align_right\""},{"name":"core:e\/anchor","icon":"fa-chain","standardicon":"\"core:e\/anchor\""},{"name":"core:e\/backward","icon":"fa-undo","standardicon":"\"core:e\/backward\""},{"name":"core:e\/bold","icon":"fa-bold","standardicon":"\"core:e\/bold\""},{"name":"core:e\/bullet_list","icon":"fa-list-ul","standardicon":"\"core:e\/bullet_list\""},{"name":"core:e\/cancel","icon":"fa-times","standardicon":"\"core:e\/cancel\""},{"name":"core:e\/cancel_solid_circle","icon":"fas fa-times-circle","standardicon":"\"core:e\/cancel_solid_circle\""},{"name":"core:e\/cell_props","icon":"fa-info","standardicon":"\"core:e\/cell_props\""},{"name":"core:e\/cite","icon":"fa-quote-right","standardicon":"\"core:e\/cite\""},{"name":"core:e\/cleanup_messy_code","icon":"fa-eraser","standardicon":"\"core:e\/cleanup_messy_code\""},{"name":"core:e\/clear_formatting","icon":"fa-i-cursor","standardicon":"\"core:e\/clear_formatting\""},{"name":"core:e\/copy","icon":"fa-clone","standardicon":"\"core:e\/copy\""},{"name":"core:e\/cut","icon":"fa-scissors","standardicon":"\"core:e\/cut\""},{"name":"core:e\/decrease_indent","icon":"fa-outdent","standardicon":"\"core:e\/decrease_indent\""},{"name":"core:e\/delete_col","icon":"fa-minus","standardicon":"\"core:e\/delete_col\""},{"name":"core:e\/delete_row","icon":"fa-minus","standardicon":"\"core:e\/delete_row\""},{"name":"core:e\/delete","icon":"fa-minus","standardicon":"\"core:e\/delete\""},{"name":"core:e\/delete_table","icon":"fa-minus","standardicon":"\"core:e\/delete_table\""},{"name":"core:e\/document_properties","icon":"fa-info","standardicon":"\"core:e\/document_properties\""},{"name":"core:e\/emoticons","icon":"fa-smile-o","standardicon":"\"core:e\/emoticons\""},{"name":"core:e\/find_replace","icon":"fa-search-plus","standardicon":"\"core:e\/find_replace\""},{"name":"core:e\/file-text","icon":"fa-file-text","standardicon":"\"core:e\/file-text\""},{"name":"core:e\/forward","icon":"fa-arrow-right","standardicon":"\"core:e\/forward\""},{"name":"core:e\/fullpage","icon":"fa-arrows-alt","standardicon":"\"core:e\/fullpage\""},{"name":"core:e\/fullscreen","icon":"fa-arrows-alt","standardicon":"\"core:e\/fullscreen\""},{"name":"core:e\/help","icon":"fa-question-circle","standardicon":"\"core:e\/help\""},{"name":"core:e\/increase_indent","icon":"fa-indent","standardicon":"\"core:e\/increase_indent\""},{"name":"core:e\/insert_col_after","icon":"fa-columns","standardicon":"\"core:e\/insert_col_after\""},{"name":"core:e\/insert_col_before","icon":"fa-columns","standardicon":"\"core:e\/insert_col_before\""},{"name":"core:e\/insert_date","icon":"fa-calendar","standardicon":"\"core:e\/insert_date\""},{"name":"core:e\/insert_edit_image","icon":"fa-picture-o","standardicon":"\"core:e\/insert_edit_image\""},{"name":"core:e\/insert_edit_link","icon":"fa-link","standardicon":"\"core:e\/insert_edit_link\""},{"name":"core:e\/insert_edit_video","icon":"fa-file-video-o","standardicon":"\"core:e\/insert_edit_video\""},{"name":"core:e\/insert_file","icon":"fa-file","standardicon":"\"core:e\/insert_file\""},{"name":"core:e\/insert_horizontal_ruler","icon":"fa-arrows-h","standardicon":"\"core:e\/insert_horizontal_ruler\""},{"name":"core:e\/insert_nonbreaking_space","icon":"fa-square-o","standardicon":"\"core:e\/insert_nonbreaking_space\""},{"name":"core:e\/insert_page_break","icon":"fa-level-down","standardicon":"\"core:e\/insert_page_break\""},{"name":"core:e\/insert_row_after","icon":"fa-plus","standardicon":"\"core:e\/insert_row_after\""},{"name":"core:e\/insert_row_before","icon":"fa-plus","standardicon":"\"core:e\/insert_row_before\""},{"name":"core:e\/insert","icon":"fa-plus","standardicon":"\"core:e\/insert\""},{"name":"core:e\/insert_time","icon":"fa-clock-o","standardicon":"\"core:e\/insert_time\""},{"name":"core:e\/italic","icon":"fa-italic","standardicon":"\"core:e\/italic\""},{"name":"core:e\/justify","icon":"fa-align-justify","standardicon":"\"core:e\/justify\""},{"name":"core:e\/layers_over","icon":"fa-level-up","standardicon":"\"core:e\/layers_over\""},{"name":"core:e\/layers","icon":"fa-window-restore","standardicon":"\"core:e\/layers\""},{"name":"core:e\/layers_under","icon":"fa-level-down","standardicon":"\"core:e\/layers_under\""},{"name":"core:e\/left_to_right","icon":"fa-chevron-right","standardicon":"\"core:e\/left_to_right\""},{"name":"core:e\/manage_files","icon":"fa-files-o","standardicon":"\"core:e\/manage_files\""},{"name":"core:e\/math","icon":"fa-calculator","standardicon":"\"core:e\/math\""},{"name":"core:e\/merge_cells","icon":"fa-compress","standardicon":"\"core:e\/merge_cells\""},{"name":"core:e\/new_document","icon":"fa-file-o","standardicon":"\"core:e\/new_document\""},{"name":"core:e\/numbered_list","icon":"fa-list-ol","standardicon":"\"core:e\/numbered_list\""},{"name":"core:e\/page_break","icon":"fa-level-down","standardicon":"\"core:e\/page_break\""},{"name":"core:e\/paste","icon":"fa-clipboard","standardicon":"\"core:e\/paste\""},{"name":"core:e\/paste_text","icon":"fa-clipboard","standardicon":"\"core:e\/paste_text\""},{"name":"core:e\/paste_word","icon":"fa-clipboard","standardicon":"\"core:e\/paste_word\""},{"name":"core:e\/prevent_autolink","icon":"fa-exclamation","standardicon":"\"core:e\/prevent_autolink\""},{"name":"core:e\/preview","icon":"fa-search-plus","standardicon":"\"core:e\/preview\""},{"name":"core:e\/print","icon":"fa-print","standardicon":"\"core:e\/print\""},{"name":"core:e\/question","icon":"fa-question","standardicon":"\"core:e\/question\""},{"name":"core:e\/redo","icon":"fa-repeat","standardicon":"\"core:e\/redo\""},{"name":"core:e\/remove_link","icon":"fa-chain-broken","standardicon":"\"core:e\/remove_link\""},{"name":"core:e\/remove_page_break","icon":"fa-remove","standardicon":"\"core:e\/remove_page_break\""},{"name":"core:e\/resize","icon":"fa-expand","standardicon":"\"core:e\/resize\""},{"name":"core:e\/restore_draft","icon":"fa-undo","standardicon":"\"core:e\/restore_draft\""},{"name":"core:e\/restore_last_draft","icon":"fa-undo","standardicon":"\"core:e\/restore_last_draft\""},{"name":"core:e\/right_to_left","icon":"fa-chevron-left","standardicon":"\"core:e\/right_to_left\""},{"name":"core:e\/row_props","icon":"fa-info","standardicon":"\"core:e\/row_props\""},{"name":"core:e\/save","icon":"fa-floppy-o","standardicon":"\"core:e\/save\""},{"name":"core:e\/screenreader_helper","icon":"fa-braille","standardicon":"\"core:e\/screenreader_helper\""},{"name":"core:e\/search","icon":"fa-search","standardicon":"\"core:e\/search\""},{"name":"core:e\/select_all","icon":"fa-arrows-h","standardicon":"\"core:e\/select_all\""},{"name":"core:e\/show_invisible_characters","icon":"fa-eye-slash","standardicon":"\"core:e\/show_invisible_characters\""},{"name":"core:e\/source_code","icon":"fa-code","standardicon":"\"core:e\/source_code\""},{"name":"core:e\/special_character","icon":"fa-pencil-square-o","standardicon":"\"core:e\/special_character\""},{"name":"core:e\/spellcheck","icon":"fa-check","standardicon":"\"core:e\/spellcheck\""},{"name":"core:e\/split_cells","icon":"fa-columns","standardicon":"\"core:e\/split_cells\""},{"name":"core:e\/strikethrough","icon":"fa-strikethrough","standardicon":"\"core:e\/strikethrough\""},{"name":"core:e\/styleparagraph","icon":"fa-font","standardicon":"\"core:e\/styleparagraph\""},{"name":"core:e\/subscript","icon":"fa-subscript","standardicon":"\"core:e\/subscript\""},{"name":"core:e\/superscript","icon":"fa-superscript","standardicon":"\"core:e\/superscript\""},{"name":"core:e\/table_props","icon":"fa-table","standardicon":"\"core:e\/table_props\""},{"name":"core:e\/table","icon":"fa-table","standardicon":"\"core:e\/table\""},{"name":"core:e\/template","icon":"fa-sticky-note","standardicon":"\"core:e\/template\""},{"name":"core:e\/text_color_picker","icon":"fa-paint-brush","standardicon":"\"core:e\/text_color_picker\""},{"name":"core:e\/text_color","icon":"fa-paint-brush","standardicon":"\"core:e\/text_color\""},{"name":"core:e\/text_highlight_picker","icon":"fa-lightbulb-o","standardicon":"\"core:e\/text_highlight_picker\""},{"name":"core:e\/text_highlight","icon":"fa-lightbulb-o","standardicon":"\"core:e\/text_highlight\""},{"name":"core:e\/tick","icon":"fa-check","standardicon":"\"core:e\/tick\""},{"name":"core:e\/toggle_blockquote","icon":"fa-quote-left","standardicon":"\"core:e\/toggle_blockquote\""},{"name":"core:e\/underline","icon":"fa-underline","standardicon":"\"core:e\/underline\""},{"name":"core:e\/undo","icon":"fa-undo","standardicon":"\"core:e\/undo\""},{"name":"core:e\/visual_aid","icon":"fa-universal-access","standardicon":"\"core:e\/visual_aid\""},{"name":"core:e\/visual_blocks","icon":"fa-audio-description","standardicon":"\"core:e\/visual_blocks\""},{"name":"core:i\/activities","icon":"fa-file-pen","standardicon":"\"core:i\/activities\""},{"name":"core:i\/addblock","icon":"fa-plus-square","standardicon":"\"core:i\/addblock\""},{"name":"core:i\/assignroles","icon":"fa-user-plus","standardicon":"\"core:i\/assignroles\""},{"name":"core:i\/asterisk","icon":"fa-asterisk","standardicon":"\"core:i\/asterisk\""},{"name":"core:i\/backup","icon":"fa-file-zip-o","standardicon":"\"core:i\/backup\""},{"name":"core:i\/badge","icon":"fa-shield","standardicon":"\"core:i\/badge\""},{"name":"core:i\/breadcrumbdivider","icon":"fa-angle-right","standardicon":"\"core:i\/breadcrumbdivider\""},{"name":"core:i\/bullhorn","icon":"fa-bullhorn","standardicon":"\"core:i\/bullhorn\""},{"name":"core:i\/calc","icon":"fa-calculator","standardicon":"\"core:i\/calc\""},{"name":"core:i\/calendar","icon":"fa-calendar","standardicon":"\"core:i\/calendar\""},{"name":"core:i\/calendareventdescription","icon":"fa-align-left","standardicon":"\"core:i\/calendareventdescription\""},{"name":"core:i\/calendareventtime","icon":"fa-clock-o","standardicon":"\"core:i\/calendareventtime\""},{"name":"core:i\/caution","icon":"fa-exclamation text-warning","standardicon":"\"core:i\/caution\""},{"name":"core:i\/checked","icon":"fa-check","standardicon":"\"core:i\/checked\""},{"name":"core:i\/checkedcircle","icon":"fa-check-circle","standardicon":"\"core:i\/checkedcircle\""},{"name":"core:i\/checkpermissions","icon":"fa-unlock-alt","standardicon":"\"core:i\/checkpermissions\""},{"name":"core:i\/cohort","icon":"fa-users","standardicon":"\"core:i\/cohort\""},{"name":"core:i\/competencies","icon":"fa-check-square-o","standardicon":"\"core:i\/competencies\""},{"name":"core:i\/completion_self","icon":"fa-user-o","standardicon":"\"core:i\/completion_self\""},{"name":"core:i\/contentbank","icon":"fa-paint-brush","standardicon":"\"core:i\/contentbank\""},{"name":"core:i\/dashboard","icon":"fa-tachometer","standardicon":"\"core:i\/dashboard\""},{"name":"core:i\/categoryevent","icon":"fa-cubes","standardicon":"\"core:i\/categoryevent\""},{"name":"core:i\/chartbar","icon":"fa-chart-bar","standardicon":"\"core:i\/chartbar\""},{"name":"core:i\/course","icon":"fa-graduation-cap","standardicon":"\"core:i\/course\""},{"name":"core:i\/courseevent","icon":"fa-graduation-cap","standardicon":"\"core:i\/courseevent\""},{"name":"core:i\/cloudupload","icon":"fa-cloud-upload","standardicon":"\"core:i\/cloudupload\""},{"name":"core:i\/customfield","icon":"fa-hand-o-right","standardicon":"\"core:i\/customfield\""},{"name":"core:i\/db","icon":"fa-database","standardicon":"\"core:i\/db\""},{"name":"core:i\/delete","icon":"fa-trash","standardicon":"\"core:i\/delete\""},{"name":"core:i\/down","icon":"fa-arrow-down","standardicon":"\"core:i\/down\""},{"name":"core:i\/dragdrop","icon":"fa-arrows","standardicon":"\"core:i\/dragdrop\""},{"name":"core:i\/duration","icon":"fa-clock-o","standardicon":"\"core:i\/duration\""},{"name":"core:i\/emojicategoryactivities","icon":"fa-futbol-o","standardicon":"\"core:i\/emojicategoryactivities\""},{"name":"core:i\/emojicategoryanimalsnature","icon":"fa-leaf","standardicon":"\"core:i\/emojicategoryanimalsnature\""},{"name":"core:i\/emojicategoryflags","icon":"fa-flag","standardicon":"\"core:i\/emojicategoryflags\""},{"name":"core:i\/emojicategoryfooddrink","icon":"fa-cutlery","standardicon":"\"core:i\/emojicategoryfooddrink\""},{"name":"core:i\/emojicategoryobjects","icon":"fa-lightbulb-o","standardicon":"\"core:i\/emojicategoryobjects\""},{"name":"core:i\/emojicategorypeoplebody","icon":"fa-male","standardicon":"\"core:i\/emojicategorypeoplebody\""},{"name":"core:i\/emojicategoryrecent","icon":"fa-clock-o","standardicon":"\"core:i\/emojicategoryrecent\""},{"name":"core:i\/emojicategorysmileysemotion","icon":"fa-smile-o","standardicon":"\"core:i\/emojicategorysmileysemotion\""},{"name":"core:i\/emojicategorysymbols","icon":"fa-heart","standardicon":"\"core:i\/emojicategorysymbols\""},{"name":"core:i\/emojicategorytravelplaces","icon":"fa-plane","standardicon":"\"core:i\/emojicategorytravelplaces\""},{"name":"core:i\/edit","icon":"fa-pencil","standardicon":"\"core:i\/edit\""},{"name":"core:i\/email","icon":"fa-envelope","standardicon":"\"core:i\/email\""},{"name":"core:i\/empty","icon":"fa-fw","standardicon":"\"core:i\/empty\""},{"name":"core:i\/enrolmentsuspended","icon":"fa-pause","standardicon":"\"core:i\/enrolmentsuspended\""},{"name":"core:i\/enrolusers","icon":"fa-user-plus","standardicon":"\"core:i\/enrolusers\""},{"name":"core:i\/excluded","icon":"fa-minus-circle","standardicon":"\"core:i\/excluded\""},{"name":"core:i\/expired","icon":"fa-exclamation text-warning","standardicon":"\"core:i\/expired\""},{"name":"core:i\/export","icon":"fa-download","standardicon":"\"core:i\/export\""},{"name":"core:i\/link","icon":"fa-link","standardicon":"\"core:i\/link\""},{"name":"core:i\/externallink","icon":"fa-external-link","standardicon":"\"core:i\/externallink\""},{"name":"core:i\/files","icon":"fa-file","standardicon":"\"core:i\/files\""},{"name":"core:i\/file_plus","icon":"fa-file-circle-plus","standardicon":"\"core:i\/file_plus\""},{"name":"core:i\/file_export","icon":"fa-file-export","standardicon":"\"core:i\/file_export\""},{"name":"core:i\/file_import","icon":"fa-file-import","standardicon":"\"core:i\/file_import\""},{"name":"core:i\/filter","icon":"fa-filter","standardicon":"\"core:i\/filter\""},{"name":"core:i\/flagged","icon":"fa-flag","standardicon":"\"core:i\/flagged\""},{"name":"core:i\/folder","icon":"fa-folder","standardicon":"\"core:i\/folder\""},{"name":"core:i\/grade_correct","icon":"fa-check text-success","standardicon":"\"core:i\/grade_correct\""},{"name":"core:i\/grade_incorrect","icon":"fa-remove text-danger","standardicon":"\"core:i\/grade_incorrect\""},{"name":"core:i\/grade_partiallycorrect","icon":"fa-check-square","standardicon":"\"core:i\/grade_partiallycorrect\""},{"name":"core:i\/grades","icon":"fa-table","standardicon":"\"core:i\/grades\""},{"name":"core:i\/grading","icon":"fa-magic","standardicon":"\"core:i\/grading\""},{"name":"core:i\/gradingnotifications","icon":"fa-bell-o","standardicon":"\"core:i\/gradingnotifications\""},{"name":"core:i\/groupevent","icon":"fa-group","standardicon":"\"core:i\/groupevent\""},{"name":"core:i\/group","icon":"fa-users","standardicon":"\"core:i\/group\""},{"name":"core:i\/home","icon":"fa-home","standardicon":"\"core:i\/home\""},{"name":"core:i\/hide","icon":"fa-eye","standardicon":"\"core:i\/hide\""},{"name":"core:i\/hierarchylock","icon":"fa-lock","standardicon":"\"core:i\/hierarchylock\""},{"name":"core:i\/import","icon":"fa-level-up","standardicon":"\"core:i\/import\""},{"name":"core:i\/incorrect","icon":"fa-exclamation","standardicon":"\"core:i\/incorrect\""},{"name":"core:i\/info","icon":"fa-info","standardicon":"\"core:i\/info\""},{"name":"core:i\/invalid","icon":"fa-times text-danger","standardicon":"\"core:i\/invalid\""},{"name":"core:i\/item","icon":"fa-circle","standardicon":"\"core:i\/item\""},{"name":"core:i\/loading","icon":"fa-circle-o-notch fa-spin fa-sm","standardicon":"\"core:i\/loading\""},{"name":"core:i\/loading_small","icon":"fa-circle-o-notch fa-spin fa-sm","standardicon":"\"core:i\/loading_small\""},{"name":"core:i\/location","icon":"fa-map-marker","standardicon":"\"core:i\/location\""},{"name":"core:i\/lock","icon":"fa-lock","standardicon":"\"core:i\/lock\""},{"name":"core:i\/log","icon":"fa-list-alt","standardicon":"\"core:i\/log\""},{"name":"core:i\/mahara_host","icon":"fa-id-badge","standardicon":"\"core:i\/mahara_host\""},{"name":"core:i\/manual_item","icon":"fa-pencil-square-o","standardicon":"\"core:i\/manual_item\""},{"name":"core:i\/marked","icon":"fa-circle","standardicon":"\"core:i\/marked\""},{"name":"core:i\/marker","icon":"fa-circle-o","standardicon":"\"core:i\/marker\""},{"name":"core:i\/mean","icon":"fa-calculator","standardicon":"\"core:i\/mean\""},{"name":"core:i\/menu","icon":"fa-ellipsis-v","standardicon":"\"core:i\/menu\""},{"name":"core:i\/menubars","icon":"fa-bars","standardicon":"\"core:i\/menubars\""},{"name":"core:i\/messagecontentaudio","icon":"fa-headphones","standardicon":"\"core:i\/messagecontentaudio\""},{"name":"core:i\/messagecontentimage","icon":"fa-image","standardicon":"\"core:i\/messagecontentimage\""},{"name":"core:i\/messagecontentvideo","icon":"fa-film","standardicon":"\"core:i\/messagecontentvideo\""},{"name":"core:i\/messagecontentmultimediageneral","icon":"fa-file-video-o","standardicon":"\"core:i\/messagecontentmultimediageneral\""},{"name":"core:i\/mnethost","icon":"fa-external-link","standardicon":"\"core:i\/mnethost\""},{"name":"core:i\/moodle_host","icon":"fa-graduation-cap","standardicon":"\"core:i\/moodle_host\""},{"name":"core:i\/moremenu","icon":"fa-ellipsis-h","standardicon":"\"core:i\/moremenu\""},{"name":"core:i\/move_2d","icon":"fa-arrows","standardicon":"\"core:i\/move_2d\""},{"name":"core:i\/muted","icon":"fa-microphone-slash","standardicon":"\"core:i\/muted\""},{"name":"core:i\/navigationitem","icon":"fa-fw","standardicon":"\"core:i\/navigationitem\""},{"name":"core:i\/ne_red_mark","icon":"fa-remove","standardicon":"\"core:i\/ne_red_mark\""},{"name":"core:i\/new","icon":"fa-bolt","standardicon":"\"core:i\/new\""},{"name":"core:i\/news","icon":"fa-newspaper-o","standardicon":"\"core:i\/news\""},{"name":"core:i\/next","icon":"fa-chevron-right","standardicon":"\"core:i\/next\""},{"name":"core:i\/nosubcat","icon":"fa-plus-square-o","standardicon":"\"core:i\/nosubcat\""},{"name":"core:i\/notifications","icon":"fa-bell-o","standardicon":"\"core:i\/notifications\""},{"name":"core:i\/open","icon":"fa-folder-open","standardicon":"\"core:i\/open\""},{"name":"core:i\/otherevent","icon":"fa-calendar","standardicon":"\"core:i\/otherevent\""},{"name":"core:i\/outcomes","icon":"fa-tasks","standardicon":"\"core:i\/outcomes\""},{"name":"core:i\/overriden_grade","icon":"fa-edit","standardicon":"\"core:i\/overriden_grade\""},{"name":"core:i\/payment","icon":"fa-money","standardicon":"\"core:i\/payment\""},{"name":"core:i\/permissionlock","icon":"fa-lock","standardicon":"\"core:i\/permissionlock\""},{"name":"core:i\/permissions","icon":"fa-pencil-square-o","standardicon":"\"core:i\/permissions\""},{"name":"core:i\/persona_sign_in_black","icon":"fa-male","standardicon":"\"core:i\/persona_sign_in_black\""},{"name":"core:i\/portfolio","icon":"fa-id-badge","standardicon":"\"core:i\/portfolio\""},{"name":"core:i\/preview","icon":"fa-search-plus","standardicon":"\"core:i\/preview\""},{"name":"core:i\/previous","icon":"fa-chevron-left","standardicon":"\"core:i\/previous\""},{"name":"core:i\/privatefiles","icon":"fa-file-o","standardicon":"\"core:i\/privatefiles\""},{"name":"core:i\/progressbar","icon":"fa-spinner fa-spin","standardicon":"\"core:i\/progressbar\""},{"name":"core:i\/publish","icon":"fa-share","standardicon":"\"core:i\/publish\""},{"name":"core:i\/questions","icon":"fa-question","standardicon":"\"core:i\/questions\""},{"name":"core:i\/reload","icon":"fa-refresh","standardicon":"\"core:i\/reload\""},{"name":"core:i\/report","icon":"fa-area-chart","standardicon":"\"core:i\/report\""},{"name":"core:i\/repository","icon":"fa-hdd-o","standardicon":"\"core:i\/repository\""},{"name":"core:i\/restore","icon":"fa-level-up","standardicon":"\"core:i\/restore\""},{"name":"core:i\/return","icon":"fa-arrow-left","standardicon":"\"core:i\/return\""},{"name":"core:i\/risk_config","icon":"fa-exclamation text-muted","standardicon":"\"core:i\/risk_config\""},{"name":"core:i\/risk_managetrust","icon":"fa-exclamation-triangle text-warning","standardicon":"\"core:i\/risk_managetrust\""},{"name":"core:i\/risk_personal","icon":"fa-exclamation-circle text-info","standardicon":"\"core:i\/risk_personal\""},{"name":"core:i\/risk_spam","icon":"fa-exclamation text-primary","standardicon":"\"core:i\/risk_spam\""},{"name":"core:i\/risk_xss","icon":"fa-exclamation-triangle text-danger","standardicon":"\"core:i\/risk_xss\""},{"name":"core:i\/role","icon":"fa-user-md","standardicon":"\"core:i\/role\""},{"name":"core:i\/rss","icon":"fa-rss","standardicon":"\"core:i\/rss\""},{"name":"core:i\/rsssitelogo","icon":"fa-graduation-cap","standardicon":"\"core:i\/rsssitelogo\""},{"name":"core:i\/scales","icon":"fa-balance-scale","standardicon":"\"core:i\/scales\""},{"name":"core:i\/scheduled","icon":"fa-calendar-check-o","standardicon":"\"core:i\/scheduled\""},{"name":"core:i\/search","icon":"fa-search","standardicon":"\"core:i\/search\""},{"name":"core:i\/section","icon":"fa-folder-o","standardicon":"\"core:i\/section\""},{"name":"core:i\/sendmessage","icon":"fa-paper-plane","standardicon":"\"core:i\/sendmessage\""},{"name":"core:i\/settings","icon":"fa-cog","standardicon":"\"core:i\/settings\""},{"name":"core:i\/share","icon":"fa-share-square-o","standardicon":"\"core:i\/share\""},{"name":"core:i\/show","icon":"fa-eye-slash","standardicon":"\"core:i\/show\""},{"name":"core:i\/siteevent","icon":"fa-globe","standardicon":"\"core:i\/siteevent\""},{"name":"core:i\/star","icon":"fa-star","standardicon":"\"core:i\/star\""},{"name":"core:i\/star-o","icon":"fa-star-o","standardicon":"\"core:i\/star-o\""},{"name":"core:i\/star-rating","icon":"fa-star","standardicon":"\"core:i\/star-rating\""},{"name":"core:i\/stats","icon":"fa-line-chart","standardicon":"\"core:i\/stats\""},{"name":"core:i\/switch","icon":"fa-exchange","standardicon":"\"core:i\/switch\""},{"name":"core:i\/switchrole","icon":"fa-user-secret","standardicon":"\"core:i\/switchrole\""},{"name":"core:i\/trash","icon":"fa-trash","standardicon":"\"core:i\/trash\""},{"name":"core:i\/twoway","icon":"fa-arrows-h","standardicon":"\"core:i\/twoway\""},{"name":"core:i\/unchecked","icon":"fa-square-o","standardicon":"\"core:i\/unchecked\""},{"name":"core:i\/uncheckedcircle","icon":"fa-circle-o","standardicon":"\"core:i\/uncheckedcircle\""},{"name":"core:i\/unflagged","icon":"fa-flag-o","standardicon":"\"core:i\/unflagged\""},{"name":"core:i\/unlock","icon":"fa-unlock","standardicon":"\"core:i\/unlock\""},{"name":"core:i\/up","icon":"fa-arrow-up","standardicon":"\"core:i\/up\""},{"name":"core:i\/upload","icon":"fa-upload","standardicon":"\"core:i\/upload\""},{"name":"core:i\/userevent","icon":"fa-user","standardicon":"\"core:i\/userevent\""},{"name":"core:i\/user","icon":"fa-user","standardicon":"\"core:i\/user\""},{"name":"core:i\/users","icon":"fa-users","standardicon":"\"core:i\/users\""},{"name":"core:i\/valid","icon":"fa-check text-success","standardicon":"\"core:i\/valid\""},{"name":"core:i\/viewsection","icon":"fa-pager","standardicon":"\"core:i\/viewsection\""},{"name":"core:i\/warning","icon":"fa-exclamation text-warning","standardicon":"\"core:i\/warning\""},{"name":"core:i\/window_close","icon":"fa-window-close","standardicon":"\"core:i\/window_close\""},{"name":"core:i\/withsubcat","icon":"fa-plus-square","standardicon":"\"core:i\/withsubcat\""},{"name":"core:i\/language","icon":"fa-language","standardicon":"\"core:i\/language\""},{"name":"core:m\/USD","icon":"fa-usd","standardicon":"\"core:m\/USD\""},{"name":"core:t\/addcontact","icon":"fa-address-card","standardicon":"\"core:t\/addcontact\""},{"name":"core:t\/add","icon":"fa-plus","standardicon":"\"core:t\/add\""},{"name":"core:t\/angles-down","icon":"fa-angles-down","standardicon":"\"core:t\/angles-down\""},{"name":"core:t\/angles-left","icon":"fa-angles-left","standardicon":"\"core:t\/angles-left\""},{"name":"core:t\/angles-right","icon":"fa-angles-right","standardicon":"\"core:t\/angles-right\""},{"name":"core:t\/angles-up","icon":"fa-angles-up","standardicon":"\"core:t\/angles-up\""},{"name":"core:t\/approve","icon":"fa-thumbs-up","standardicon":"\"core:t\/approve\""},{"name":"core:t\/assignroles","icon":"fa-user-circle","standardicon":"\"core:t\/assignroles\""},{"name":"core:t\/award","icon":"fa-trophy","standardicon":"\"core:t\/award\""},{"name":"core:t\/backpack","icon":"fa-shopping-bag","standardicon":"\"core:t\/backpack\""},{"name":"core:t\/backup","icon":"fa-arrow-circle-down","standardicon":"\"core:t\/backup\""},{"name":"core:t\/block","icon":"fa-ban","standardicon":"\"core:t\/block\""},{"name":"core:t\/block_to_dock_rtl","icon":"fa-chevron-right","standardicon":"\"core:t\/block_to_dock_rtl\""},{"name":"core:t\/block_to_dock","icon":"fa-chevron-left","standardicon":"\"core:t\/block_to_dock\""},{"name":"core:t\/blocks_drawer","icon":"fa-chevron-left","standardicon":"\"core:t\/blocks_drawer\""},{"name":"core:t\/blocks_drawer_rtl","icon":"fa-chevron-right","standardicon":"\"core:t\/blocks_drawer_rtl\""},{"name":"core:t\/calc_off","icon":"fa-calculator","standardicon":"\"core:t\/calc_off\""},{"name":"core:t\/calc","icon":"fa-calculator","standardicon":"\"core:t\/calc\""},{"name":"core:t\/check","icon":"fa-check","standardicon":"\"core:t\/check\""},{"name":"core:t\/clipboard","icon":"fa-clipboard","standardicon":"\"core:t\/clipboard\""},{"name":"core:t\/cohort","icon":"fa-users","standardicon":"\"core:t\/cohort\""},{"name":"core:t\/collapsed_empty_rtl","icon":"fa-caret-square-o-left","standardicon":"\"core:t\/collapsed_empty_rtl\""},{"name":"core:t\/collapsed_empty","icon":"fa-caret-square-o-right","standardicon":"\"core:t\/collapsed_empty\""},{"name":"core:t\/collapsed_rtl","icon":"fa-caret-left","standardicon":"\"core:t\/collapsed_rtl\""},{"name":"core:t\/collapsed","icon":"fa-caret-right","standardicon":"\"core:t\/collapsed\""},{"name":"core:t\/collapsedcaret","icon":"fa-caret-right","standardicon":"\"core:t\/collapsedcaret\""},{"name":"core:t\/collapsedchevron","icon":"fa-chevron-right","standardicon":"\"core:t\/collapsedchevron\""},{"name":"core:t\/collapsedchevron_rtl","icon":"fa-chevron-left","standardicon":"\"core:t\/collapsedchevron_rtl\""},{"name":"core:t\/collapsedchevron_up","icon":"fa-chevron-up","standardicon":"\"core:t\/collapsedchevron_up\""},{"name":"core:t\/completion_complete","icon":"fa-circle","standardicon":"\"core:t\/completion_complete\""},{"name":"core:t\/completion_fail","icon":"fa-times","standardicon":"\"core:t\/completion_fail\""},{"name":"core:t\/completion_incomplete","icon":"fa-circle-thin","standardicon":"\"core:t\/completion_incomplete\""},{"name":"core:t\/contextmenu","icon":"fa-cog","standardicon":"\"core:t\/contextmenu\""},{"name":"core:t\/copy","icon":"fa-copy","standardicon":"\"core:t\/copy\""},{"name":"core:t\/delete","icon":"fa-trash","standardicon":"\"core:t\/delete\""},{"name":"core:t\/dockclose","icon":"fa-window-close","standardicon":"\"core:t\/dockclose\""},{"name":"core:t\/dock_to_block_rtl","icon":"fa-chevron-right","standardicon":"\"core:t\/dock_to_block_rtl\""},{"name":"core:t\/dock_to_block","icon":"fa-chevron-left","standardicon":"\"core:t\/dock_to_block\""},{"name":"core:t\/download","icon":"fa-download","standardicon":"\"core:t\/download\""},{"name":"core:t\/down","icon":"fa-arrow-down","standardicon":"\"core:t\/down\""},{"name":"core:t\/downlong","icon":"fa-long-arrow-down","standardicon":"\"core:t\/downlong\""},{"name":"core:t\/dropdown","icon":"fa-cog","standardicon":"\"core:t\/dropdown\""},{"name":"core:t\/editinline","icon":"fa-pencil","standardicon":"\"core:t\/editinline\""},{"name":"core:t\/edit_menu","icon":"fa-cog","standardicon":"\"core:t\/edit_menu\""},{"name":"core:t\/editstring","icon":"fa-pencil","standardicon":"\"core:t\/editstring\""},{"name":"core:t\/edit","icon":"fa-cog","standardicon":"\"core:t\/edit\""},{"name":"core:t\/emailno","icon":"fa-ban","standardicon":"\"core:t\/emailno\""},{"name":"core:t\/email","icon":"fa-envelope-o","standardicon":"\"core:t\/email\""},{"name":"core:t\/emptystar","icon":"fa-star-o","standardicon":"\"core:t\/emptystar\""},{"name":"core:t\/enrolusers","icon":"fa-user-plus","standardicon":"\"core:t\/enrolusers\""},{"name":"core:t\/expanded","icon":"fa-caret-down","standardicon":"\"core:t\/expanded\""},{"name":"core:t\/expandedchevron","icon":"fa-chevron-down","standardicon":"\"core:t\/expandedchevron\""},{"name":"core:t\/go","icon":"fa-play","standardicon":"\"core:t\/go\""},{"name":"core:t\/grades","icon":"fa-table","standardicon":"\"core:t\/grades\""},{"name":"core:t\/groupn","icon":"fa-user","standardicon":"\"core:t\/groupn\""},{"name":"core:t\/groups","icon":"fa-user-circle","standardicon":"\"core:t\/groups\""},{"name":"core:t\/groupv","icon":"fa-user-circle-o","standardicon":"\"core:t\/groupv\""},{"name":"core:t\/hide","icon":"fa-eye","standardicon":"\"core:t\/hide\""},{"name":"core:t\/index_drawer","icon":"fa-list","standardicon":"\"core:t\/index_drawer\""},{"name":"core:t\/left","icon":"fa-arrow-left","standardicon":"\"core:t\/left\""},{"name":"core:t\/less","icon":"fa-caret-up","standardicon":"\"core:t\/less\""},{"name":"core:t\/life-ring","icon":"fa-life-ring","standardicon":"\"core:t\/life-ring\""},{"name":"core:t\/locked","icon":"fa-lock","standardicon":"\"core:t\/locked\""},{"name":"core:t\/lock","icon":"fa-unlock","standardicon":"\"core:t\/lock\""},{"name":"core:t\/locktime","icon":"fa-lock","standardicon":"\"core:t\/locktime\""},{"name":"core:t\/markasread","icon":"fa-check","standardicon":"\"core:t\/markasread\""},{"name":"core:t\/messages","icon":"fa-comments","standardicon":"\"core:t\/messages\""},{"name":"core:t\/messages-o","icon":"fa-comments-o","standardicon":"\"core:t\/messages-o\""},{"name":"core:t\/message","icon":"fa-comment-o","standardicon":"\"core:t\/message\""},{"name":"core:t\/more","icon":"fa-caret-down","standardicon":"\"core:t\/more\""},{"name":"core:t\/move","icon":"fa-arrows-v","standardicon":"\"core:t\/move\""},{"name":"core:t\/online","icon":"fa-circle","standardicon":"\"core:t\/online\""},{"name":"core:t\/passwordunmask-edit","icon":"fa-pencil","standardicon":"\"core:t\/passwordunmask-edit\""},{"name":"core:t\/passwordunmask-reveal","icon":"fa-eye","standardicon":"\"core:t\/passwordunmask-reveal\""},{"name":"core:t\/play","icon":"fa-play","standardicon":"\"core:t\/play\""},{"name":"core:t\/portfolioadd","icon":"fa-plus","standardicon":"\"core:t\/portfolioadd\""},{"name":"core:t\/preferences","icon":"fa-wrench","standardicon":"\"core:t\/preferences\""},{"name":"core:t\/preview","icon":"fa-search-plus","standardicon":"\"core:t\/preview\""},{"name":"core:t\/print","icon":"fa-print","standardicon":"\"core:t\/print\""},{"name":"core:t\/removecontact","icon":"fa-user-times","standardicon":"\"core:t\/removecontact\""},{"name":"core:t\/reload","icon":"fa-refresh","standardicon":"\"core:t\/reload\""},{"name":"core:t\/reset","icon":"fa-repeat","standardicon":"\"core:t\/reset\""},{"name":"core:t\/restore","icon":"fa-arrow-circle-up","standardicon":"\"core:t\/restore\""},{"name":"core:t\/right","icon":"fa-arrow-right","standardicon":"\"core:t\/right\""},{"name":"core:t\/sendmessage","icon":"fa-paper-plane","standardicon":"\"core:t\/sendmessage\""},{"name":"core:t\/show","icon":"fa-eye-slash","standardicon":"\"core:t\/show\""},{"name":"core:t\/sort_by","icon":"fa-sort-amount-asc","standardicon":"\"core:t\/sort_by\""},{"name":"core:t\/sort_asc","icon":"fa-sort-asc","standardicon":"\"core:t\/sort_asc\""},{"name":"core:t\/sort_desc","icon":"fa-sort-desc","standardicon":"\"core:t\/sort_desc\""},{"name":"core:t\/sort","icon":"fa-sort","standardicon":"\"core:t\/sort\""},{"name":"core:t\/stealth","icon":"fa-low-vision","standardicon":"\"core:t\/stealth\""},{"name":"core:t\/stop","icon":"fa-stop","standardicon":"\"core:t\/stop\""},{"name":"core:t\/switch_minus","icon":"fa-minus","standardicon":"\"core:t\/switch_minus\""},{"name":"core:t\/switch_plus","icon":"fa-plus","standardicon":"\"core:t\/switch_plus\""},{"name":"core:t\/switch_whole","icon":"fa-square-o","standardicon":"\"core:t\/switch_whole\""},{"name":"core:t\/tags","icon":"fa-tags","standardicon":"\"core:t\/tags\""},{"name":"core:t\/unblock","icon":"fa-commenting","standardicon":"\"core:t\/unblock\""},{"name":"core:t\/unlocked","icon":"fa-unlock-alt","standardicon":"\"core:t\/unlocked\""},{"name":"core:t\/unlock","icon":"fa-lock","standardicon":"\"core:t\/unlock\""},{"name":"core:t\/up","icon":"fa-arrow-up","standardicon":"\"core:t\/up\""},{"name":"core:t\/uplong","icon":"fa-long-arrow-up","standardicon":"\"core:t\/uplong\""},{"name":"core:t\/user","icon":"fa-user","standardicon":"\"core:t\/user\""},{"name":"core:t\/viewdetails","icon":"fa-list","standardicon":"\"core:t\/viewdetails\""},{"name":"qtype_ddmarker:crosshairs","icon":"fa-crosshairs","standardicon":"\"qtype_ddmarker:crosshairs\""},{"name":"qtype_ddmarker:grid","icon":"fa-th","standardicon":"\"qtype_ddmarker:grid\""},{"name":"mod_book:chapter","icon":"fa-bookmark-o","standardicon":"\"mod_book:chapter\""},{"name":"mod_book:nav_prev","icon":"fa-arrow-left","standardicon":"\"mod_book:nav_prev\""},{"name":"mod_book:nav_sep","icon":"fa-minus","standardicon":"\"mod_book:nav_sep\""},{"name":"mod_book:add","icon":"fa-plus","standardicon":"\"mod_book:add\""},{"name":"mod_book:nav_next","icon":"fa-arrow-right","standardicon":"\"mod_book:nav_next\""},{"name":"mod_book:nav_exit","icon":"fa-arrow-up","standardicon":"\"mod_book:nav_exit\""},{"name":"mod_choice:row","icon":"fa-info","standardicon":"\"mod_choice:row\""},{"name":"mod_choice:column","icon":"fa-columns","standardicon":"\"mod_choice:column\""},{"name":"mod_data:field\/checkbox","icon":"fa-check-square-o","standardicon":"\"mod_data:field\/checkbox\""},{"name":"mod_data:field\/date","icon":"fa-calendar-o","standardicon":"\"mod_data:field\/date\""},{"name":"mod_data:field\/file","icon":"fa-file","standardicon":"\"mod_data:field\/file\""},{"name":"mod_data:field\/latlong","icon":"fa-globe","standardicon":"\"mod_data:field\/latlong\""},{"name":"mod_data:field\/menu","icon":"fa-bars","standardicon":"\"mod_data:field\/menu\""},{"name":"mod_data:field\/multimenu","icon":"fa-bars","standardicon":"\"mod_data:field\/multimenu\""},{"name":"mod_data:field\/number","icon":"fa-hashtag","standardicon":"\"mod_data:field\/number\""},{"name":"mod_data:field\/picture","icon":"fa-picture-o","standardicon":"\"mod_data:field\/picture\""},{"name":"mod_data:field\/radiobutton","icon":"fa-circle-o","standardicon":"\"mod_data:field\/radiobutton\""},{"name":"mod_data:field\/textarea","icon":"fa-font","standardicon":"\"mod_data:field\/textarea\""},{"name":"mod_data:field\/text","icon":"fa-i-cursor","standardicon":"\"mod_data:field\/text\""},{"name":"mod_data:field\/url","icon":"fa-link","standardicon":"\"mod_data:field\/url\""},{"name":"mod_feedback:required","icon":"fa-exclamation-circle","standardicon":"\"mod_feedback:required\""},{"name":"mod_feedback:notrequired","icon":"fa-question-circle-o","standardicon":"\"mod_feedback:notrequired\""},{"name":"mod_forum:i\/pinned","icon":"fa-map-pin","standardicon":"\"mod_forum:i\/pinned\""},{"name":"mod_forum:t\/selected","icon":"fa-check","standardicon":"\"mod_forum:t\/selected\""},{"name":"mod_forum:t\/subscribed","icon":"fa-envelope-o","standardicon":"\"mod_forum:t\/subscribed\""},{"name":"mod_forum:t\/unsubscribed","icon":"fa-envelope-open-o","standardicon":"\"mod_forum:t\/unsubscribed\""},{"name":"mod_forum:t\/star","icon":"fa-star","standardicon":"\"mod_forum:t\/star\""},{"name":"mod_glossary:export","icon":"fa-download","standardicon":"\"mod_glossary:export\""},{"name":"mod_glossary:minus","icon":"fa-minus","standardicon":"\"mod_glossary:minus\""},{"name":"mod_lesson:e\/copy","icon":"fa-clone","standardicon":"\"mod_lesson:e\/copy\""},{"name":"mod_lti:warning","icon":"fa-exclamation text-warning","standardicon":"\"mod_lti:warning\""},{"name":"mod_quiz:navflagged","icon":"fa-flag","standardicon":"\"mod_quiz:navflagged\""},{"name":"mod_scorm:assetc","icon":"fa-file-archive-o","standardicon":"\"mod_scorm:assetc\""},{"name":"mod_scorm:asset","icon":"fa-file-archive-o","standardicon":"\"mod_scorm:asset\""},{"name":"mod_scorm:browsed","icon":"fa-book","standardicon":"\"mod_scorm:browsed\""},{"name":"mod_scorm:completed","icon":"fa-check-square-o","standardicon":"\"mod_scorm:completed\""},{"name":"mod_scorm:failed","icon":"fa-times","standardicon":"\"mod_scorm:failed\""},{"name":"mod_scorm:incomplete","icon":"fa-pencil-square-o","standardicon":"\"mod_scorm:incomplete\""},{"name":"mod_scorm:minus","icon":"fa-minus","standardicon":"\"mod_scorm:minus\""},{"name":"mod_scorm:notattempted","icon":"fa-square-o","standardicon":"\"mod_scorm:notattempted\""},{"name":"mod_scorm:passed","icon":"fa-check","standardicon":"\"mod_scorm:passed\""},{"name":"mod_scorm:plus","icon":"fa-plus","standardicon":"\"mod_scorm:plus\""},{"name":"mod_scorm:popdown","icon":"fa-window-close-o","standardicon":"\"mod_scorm:popdown\""},{"name":"mod_scorm:popup","icon":"fa-window-restore","standardicon":"\"mod_scorm:popup\""},{"name":"mod_scorm:suspend","icon":"fa-pause","standardicon":"\"mod_scorm:suspend\""},{"name":"mod_scorm:wait","icon":"fa-clock-o","standardicon":"\"mod_scorm:wait\""},{"name":"mod_wiki:attachment","icon":"fa-paperclip","standardicon":"\"mod_wiki:attachment\""},{"name":"mod_workshop:userplan\/task-info","icon":"fa-info text-info","standardicon":"\"mod_workshop:userplan\/task-info\""},{"name":"mod_workshop:userplan\/task-todo","icon":"fa-square-o","standardicon":"\"mod_workshop:userplan\/task-todo\""},{"name":"mod_workshop:userplan\/task-done","icon":"fa-check text-success","standardicon":"\"mod_workshop:userplan\/task-done\""},{"name":"mod_workshop:userplan\/task-fail","icon":"fa-remove text-danger","standardicon":"\"mod_workshop:userplan\/task-fail\""},{"name":"customfield_date:checked","icon":"fa-check-square-o","standardicon":"\"customfield_date:checked\""},{"name":"customfield_date:notchecked","icon":"fa-square-o","standardicon":"\"customfield_date:notchecked\""},{"name":"enrol_guest:withpassword","icon":"fa-key","standardicon":"\"enrol_guest:withpassword\""},{"name":"enrol_guest:withoutpassword","icon":"fa-unlock-alt","standardicon":"\"enrol_guest:withoutpassword\""},{"name":"enrol_lti:managedeployments","icon":"fa-sitemap","standardicon":"\"enrol_lti:managedeployments\""},{"name":"enrol_lti:platformdetails","icon":"fa-pencil-square-o","standardicon":"\"enrol_lti:platformdetails\""},{"name":"enrol_lti:enrolinstancewarning","icon":"fa-exclamation-circle text-danger","standardicon":"\"enrol_lti:enrolinstancewarning\""},{"name":"enrol_self:withkey","icon":"fa-key","standardicon":"\"enrol_self:withkey\""},{"name":"enrol_self:withoutkey","icon":"fa-sign-in","standardicon":"\"enrol_self:withoutkey\""},{"name":"block_accessreview:smile","icon":"fa-smile-o","standardicon":"\"block_accessreview:smile\""},{"name":"block_accessreview:frown","icon":"fa-frown-o","standardicon":"\"block_accessreview:frown\""},{"name":"block_accessreview:errorsfound","icon":"fa-ban","standardicon":"\"block_accessreview:errorsfound\""},{"name":"block_accessreview:f\/pdf","icon":"fa-file-pdf-o","standardicon":"\"block_accessreview:f\/pdf\""},{"name":"block_accessreview:f\/video","icon":"fa-file-video-o","standardicon":"\"block_accessreview:f\/video\""},{"name":"block_accessreview:f\/find","icon":"fa-bar-chart","standardicon":"\"block_accessreview:f\/find\""},{"name":"block_accessreview:f\/form","icon":"fa-pencil-square-o","standardicon":"\"block_accessreview:f\/form\""},{"name":"block_accessreview:f\/image","icon":"fa-image","standardicon":"\"block_accessreview:f\/image\""},{"name":"block_accessreview:f\/layout","icon":"fa-th-large","standardicon":"\"block_accessreview:f\/layout\""},{"name":"block_accessreview:f\/link","icon":"fa-link","standardicon":"\"block_accessreview:f\/link\""},{"name":"block_accessreview:f\/media","icon":"fa-play-circle-o","standardicon":"\"block_accessreview:f\/media\""},{"name":"block_accessreview:f\/table","icon":"fa-table","standardicon":"\"block_accessreview:f\/table\""},{"name":"block_accessreview:f\/text","icon":"fa-font","standardicon":"\"block_accessreview:f\/text\""},{"name":"block_accessreview:t\/fail","icon":"fa-ban","standardicon":"\"block_accessreview:t\/fail\""},{"name":"block_accessreview:t\/pass","icon":"fa-check","standardicon":"\"block_accessreview:t\/pass\""},{"name":"gradingform_guide:info","icon":"fa-info-circle","standardicon":"\"gradingform_guide:info\""},{"name":"gradingform_guide:plus","icon":"fa-plus","standardicon":"\"gradingform_guide:plus\""},{"name":"tool_brickfield:f\/award","icon":"fa-tachometer","standardicon":"\"tool_brickfield:f\/award\""},{"name":"tool_brickfield:f\/done","icon":"fa-check-circle-o","standardicon":"\"tool_brickfield:f\/done\""},{"name":"tool_brickfield:f\/done2","icon":"fa-check-square-o","standardicon":"\"tool_brickfield:f\/done2\""},{"name":"tool_brickfield:f\/error","icon":"fa-times-circle-o","standardicon":"\"tool_brickfield:f\/error\""},{"name":"tool_brickfield:f\/find","icon":"fa-bar-chart","standardicon":"\"tool_brickfield:f\/find\""},{"name":"tool_brickfield:f\/total","icon":"fa-calculator","standardicon":"\"tool_brickfield:f\/total\""},{"name":"tool_brickfield:f\/form","icon":"fa-pencil-square-o","standardicon":"\"tool_brickfield:f\/form\""},{"name":"tool_brickfield:f\/image","icon":"fa-image","standardicon":"\"tool_brickfield:f\/image\""},{"name":"tool_brickfield:f\/layout","icon":"fa-th-large","standardicon":"\"tool_brickfield:f\/layout\""},{"name":"tool_brickfield:f\/link","icon":"fa-link","standardicon":"\"tool_brickfield:f\/link\""},{"name":"tool_brickfield:f\/media","icon":"fa-play-circle-o","standardicon":"\"tool_brickfield:f\/media\""},{"name":"tool_brickfield:f\/table","icon":"fa-table","standardicon":"\"tool_brickfield:f\/table\""},{"name":"tool_brickfield:f\/text","icon":"fa-font","standardicon":"\"tool_brickfield:f\/text\""},{"name":"tool_lp:url","icon":"fa-external-link","standardicon":"\"tool_lp:url\""},{"name":"tool_policy:agreed","icon":"fa-check text-success","standardicon":"\"tool_policy:agreed\""},{"name":"tool_policy:declined","icon":"fa-times text-danger","standardicon":"\"tool_policy:declined\""},{"name":"tool_policy:pending","icon":"fa-clock-o text-warning","standardicon":"\"tool_policy:pending\""},{"name":"tool_policy:partial","icon":"fa-exclamation-triangle text-warning","standardicon":"\"tool_policy:partial\""},{"name":"tool_policy:level","icon":"fa-level-up fa-rotate-90 text-muted","standardicon":"\"tool_policy:level\""},{"name":"tool_recyclebin:trash","icon":"fa-trash","standardicon":"\"tool_recyclebin:trash\""},{"name":"tool_usertours:t\/export","icon":"fa-download","standardicon":"\"tool_usertours:t\/export\""},{"name":"tool_usertours:i\/reload","icon":"fa-refresh","standardicon":"\"tool_usertours:i\/reload\""},{"name":"tool_usertours:t\/filler","icon":"fa-spacer","standardicon":"\"tool_usertours:t\/filler\""},{"name":"atto_collapse:icon","icon":"fa-level-down","standardicon":"\"atto_collapse:icon\""},{"name":"atto_recordrtc:i\/audiortc","icon":"fa-microphone","standardicon":"\"atto_recordrtc:i\/audiortc\""},{"name":"atto_recordrtc:i\/videortc","icon":"fa-video-camera","standardicon":"\"atto_recordrtc:i\/videortc\""}] \ No newline at end of file diff --git a/admin/tool/componentlibrary/hugo/site/layouts/shortcodes/moodleicons.html b/admin/tool/componentlibrary/hugo/site/layouts/shortcodes/moodleicons.html index bb0012391f915..ae90f3d61dad1 100644 --- a/admin/tool/componentlibrary/hugo/site/layouts/shortcodes/moodleicons.html +++ b/admin/tool/componentlibrary/hugo/site/layouts/shortcodes/moodleicons.html @@ -8,7 +8,7 @@
{{ .name }}
- +
{{ .standardicon | safeHTML }} diff --git a/admin/tool/dataprivacy/classes/data_request.php b/admin/tool/dataprivacy/classes/data_request.php index 816ef5c53e1c5..640b5e5290ae8 100644 --- a/admin/tool/dataprivacy/classes/data_request.php +++ b/admin/tool/dataprivacy/classes/data_request.php @@ -156,9 +156,9 @@ public static function is_expired(data_request $request) { case api::DATAREQUEST_STATUS_EXPIRED: $result = true; break; - // Complete requests are expired if the expiry time has elapsed. + // Complete requests are expired if the expiry time is a positive value, and has elapsed. case api::DATAREQUEST_STATUS_DOWNLOAD_READY: - $expiryseconds = get_config('tool_dataprivacy', 'privacyrequestexpiry'); + $expiryseconds = (int) get_config('tool_dataprivacy', 'privacyrequestexpiry'); if ($expiryseconds > 0 && time() >= ($request->get('timemodified') + $expiryseconds)) { $result = true; } @@ -179,7 +179,12 @@ public static function is_expired(data_request $request) { public static function get_expired_requests($userid = 0) { global $DB; - $expiryseconds = get_config('tool_dataprivacy', 'privacyrequestexpiry'); + // Complete requests are expired if the expiry time is a positive value, and has elapsed. + $expiryseconds = (int) get_config('tool_dataprivacy', 'privacyrequestexpiry'); + if ($expiryseconds <= 0) { + return []; + } + $expirytime = strtotime("-{$expiryseconds} second"); $table = self::TABLE; $sqlwhere = 'type = :export_type AND status = :completestatus AND timemodified <= :expirytime'; diff --git a/admin/tool/dataprivacy/tests/expired_data_requests_test.php b/admin/tool/dataprivacy/tests/expired_data_requests_test.php index 2efa7c8015e13..142e812fb0c74 100644 --- a/admin/tool/dataprivacy/tests/expired_data_requests_test.php +++ b/admin/tool/dataprivacy/tests/expired_data_requests_test.php @@ -26,10 +26,11 @@ * Expired data requests tests. * * @package tool_dataprivacy + * @covers \tool_dataprivacy\data_request * @copyright 2018 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class expired_data_requests_test extends data_privacy_testcase { +final class expired_data_requests_test extends data_privacy_testcase { /** * Test tearDown. @@ -108,6 +109,52 @@ public function test_data_request_expiry(): void { $this->assertEquals(0, $DB->count_records('files', $fileconditions)); } + /** + * Test that data requests are not expired when expiration is disabled (set to zero) + */ + public function test_data_request_expiry_never(): void { + global $DB; + + $this->resetAfterTest(); + + \core_privacy\local\request\writer::setup_real_writer_instance(); + + // Disable request expiry. + set_config('privacyrequestexpiry', 0, 'tool_dataprivacy'); + + // Create and approve data request. + $user = $this->getDataGenerator()->create_user(); + $usercontext = \context_user::instance($user->id); + + $this->setUser($user->id); + $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT); + $requestid = $datarequest->get('id'); + + $this->setAdminUser(); + api::approve_data_request($requestid); + + ob_start(); + $this->runAdhocTasks('\tool_dataprivacy\task\process_data_request_task'); + ob_end_clean(); + + // Run expiry deletion - should not affect test export. + $expiredrequests = data_request::get_expired_requests(); + $this->assertEmpty($expiredrequests); + data_request::expire($expiredrequests); + + // Confirm approved and exported. + $request = new data_request($requestid); + $this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $request->get('status')); + $fileconditions = [ + 'userid' => $user->id, + 'component' => 'tool_dataprivacy', + 'filearea' => 'export', + 'itemid' => $requestid, + 'contextid' => $usercontext->id, + ]; + $this->assertEquals(2, $DB->count_records('files', $fileconditions)); + } + /** * Test for \tool_dataprivacy\data_request::is_expired() * Tests for the expected request status to protect from false positive/negative, diff --git a/admin/tool/generator/classes/form/featureimport.php b/admin/tool/generator/classes/form/featureimport.php index e5c121d58f821..e6497ab3a8177 100644 --- a/admin/tool/generator/classes/form/featureimport.php +++ b/admin/tool/generator/classes/form/featureimport.php @@ -43,6 +43,13 @@ public function definition(): void { ['accepted_types' => ['.feature']] ); $mform->addRule('featurefile', null, 'required'); + + $options = [ + 0 => get_string('execute_scenarios', 'tool_generator'), + 1 => get_string('execute_cleanup', 'tool_generator'), + ]; + $mform->addElement('select', 'executecleanup', get_string('execute', 'tool_generator'), $options); + $this->add_action_buttons(false, get_string('import')); } diff --git a/admin/tool/generator/classes/local/testscenario/runner.php b/admin/tool/generator/classes/local/testscenario/runner.php index 8122cf5cadc5f..3f8d2dc47833a 100644 --- a/admin/tool/generator/classes/local/testscenario/runner.php +++ b/admin/tool/generator/classes/local/testscenario/runner.php @@ -19,9 +19,12 @@ use behat_admin; use behat_data_generators; use behat_base; +use behat_course; +use behat_user; use Behat\Gherkin\Parser; use Behat\Gherkin\Lexer; use Behat\Gherkin\Keywords\ArrayKeywords; +use Behat\Gherkin\Node\OutlineNode; use ReflectionClass; use ReflectionMethod; use stdClass; @@ -48,6 +51,7 @@ public function init() { $this->include_composer_libraries(); $this->include_behat_libraries(); $this->load_generator(); + $this->load_cleanup(); } /** @@ -76,6 +80,9 @@ public function include_behat_libraries() { require_once($CFG->libdir . '/behat/behat_base.php'); require_once("{$CFG->libdir}/tests/behat/behat_data_generators.php"); require_once("{$CFG->dirroot}/admin/tests/behat/behat_admin.php"); + require_once("{$CFG->dirroot}/course/lib.php"); + require_once("{$CFG->dirroot}/course/tests/behat/behat_course.php"); + require_once("{$CFG->dirroot}/user/tests/behat/behat_user.php"); return true; } @@ -96,6 +103,27 @@ private function load_generator() { } } + /** + * Load all cleanup steps. + */ + private function load_cleanup() { + $extra = $this->scan_method( + new ReflectionMethod(behat_course::class, 'the_course_is_deleted'), + new behat_course(), + ); + if ($extra) { + $this->validsteps[$extra->given] = $extra; + } + + $extra = $this->scan_method( + new ReflectionMethod(behat_user::class, 'the_user_is_deleted'), + new behat_user(), + ); + if ($extra) { + $this->validsteps[$extra->given] = $extra; + } + } + /** * Scan a generator to get all valid steps. * @param behat_data_generators $generator the generator to scan. @@ -158,6 +186,19 @@ private function get_method_given(ReflectionMethod $method): ?string { * @return parsedfeature */ public function parse_feature(string $content): parsedfeature { + return $this->parse_selected_scenarios($content); + } + + /** + * Parse all feature file scenarios. + * + * Note: if no filter is passed, it will execute only the scenarios that are not tagged. + * + * @param string $content the feature file content. + * @param string $filtertag the tag to filter the scenarios. + * @return parsedfeature + */ + private function parse_selected_scenarios(string $content, ?string $filtertag = null): parsedfeature { $result = new parsedfeature(); $parser = $this->get_parser(); @@ -169,9 +210,15 @@ public function parse_feature(string $content): parsedfeature { if ($feature->hasScenarios()) { $scenarios = $feature->getScenarios(); foreach ($scenarios as $scenario) { + // By default, we only execute scenaros that are not tagged. + if (empty($filtertag) && !empty($scenario->getTags())) { + continue; + } + if ($filtertag && !in_array($filtertag, $scenario->getTags())) { + continue; + } if ($scenario->getNodeType() == 'Outline') { - $result->add_scenario($scenario->getNodeType(), $scenario->getTitle()); - $result->add_error(get_string('testscenario_outline', 'tool_generator')); + $this->parse_scenario_outline($scenario, $result); continue; } $result->add_scenario($scenario->getNodeType(), $scenario->getTitle()); @@ -184,6 +231,32 @@ public function parse_feature(string $content): parsedfeature { return $result; } + /** + * Parse a feature file using only the scenarios with cleanup tag. + * @param string $content the feature file content. + * @return parsedfeature + */ + public function parse_cleanup(string $content): parsedfeature { + return $this->parse_selected_scenarios($content, 'cleanup'); + } + + /** + * Parse a scenario outline. + * @param OutlineNode $scenario the scenario outline to parse. + * @param parsedfeature $result the parsed feature to add the scenario. + */ + private function parse_scenario_outline(OutlineNode $scenario, parsedfeature $result) { + $count = 1; + foreach ($scenario->getExamples() as $example) { + $result->add_scenario($example->getNodeType(), $example->getOutlineTitle() . " ($count)"); + $steps = $example->getSteps(); + foreach ($steps as $step) { + $result->add_step(new steprunner(null, $this->validsteps, $step)); + } + $count++; + } + } + /** * Get the parser. * @return Parser @@ -192,7 +265,6 @@ private function get_parser(): Parser { $keywords = new ArrayKeywords([ 'en' => [ 'feature' => 'Feature', - // If in the future we have clean up steps, background will be renamed to "Clean up". 'background' => 'Background', 'scenario' => 'Scenario', 'scenario_outline' => 'Scenario Outline|Scenario Template', diff --git a/admin/tool/generator/classes/output/parsingresult.php b/admin/tool/generator/classes/output/parsingresult.php index cdf99520db4e1..9a6f4588999b5 100644 --- a/admin/tool/generator/classes/output/parsingresult.php +++ b/admin/tool/generator/classes/output/parsingresult.php @@ -57,10 +57,12 @@ public function export_for_template(renderer_base $output): array { $haslines = false; foreach ($this->parsedfeature->get_scenarios() as $scenario) { $scenariodata = [ - 'type' => $scenario->type, - 'name' => $scenario->name, + 'type' => ucfirst($scenario->type), 'steps' => [], ]; + if (!empty($scenario->name)) { + $scenariodata['name'] = $scenario->name; + } if (!empty($scenario->error)) { $scenariodata['scenarioerror'] = $scenario->error; } diff --git a/admin/tool/generator/cli/runtestscenario.php b/admin/tool/generator/cli/runtestscenario.php index 855544a0a670c..fe52edcb9f6ab 100644 --- a/admin/tool/generator/cli/runtestscenario.php +++ b/admin/tool/generator/cli/runtestscenario.php @@ -43,10 +43,12 @@ 'disable-composer' => false, 'composer-upgrade' => true, 'composer-self-update' => true, + 'cleanup' => false, ], [ 'h' => 'help', 'f' => 'feature', + 'c' => 'cleanup', ] ); @@ -60,11 +62,13 @@ Usage: php runtestscenario.php [--feature=\"value\"] [--help] [--no-composer-self-update] [--no-composer-upgrade] - [--disable-composer] + [--disable-composer] [--cleanup] Options: -f, --feature Execute specified feature file (Absolute path of feature file). +-c, --cleanup Execute the scenarios with @cleanup tag. + --no-composer-self-update Prevent upgrade of the composer utility using its self-update command @@ -79,6 +83,7 @@ Example from Moodle root directory: \$ php admin/tool/generator/cli/runtestscenario.php --feature=/path/to/some/testing/scenario.feature +\$ php admin/tool/generator/cli/runtestscenario.php --feature=/path/to/some/testing/scenario.feature --cleanup "; if (!empty($options['help'])) { @@ -139,7 +144,11 @@ } try { - $parsedfeature = $runner->parse_feature($content); + if (!empty($options['cleanup'])) { + $parsedfeature = $runner->parse_cleanup($content); + } else { + $parsedfeature = $runner->parse_feature($content); + } } catch (\Exception $error) { echo "Error parsing feature file: {$error->getMessage()}\n"; echo "Use the web version of the tool to see the parsing details:\n"; diff --git a/admin/tool/generator/lang/en/tool_generator.php b/admin/tool/generator/lang/en/tool_generator.php index a60fbdf4e59a2..731c6b92ea1b7 100644 --- a/admin/tool/generator/lang/en/tool_generator.php +++ b/admin/tool/generator/lang/en/tool_generator.php @@ -64,6 +64,9 @@ $string['error_nopageinstances'] = 'The selected course does not contain page module instances'; $string['error_notdebugging'] = 'Not available on this server because debugging is not set to DEVELOPER'; $string['error_nouserspassword'] = 'You need to set $CFG->tool_generator_users_password in config.php to generate the test plan'; +$string['execute'] = 'Execute'; +$string['execute_cleanup'] = 'Cleanup scenarios'; +$string['execute_scenarios'] = 'Testing scenarios'; $string['fullname'] = 'Test course: {$a->size}'; $string['maketestcourse'] = 'Make test course'; $string['maketestplan'] = 'Make JMeter test plan'; diff --git a/admin/tool/generator/runtestscenario.php b/admin/tool/generator/runtestscenario.php index c1253151629ad..dfadef909570e 100644 --- a/admin/tool/generator/runtestscenario.php +++ b/admin/tool/generator/runtestscenario.php @@ -79,7 +79,11 @@ } try { - $parsedfeature = $runner->parse_feature($content); + if ($data->executecleanup) { + $parsedfeature = $runner->parse_cleanup($content); + } else { + $parsedfeature = $runner->parse_feature($content); + } } catch (\Throwable $th) { echo $output->notification(get_string('testscenario_errorparsing', 'tool_generator', $th->getMessage())); echo $output->continue_button($currenturl); diff --git a/admin/tool/generator/templates/parsingresult.mustache b/admin/tool/generator/templates/parsingresult.mustache index 244996b3af358..25bf2492a7423 100644 --- a/admin/tool/generator/templates/parsingresult.mustache +++ b/admin/tool/generator/templates/parsingresult.mustache @@ -27,7 +27,7 @@ "scenarios": [ { "type": "Scenario", - "title": "Scenario title", + "name": "Scenario title", "hassteps": true, "steps": [ { @@ -53,7 +53,7 @@

{{#str}} testscenario_steps, tool_generator {{/str}}

{{/haslines}} {{#scenarios}} -

{{type}}: {{name}}

+

{{type}}{{#name}}: {{name}} {{/name}}

{{#scenarioerror}} ')),this.children.push(i),i.addTarget(this),this.inner.one(".availability-children").appendChild(i.node)},M.core_availability.List.prototype.focusAfterAdd=function(){this.inner.one("button").focus()},M.core_availability.List.prototype.isIndividualShowIcons=function(){var i,t;if(!this.root)throw"Can only call this on root list";return i="!"===this.node.one(".availability-neg").get("value"),t="|"===this.node.one(".availability-op").get("value"),!i&&!t||i&&t},M.core_availability.List.prototype.renumber=function(i){var t,e={count:this.children.length},a=i===undefined?e.number="":(e.number=i+":",i+"."),e=M.util.get_string("setheading","availability",e);for(this.node.one("> h3").set("innerHTML",e),this.node.one("> h3").getDOMNode().dataset.restrictionOrder=i||"root",t=0;t .availability-children").removeAttribute("aria-hidden"),this.inner.one("> .availability-none").setAttribute("aria-hidden","true"),this.inner.one("> .availability-header").removeAttribute("aria-hidden"),1 .availability-children").setAttribute("aria-hidden","true"),this.inner.one("> .availability-none").removeAttribute("aria-hidden"),this.inner.one("> .availability-header").setAttribute("aria-hidden","true")),this.root){for(i=this.isIndividualShowIcons(),t=0;t .availability-children > .availability-connector span.label").each(function(i){i.set("innerHTML",a)})},M.core_availability.List.prototype.deleteDescendant=function(i){for(var t,e,a=0;a .availability-children").removeChild(e),M.core_availability.form.update(),this.updateHtml(),this.inner.one("> .availability-button").one("button").focus(),!0;if(t instanceof M.core_availability.List&&t.deleteDescendant(i))return!0}return!1},M.core_availability.List.prototype.clickAdd=function(){var i,t,e,a,l,n=d.Node.create('
    "),o=n.one("button"),s={dialog:null},r=n.one("ul");for(l in M.core_availability.form.plugins)M.core_availability.form.plugins[l].allowAdd&&(i=d.Node.create('
  • '),(e=d.Node.create('
    ")).on("click",this.getAddHandler(l,s),this),i.appendChild(e),a=d.Node.create('
    "),i.appendChild(a),r.appendChild(i));i=d.Node.create('
  • '),(e=d.Node.create('
    ")).on("click",this.getAddHandler(null,s),this),i.appendChild(e),a=d.Node.create('
    "),i.appendChild(a),r.appendChild(i),n={headerContent:M.util.get_string("addrestriction","availability"),bodyContent:n,additionalBaseClass:"availability-dialogue",draggable:!0,modal:!0, +closeButton:!1,width:"450px"},s.dialog=new M.core.dialogue(n),s.dialog.show(),o.on("click",function(){s.dialog.destroy(),this.inner.one("> .availability-button").one("button").focus()},this)},M.core_availability.List.prototype.getAddHandler=function(t,e){return function(){var i=!0;t&&M.core_availability.form.plugins[t].displayMode&&(i=!1),i=t?new M.core_availability.Item({type:t,creating:!0,showc:i},this.root):new M.core_availability.List({c:[],showc:i},!1,this.root),this.addChild(i),M.core_availability.form.update(),M.core_availability.form.rootList.renumber(),this.updateHtml(),e.dialog.destroy(),i.focusAfterAdd()}},M.core_availability.List.prototype.getValue=function(){var i,t,e={};for(e.op=this.node.one(".availability-neg").get("value")+this.node.one(".availability-op").get("value"),e.c=[],i=0;i'+M.util.get_string("missingplugin","availability")+"
    ")):(this.plugin=M.core_availability.form.plugins[i.type],this.pluginNode=this.plugin.getNode(i),this.pluginNode.addClass("availability_"+i.type)),d.augment(this.pluginNode,d.EventTarget,!0,null,{emitFacade:!0}),this.pluginNode.addTarget(this),this.node=d.Node.create('

    '),t&&(t=!0,i.showc!==undefined&&(t=i.showc),this.eyeIcon=new M.core_availability.EyeIcon(!0,t),this.node.appendChild(this.eyeIcon.span),this.node.appendChild(this.eyeIcon.disabledSpan)),this.pluginNode.addClass("availability-plugincontrols"),this.node.appendChild(this.pluginNode),i=new M.core_availability.DeleteIcon(this),this.node.appendChild(i.span),this.node.appendChild(document.createTextNode(" ")),this.node.appendChild(d.Node.create(''))},d.augment(M.core_availability.Item,d.EventTarget,!0,null,{emitFacade:!0}),M.core_availability.Item.prototype.getValue=function(){var i={type:this.pluginType};return this.plugin&&this.plugin.fillValue(i,this.pluginNode),i},M.core_availability.Item.prototype.fillErrors=function(i){var t,e,a,l=i.length;this.plugin?this.plugin.fillErrors(i,this.pluginNode):i.push("core_availability:item_unknowntype"),t=this.node.one("> .bg-warning"),i.length===l||t.get("firstChild")?i.length===l&&t.get("firstChild")&&t.get("firstChild").remove():(l="",e=(i=i[i.length-1].split(":"))[0],a="[["+(i=i[1])+","+e+"]]",(l=M.util.get_string(i,e))===a&&(l=M.util.get_string("invalid","availability")),t.appendChild(document.createTextNode(l)))},M.core_availability.Item.prototype.renumber=function(i){var t={number:i};this.plugin?t.type=M.util.get_string("title","availability_"+this.pluginType):t.type="["+this.pluginType+"]",t.number=i+":",t=M.util.get_string("itemheading","availability",t),this.node.one("> h3").set("innerHTML",t),this.node.one("> h3").getDOMNode().dataset.restrictionOrder=i||"root"},M.core_availability.Item.prototype.focusAfterAdd=function(){this.plugin.focusAfterAdd(this.pluginNode)},M.core_availability.Item.prototype.pluginType=null,M.core_availability.Item.prototype.plugin=null,M.core_availability.Item.prototype.eyeIcon=null,M.core_availability.Item.prototype.node=null,M.core_availability.Item.prototype.pluginNode=null,M.core_availability.EyeIcon=function(i,t){var e,a,l,n;this.individual=i,this.span=d.Node.create('
    '),e=d.Node.create(""),this.span.appendChild(e),a=i?"_individual":"_all",l=function(){var i=M.util.get_string("hidden"+a,"availability");e.set("src",M.util.image_url("i/show","core")),e.set("alt",i),this.span.set("title",i+" • "+M.util.get_string("show_verb","availability"))},n=function(){var i=M.util.get_string("shown"+a,"availability");e.set("src",M.util.image_url("i/hide","core")),e.set("alt",i),this.span.set("title",i+" • "+M.util.get_string("hide_verb","availability"))},(t?n:l).call(this),this.span.on("click",i=function(i){i.preventDefault(),(this.isHidden()?n:l).call(this),M.core_availability.form.update()},this),this.span.on("key",i,"up:32",this),this.span.on("key",function(i){i.preventDefault()},"down:32",this),this.disabledSpan=d.Node.create(''),t=d.Node.create(""),i=M.util.get_string("hidden"+a,"availability"),t.set("src",M.util.image_url("i/show","core")),t.set("alt",i), +this.disabledSpan.set("title",i+" • "+M.util.get_string("disabled_verb","availability")),this.disabledSpan.appendChild(t),this.disabledSpan.hide()},M.core_availability.EyeIcon.prototype.individual=!1,M.core_availability.EyeIcon.prototype.span=null,M.core_availability.EyeIcon.prototype.disabledSpan=null,M.core_availability.EyeIcon.prototype.isHidden=function(){var i=this.individual?"_individual":"_all",i=M.util.get_string("hidden"+i,"availability");return this.span.one("img").get("alt")===i},M.core_availability.EyeIcon.prototype.isDisabled=function(){return this.span.hasAttribute("hidden")},M.core_availability.EyeIcon.prototype.setDisabled=function(){this.isDisabled()||(this.span.hide(),this.disabledSpan.show())},M.core_availability.EyeIcon.prototype.setEnabled=function(){this.isDisabled()&&(this.span.show(),this.disabledSpan.hide())},M.core_availability.DeleteIcon=function(t){var i;this.span=d.Node.create(''),i=d.Node.create(''+M.util.get_string('),this.span.appendChild(i),this.span.on("click",i=function(i){i.preventDefault(),M.core_availability.form.rootList.deleteDescendant(t),M.core_availability.form.rootList.renumber()},this),this.span.on("key",i,"up:32",this),this.span.on("key",function(i){i.preventDefault()},"down:32",this)},M.core_availability.DeleteIcon.prototype.span=null},"@VERSION@",{requires:["base","node","event","event-delegate","panel","moodle-core-notification-dialogue","json"]}); \ No newline at end of file diff --git a/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form.js b/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form.js index 3366368971fa5..57e2137f5cadb 100644 --- a/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form.js +++ b/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form.js @@ -274,13 +274,17 @@ M.core_availability.plugin = { * overridden by child plugin. * * @method init - * @param {String} component Component name e.g. 'availability_date' + * @param {String} component Component name e.g. 'availability_date'. + * @param {boolean} allowAdd Indicates whether adding new instances of the plugin is permitted. + * @param {Object} params Additional parameters. + * @param {boolean} displayMode Whether the eye icon is show or hide. True for "Hide", false for "Show". */ - init: function(component, allowAdd, params) { + init: function(component, allowAdd, params, displayMode) { var name = component.replace(/^availability_/, ''); this.allowAdd = allowAdd; M.core_availability.form.plugins[name] = this; this.initInner.apply(this, params); + this.displayMode = displayMode; }, /** @@ -778,12 +782,17 @@ M.core_availability.List.prototype.clickAdd = function() { M.core_availability.List.prototype.getAddHandler = function(type, dialogRef) { return function() { var newItem; + var displayMode = true; + // Check if we have changed the eye icon in the manage restriction to hidden. + if (type && M.core_availability.form.plugins[type].displayMode) { + displayMode = false; + } if (type) { // Create an Item object to represent the child. - newItem = new M.core_availability.Item({type: type, creating: true}, this.root); + newItem = new M.core_availability.Item({type: type, creating: true, showc: displayMode}, this.root); } else { // Create a new List object to represent the child. - newItem = new M.core_availability.List({c: [], showc: true}, false, this.root); + newItem = new M.core_availability.List({c: [], showc: displayMode}, false, this.root); } // Add to list. this.addChild(newItem); diff --git a/availability/yui/src/form/js/form.js b/availability/yui/src/form/js/form.js index d815eafbb018c..59ab34bd951f7 100644 --- a/availability/yui/src/form/js/form.js +++ b/availability/yui/src/form/js/form.js @@ -272,13 +272,17 @@ M.core_availability.plugin = { * overridden by child plugin. * * @method init - * @param {String} component Component name e.g. 'availability_date' + * @param {String} component Component name e.g. 'availability_date'. + * @param {boolean} allowAdd Indicates whether adding new instances of the plugin is permitted. + * @param {Object} params Additional parameters. + * @param {boolean} displayMode Whether the eye icon is show or hide. True for "Hide", false for "Show". */ - init: function(component, allowAdd, params) { + init: function(component, allowAdd, params, displayMode) { var name = component.replace(/^availability_/, ''); this.allowAdd = allowAdd; M.core_availability.form.plugins[name] = this; this.initInner.apply(this, params); + this.displayMode = displayMode; }, /** @@ -776,12 +780,17 @@ M.core_availability.List.prototype.clickAdd = function() { M.core_availability.List.prototype.getAddHandler = function(type, dialogRef) { return function() { var newItem; + var displayMode = true; + // Check if we have changed the eye icon in the manage restriction to hidden. + if (type && M.core_availability.form.plugins[type].displayMode) { + displayMode = false; + } if (type) { // Create an Item object to represent the child. - newItem = new M.core_availability.Item({type: type, creating: true}, this.root); + newItem = new M.core_availability.Item({type: type, creating: true, showc: displayMode}, this.root); } else { // Create a new List object to represent the child. - newItem = new M.core_availability.List({c: [], showc: true}, false, this.root); + newItem = new M.core_availability.List({c: [], showc: displayMode}, false, this.root); } // Add to list. this.addChild(newItem); diff --git a/backup/controller/base_controller.class.php b/backup/controller/base_controller.class.php index a932ca25fbd68..f6dd3cef711ff 100644 --- a/backup/controller/base_controller.class.php +++ b/backup/controller/base_controller.class.php @@ -107,38 +107,18 @@ public function get_releasesession() { } /** - * Store extra data for course copy operations. - * - * For a course copying these is data required to be passed to the restore step. - * We store this data in its own section of the backup controller - * - * @param \stdClass $data The course copy data. - * @throws backup_controller_exception - * @deprecated since Moodle 4.1 MDL-74548 - please do not use this method anymore. - * @todo MDL-75025 This method will be deleted in Moodle 4.5 - * @see restore_controller::__construct() + * @deprecated since Moodle 4.1 MDL-74548 */ - public function set_copy(\stdClass $data): void { - debugging('The method base_controller::set_copy() is deprecated. - Please use the restore_controller class instead.', DEBUG_DEVELOPER); - // Only allow setting of copy data when controller is in copy mode. - if ($this->mode != backup::MODE_COPY) { - throw new backup_controller_exception('cannot_set_copy_vars_wrong_mode'); - } - $this->copy = $data; + #[\core\attribute\deprecated(since: '4.1', mdl: 'MDL-74548', final: true)] + public function set_copy(): void { + \core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]); } /** - * Get the course copy data. - * - * @return \stdClass - * @deprecated since Moodle 4.1 MDL-74548 - please do not use this method anymore. - * @todo MDL-75026 This method will be deleted in Moodle 4.5 - * @see restore_controller::get_copy() + * @deprecated since Moodle 4.1 MDL-74548 */ - public function get_copy(): \stdClass { - debugging('The method base_controller::get_copy() is deprecated. - Please use restore_controller::get_copy() instead.', DEBUG_DEVELOPER); - return $this->copy; + #[\core\attribute\deprecated('restore_controller::get_copy()', since: '4.1', mdl: 'MDL-74548', final: true)] + public function get_copy() { + \core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]); } } diff --git a/backup/moodle2/backup_root_task.class.php b/backup/moodle2/backup_root_task.class.php index 79524fe60fab8..221aae42a9f35 100644 --- a/backup/moodle2/backup_root_task.class.php +++ b/backup/moodle2/backup_root_task.class.php @@ -139,8 +139,6 @@ protected function define_settings() { $badges = new backup_badges_setting('badges', base_setting::IS_BOOLEAN, true); $badges->set_ui(new backup_setting_ui_checkbox($badges, get_string('rootsettingbadges', 'backup'))); $this->add_setting($badges); - $activities->add_dependency($badges); - $users->add_dependency($badges); // Define calendar events. $events = new backup_calendarevents_setting('calendarevents', base_setting::IS_BOOLEAN, true); diff --git a/backup/moodle2/backup_stepslib.php b/backup/moodle2/backup_stepslib.php index e1c6db33da3b9..d433ff2841a9b 100644 --- a/backup/moodle2/backup_stepslib.php +++ b/backup/moodle2/backup_stepslib.php @@ -914,14 +914,6 @@ protected function define_structure() { */ class backup_badges_structure_step extends backup_structure_step { - protected function execute_condition() { - // Check that all activities have been included. - if ($this->task->is_excluding_activities()) { - return false; - } - return true; - } - protected function define_structure() { global $CFG; @@ -965,10 +957,16 @@ protected function define_structure() { // Build the tree. $badges->add_child($badge); - $badge->add_child($criteria); - $criteria->add_child($criterion); - $criterion->add_child($parameters); - $parameters->add_child($parameter); + + // Have the activities been included? Only if that's the case, the criteria will be included too. + $activitiesincluded = !$this->task->is_excluding_activities(); + if ($activitiesincluded) { + $badge->add_child($criteria); + $criteria->add_child($criterion); + $criterion->add_child($parameters); + $parameters->add_child($parameter); + } + $badge->add_child($endorsement); $badge->add_child($alignments); $alignments->add_child($alignment); @@ -990,19 +988,20 @@ protected function define_structure() { 'courseid' => backup::VAR_COURSEID ]; $badge->set_source_sql($parametersql, $parameterparams); - $criterion->set_source_table('badge_criteria', array('badgeid' => backup::VAR_PARENTID)); + if ($activitiesincluded) { + $criterion->set_source_table('badge_criteria', ['badgeid' => backup::VAR_PARENTID]); + $parametersql = 'SELECT cp.*, c.criteriatype + FROM {badge_criteria_param} cp JOIN {badge_criteria} c + ON cp.critid = c.id + WHERE critid = :critid'; + $parameterparams = ['critid' => backup::VAR_PARENTID]; + $parameter->set_source_sql($parametersql, $parameterparams); + } $endorsement->set_source_table('badge_endorsement', array('badgeid' => backup::VAR_PARENTID)); $alignment->set_source_table('badge_alignment', array('badgeid' => backup::VAR_PARENTID)); $relatedbadge->set_source_table('badge_related', array('badgeid' => backup::VAR_PARENTID)); - $parametersql = 'SELECT cp.*, c.criteriatype - FROM {badge_criteria_param} cp JOIN {badge_criteria} c - ON cp.critid = c.id - WHERE critid = :critid'; - $parameterparams = array('critid' => backup::VAR_PARENTID); - $parameter->set_source_sql($parametersql, $parameterparams); - $manual_award->set_source_table('badge_manual_award', array('badgeid' => backup::VAR_PARENTID)); $tag->set_source_sql('SELECT t.id, t.name, t.rawname @@ -1015,8 +1014,10 @@ protected function define_structure() { $badge->annotate_ids('user', 'usercreated'); $badge->annotate_ids('user', 'usermodified'); - $criterion->annotate_ids('badge', 'badgeid'); - $parameter->annotate_ids('criterion', 'critid'); + if ($activitiesincluded) { + $criterion->annotate_ids('badge', 'badgeid'); + $parameter->annotate_ids('criterion', 'critid'); + } $endorsement->annotate_ids('badge', 'badgeid'); $alignment->annotate_ids('badge', 'badgeid'); $relatedbadge->annotate_ids('badge', 'badgeid'); diff --git a/backup/moodle2/tests/moodle2_test.php b/backup/moodle2/tests/moodle2_test.php index eaa3d78e0203c..940330ef471c9 100644 --- a/backup/moodle2/tests/moodle2_test.php +++ b/backup/moodle2/tests/moodle2_test.php @@ -36,7 +36,7 @@ * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class moodle2_test extends \advanced_testcase { +final class moodle2_test extends \advanced_testcase { /** * Tests the availability field on modules and sections is correctly @@ -1146,7 +1146,6 @@ public function test_xapistate_backup(): void { 'filearea' => 'package', 'itemid' => 0, 'filepath' => '/', - 'filepath' => '/', 'filename' => 'dummy.h5p', 'addxapistate' => true, ]; diff --git a/backup/util/helper/backup_helper.class.php b/backup/util/helper/backup_helper.class.php index 32d0b54f5addc..89ef8298f8ec1 100644 --- a/backup/util/helper/backup_helper.class.php +++ b/backup/util/helper/backup_helper.class.php @@ -433,7 +433,7 @@ public static function print_coursereuse_selector(string $current): void { $options, ['class' => 'container-fluid tertiary-navigation full-width-bottom-border', 'id' => 'tertiary-navigation']); } else { - echo $OUTPUT->heading($current, 2, 'mb-3'); + echo $OUTPUT->heading(get_string($current), 2, 'mb-3'); } } } diff --git a/backup/util/ui/UPGRADING.md b/backup/util/ui/UPGRADING.md index 1bbf593c80546..d04ce4a0bff35 100644 --- a/backup/util/ui/UPGRADING.md +++ b/backup/util/ui/UPGRADING.md @@ -7,4 +7,9 @@ - Final deprecation and removal of core_backup\copy\copy in backup/util/ui/classes/copy.php. Please use copy_helper from backup/util/helper/copy_helper.class.php instead. For more information see [MDL-75022](https://tracker.moodle.org/browse/MDL-75022) +- Final deprecation of base_controller::get_copy(). Please use restore_controller::get_copy() instead. + For more information see [MDL-75025](https://tracker.moodle.org/browse/MDL-75025) +- Final deprecation of base_controller::set_copy(). Please use a restore controller for storing copy information instead. + + For more information see [MDL-75025](https://tracker.moodle.org/browse/MDL-75025) diff --git a/backup/util/ui/tests/behat/import_course.feature b/backup/util/ui/tests/behat/import_course.feature index c0f1a00f755ef..f0e6c8d5400ad 100644 --- a/backup/util/ui/tests/behat/import_course.feature +++ b/backup/util/ui/tests/behat/import_course.feature @@ -55,3 +55,25 @@ Feature: Import course's contents into another course | Initial | Include permission overrides | 0 | And I am on the "Course 2" "permissions" page Then I should see "Non-editing teacher (0)" + + Scenario: Import course badges to another course + Given I log in as "teacher1" + And the following "core_badges > Badges" exist: + | name | course | description | image | status | type | + | Published course badge | C1 | Badge description | badges/tests/behat/badge.png | active | 2 | + | Unpublished course badge | C1 | Badge description | badges/tests/behat/badge.png | 0 | 2 | + | Unpublished without criteria course badge | C1 | Badge description | badges/tests/behat/badge.png | 0 | 2 | + And the following "core_badges > Criterias" exist: + | badge | role | + | Published course badge | editingteacher | + | Unpublished course badge | editingteacher | + When I import "Course 1" course into "Course 2" course using this options: + | Settings | Include badges | 1 | + And I navigate to "Badges > Manage badges" in current page administration + Then I should see "Published course badge" + And I should see "Unpublished course badge" + And I should see "Unpublished without criteria course badge" + # Badges exist and the criteria have been restored too. + And I should not see "Criteria for this badge have not been set up yet" in the "Published course badge" "table_row" + And I should not see "Criteria for this badge have not been set up yet" in the "Unpublished course badge" "table_row" + And I should see "Criteria for this badge have not been set up yet" in the "Unpublished without criteria course badge" "table_row" diff --git a/backup/util/ui/tests/behat/restore_moodle2_courses.feature b/backup/util/ui/tests/behat/restore_moodle2_courses.feature index b5b93ef1b7544..62539cca0924a 100644 --- a/backup/util/ui/tests/behat/restore_moodle2_courses.feature +++ b/backup/util/ui/tests/behat/restore_moodle2_courses.feature @@ -255,3 +255,46 @@ Feature: Restore Moodle 2 course backups | Settings | Include permission overrides | 0 | Then I am on the "Course 1 copy 1" "permissions" page And I should see "Non-editing teacher (0)" + + @javascript @core_badges + Scenario Outline: Restore course badges + Given the following "core_badges > Badges" exist: + | name | course | description | image | status | type | + | Published course badge | C1 | Badge description | badges/tests/behat/badge.png | active | 2 | + | Unpublished course badge | C1 | Badge description | badges/tests/behat/badge.png | 0 | 2 | + | Unpublished without criteria course badge | C1 | Badge description | badges/tests/behat/badge.png | 0 | 2 | + And the following "core_badges > Criterias" exist: + | badge | role | + | Published course badge | editingteacher | + | Unpublished course badge | editingteacher | + And I backup "Course 1" course using this options: + | Initial | Include badges | 1 | + | Initial | Include activities and resources | | + | Initial | Include enrolled users | 0 | + | Initial | Include blocks | 0 | + | Initial | Include files | 0 | + | Initial | Include filters | 0 | + | Initial | Include calendar events | 0 | + | Initial | Include question bank | 0 | + | Initial | Include groups and groupings | 0 | + | Initial | Include competencies | 0 | + | Initial | Include custom fields | 0 | + | Initial | Include calendar events | 0 | + | Initial | Include content bank content | 0 | + | Initial | Include legacy course files | 0 | + | Confirmation | Filename | test_backup.mbz | + When I restore "test_backup.mbz" backup into a new course using this options: + | Settings | Include badges | 1 | + And I navigate to "Badges > Manage badges" in current page administration + Then I should see "Published course badge" + And I should see "Unpublished course badge" + And I should see "Unpublished without criteria course badge" + # If activities were included, the criteria have been restored too; otherwise no criteria have been set up for badges. + And I "Criteria for this badge have not been set up yet" in the "Published course badge" "table_row" + And I "Criteria for this badge have not been set up yet" in the "Unpublished course badge" "table_row" + And I should see "Criteria for this badge have not been set up yet" in the "Unpublished without criteria course badge" "table_row" + + Examples: + | includeactivities | shouldornotsee | + | 0 | should see | + | 1 | should not see | diff --git a/badges/UPGRADING.md b/badges/UPGRADING.md new file mode 100644 index 0000000000000..7d075b1c09482 --- /dev/null +++ b/badges/UPGRADING.md @@ -0,0 +1,24 @@ +# core_badges (subsystem) Upgrade notes + +## 4.5dev + +### Deprecated + +- The badges/newbadge.php page has been deprecated and merged with badges/edit.php. Please, use badges/edit.php instead. + + For more information see [MDL-43938](https://tracker.moodle.org/browse/MDL-43938) +- OPEN_BADGES_V1 is deprecated and should not be used anymore. + + For more information see [MDL-70983](https://tracker.moodle.org/browse/MDL-70983) + +### Removed + +- Final removal of BADGE_BACKPACKAPIURL and BADGE_BACKPACKWEBURL. + + For more information see [MDL-70983](https://tracker.moodle.org/browse/MDL-70983) + +### Added + +- New webservices enable_badges and disable_badges have been added. + + For more information see [MDL-82168](https://tracker.moodle.org/browse/MDL-82168) diff --git a/badges/action.php b/badges/action.php index c2a152db32043..2e6eda4cb0bd3 100644 --- a/badges/action.php +++ b/badges/action.php @@ -29,9 +29,6 @@ $badgeid = required_param('id', PARAM_INT); $copy = optional_param('copy', 0, PARAM_BOOL); -$activate = optional_param('activate', 0, PARAM_BOOL); -$deactivate = optional_param('lock', 0, PARAM_BOOL); -$confirm = optional_param('confirm', 0, PARAM_BOOL); $return = optional_param('return', 0, PARAM_LOCALURL); require_login(); @@ -74,64 +71,3 @@ } redirect(new moodle_url('/badges/overview.php', array('id' => $cloneid))); } - -if ($activate) { - require_capability('moodle/badges:configurecriteria', $context); - - $PAGE->url->param('activate', 1); - $status = ($badge->status == BADGE_STATUS_INACTIVE) ? BADGE_STATUS_ACTIVE : BADGE_STATUS_ACTIVE_LOCKED; - if ($confirm == 1) { - require_sesskey(); - $badge->set_status($status); - $returnurl->param('msg', 'activatesuccess'); - - if ($badge->type == BADGE_TYPE_SITE) { - // Review on cron if there are more than 1000 users who can earn a site-level badge. - $sql = 'SELECT COUNT(u.id) as num - FROM {user} u - LEFT JOIN {badge_issued} bi - ON u.id = bi.userid AND bi.badgeid = :badgeid - WHERE bi.badgeid IS NULL AND u.id != :guestid AND u.deleted = 0'; - $toearn = $DB->get_record_sql($sql, array('badgeid' => $badge->id, 'guestid' => $CFG->siteguest)); - - if ($toearn->num < 1000) { - $awards = $badge->review_all_criteria(); - $returnurl->param('awards', $awards); - } else { - $returnurl->param('awards', 'cron'); - } - } else { - $awards = $badge->review_all_criteria(); - $returnurl->param('awards', $awards); - } - redirect($returnurl); - } - - $strheading = get_string('reviewbadge', 'badges'); - $PAGE->navbar->add($strheading); - $PAGE->set_title($strheading); - $PAGE->set_heading($heading); - echo $OUTPUT->header(); - echo $OUTPUT->heading($strheading); - - $params = array('id' => $badge->id, 'activate' => 1, 'sesskey' => sesskey(), 'confirm' => 1, 'return' => $return); - $url = new moodle_url('/badges/action.php', $params); - - if (!$badge->has_criteria()) { - redirect($returnurl, get_string('error:cannotact', 'badges') . get_string('nocriteria', 'badges'), null, \core\output\notification::NOTIFY_ERROR); - } else { - $message = get_string('reviewconfirm', 'badges', $badge->name); - echo $OUTPUT->confirm($message, $url, $returnurl); - } - echo $OUTPUT->footer(); - die; -} - -if ($deactivate) { - require_sesskey(); - require_capability('moodle/badges:configurecriteria', $context); - - $status = ($badge->status == BADGE_STATUS_ACTIVE) ? BADGE_STATUS_INACTIVE : BADGE_STATUS_INACTIVE_LOCKED; - $badge->set_status($status); - redirect($returnurl); -} diff --git a/badges/amd/build/actions.min.js b/badges/amd/build/actions.min.js new file mode 100644 index 0000000000000..4f9e98c3012cc --- /dev/null +++ b/badges/amd/build/actions.min.js @@ -0,0 +1,10 @@ +define("core_badges/actions",["exports","core_badges/selectors","core/notification","core/prefetch","core/str","core/ajax","core/pending","core/event_dispatcher","core/toast","core_reportbuilder/local/events","core_reportbuilder/local/selectors"],(function(_exports,_selectors,_notification,_prefetch,_str,_ajax,_pending,_event_dispatcher,_toast,reportEvents,reportSelectors){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +/** + * Various actions on badges - enabling, disabling, etc. + * + * @module core_badges/actions + * @copyright 2024 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_selectors=_interopRequireDefault(_selectors),_notification=_interopRequireDefault(_notification),_ajax=_interopRequireDefault(_ajax),_pending=_interopRequireDefault(_pending),reportEvents=_interopRequireWildcard(reportEvents),reportSelectors=_interopRequireWildcard(reportSelectors);_exports.init=()=>{(0,_prefetch.prefetchStrings)("core_badges",["reviewconfirm","activatesuccess","deactivatesuccess","awardoncron","numawardstat"]),(0,_prefetch.prefetchStrings)("core",["confirm","enable"]),registerEventListeners()};const registerEventListeners=()=>{document.addEventListener("click",(event=>{const enableOption=event.target.closest(_selectors.default.actions.enablebadge);if(enableOption){event.preventDefault();const reportElement=event.target.closest(reportSelectors.regions.report),triggerElement=reportElement?enableOption.closest(".dropdown").querySelector(".dropdown-toggle"):null,badgeId=enableOption.dataset.badgeid,badgeName=enableOption.dataset.badgename;_notification.default.saveCancelPromise((0,_str.getString)("confirm","core"),(0,_str.getString)("reviewconfirm","core_badges",badgeName),(0,_str.getString)("enable","core"),{triggerElement:triggerElement}).then((()=>async function(badgeId,badgeName,reportElement){var request={methodname:"core_badges_enable_badges",args:{badgeids:[badgeId]}};const pendingPromise=new _pending.default("core_badges/enable");try{const result=await _ajax.default.call([request])[0];if(reportElement)!function(badgeName,result){var _result$result2;if((null===(_result$result2=result.result)||void 0===_result$result2?void 0:_result$result2.length)>0){var _result$result3;(0,_toast.add)((0,_str.getString)("activatesuccess","core_badges",badgeName),{type:"success"});const awards=null===(_result$result3=result.result)||void 0===_result$result3?void 0:_result$result3.pop().awards;"cron"==awards?(0,_toast.add)((0,_str.getString)("awardoncron","core_badges",{badgename:badgeName})):awards>0&&(0,_toast.add)((0,_str.getString)("numawardstat","core_badges",{badgename:badgeName,awards:awards}))}else result.warnings.length>0&&(0,_toast.add)(result.warnings[0].message,{type:"danger"})}(badgeName,result),(0,_event_dispatcher.dispatchEvent)(reportEvents.tableReload,{preservePagination:!0},reportElement);else{var _result$result;const awards=null===(_result$result=result.result)||void 0===_result$result?void 0:_result$result.pop().awards;document.location=document.location.pathname+"?id=".concat(badgeId,"&awards=").concat(awards)}}catch(error){_notification.default.exception(error)}pendingPromise.resolve()}(badgeId,badgeName,reportElement))).catch((()=>{}))}const disableOption=event.target.closest(_selectors.default.actions.disablebadge);if(disableOption){event.preventDefault();!async function(badgeId,badgeName,reportElement){var request={methodname:"core_badges_disable_badges",args:{badgeids:[badgeId]}};try{const result=await _ajax.default.call([request])[0];reportElement?(!function(badgeName,result){result.result?(0,_toast.add)((0,_str.getString)("deactivatesuccess","core_badges",badgeName),{type:"success"}):result.warnings.length>0&&(0,_toast.add)(result.warnings[0].message,{type:"danger"})}(badgeName,result),(0,_event_dispatcher.dispatchEvent)(reportEvents.tableReload,{preservePagination:!0},reportElement)):document.location=document.location.pathname+"?id=".concat(badgeId)}catch(error){_notification.default.exception(error)}}(disableOption.dataset.badgeid,disableOption.dataset.badgename,event.target.closest(reportSelectors.regions.report))}}))}})); + +//# sourceMappingURL=actions.min.js.map \ No newline at end of file diff --git a/badges/amd/build/actions.min.js.map b/badges/amd/build/actions.min.js.map new file mode 100644 index 0000000000000..221b54306dbee --- /dev/null +++ b/badges/amd/build/actions.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"actions.min.js","sources":["../src/actions.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 * Various actions on badges - enabling, disabling, etc.\n *\n * @module core_badges/actions\n * @copyright 2024 Sara Arjona \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport selectors from 'core_badges/selectors';\nimport Notification from 'core/notification';\nimport {prefetchStrings} from 'core/prefetch';\nimport {getString} from 'core/str';\nimport Ajax from 'core/ajax';\nimport Pending from 'core/pending';\nimport {dispatchEvent} from 'core/event_dispatcher';\nimport {add as addToast} from 'core/toast';\nimport * as reportEvents from 'core_reportbuilder/local/events';\nimport * as reportSelectors from 'core_reportbuilder/local/selectors';\n\n/**\n * Initialize module.\n */\nexport const init = () => {\n prefetchStrings('core_badges', [\n 'reviewconfirm',\n 'activatesuccess',\n 'deactivatesuccess',\n 'awardoncron',\n 'numawardstat',\n ]);\n prefetchStrings('core', [\n 'confirm',\n 'enable',\n ]);\n\n registerEventListeners();\n};\n\n/**\n * Register events for delete preset option in action menu.\n */\nconst registerEventListeners = () => {\n document.addEventListener('click', (event) => {\n const enableOption = event.target.closest(selectors.actions.enablebadge);\n\n if (enableOption) {\n event.preventDefault();\n\n // Use triggerElement to return focus to the action menu toggle.\n const reportElement = event.target.closest(reportSelectors.regions.report);\n const triggerElement = reportElement ? enableOption.closest('.dropdown').querySelector('.dropdown-toggle') : null;\n const badgeId = enableOption.dataset.badgeid;\n const badgeName = enableOption.dataset.badgename;\n\n Notification.saveCancelPromise(\n getString('confirm', 'core'),\n getString('reviewconfirm', 'core_badges', badgeName),\n getString('enable', 'core'),\n {triggerElement}\n ).then(() => {\n return enableBadge(badgeId, badgeName, reportElement);\n }).catch(() => {\n return;\n });\n }\n\n const disableOption = event.target.closest(selectors.actions.disablebadge);\n if (disableOption) {\n event.preventDefault();\n const badgeId = disableOption.dataset.badgeid;\n const badgeName = disableOption.dataset.badgename;\n const reportElement = event.target.closest(reportSelectors.regions.report);\n disableBadge(badgeId, badgeName, reportElement);\n }\n });\n};\n\n/**\n * Enable the badge.\n *\n * @param {Number} badgeId The id of the badge to enable.\n * @param {String} badgeName The name of the badge to enable.\n * @param {HTMLElement} reportElement the report element.\n */\nasync function enableBadge(badgeId, badgeName, reportElement) {\n var request = {\n methodname: 'core_badges_enable_badges',\n args: {\n badgeids: [badgeId],\n }\n };\n\n const pendingPromise = new Pending('core_badges/enable');\n try {\n const result = await Ajax.call([request])[0];\n if (reportElement) {\n showEnableResultToast(badgeName, result);\n // Report element is present, reload the table.\n dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement);\n } else {\n // Report element is not present, add the parameters to the current page to display the message.\n const awards = result.result?.pop().awards;\n document.location = document.location.pathname + `?id=${badgeId}&awards=${awards}`;\n }\n } catch (error) {\n Notification.exception(error);\n }\n pendingPromise.resolve();\n}\n\n/**\n * Show the result of enabling a badge.\n *\n * @param {String} badgeName The name of the badge to enable.\n * @param {Object} result The result of enabling a badge.\n */\nfunction showEnableResultToast(badgeName, result) {\n if (result.result?.length > 0) {\n addToast(getString('activatesuccess', 'core_badges', badgeName), {type: 'success'});\n const awards = result.result?.pop().awards;\n if (awards == 'cron') {\n addToast(getString('awardoncron', 'core_badges', {badgename: badgeName}));\n } else if (awards > 0) {\n addToast(getString('numawardstat', 'core_badges', {badgename: badgeName, awards: awards}));\n }\n } else if (result.warnings.length > 0) {\n addToast(result.warnings[0].message, {type: 'danger'});\n }\n}\n\n/**\n * Disable the badge.\n *\n * @param {Number} badgeId The id of the badge to disable.\n * @param {String} badgeName The name of the badge to enable.\n * @param {HTMLElement} reportElement the report element.\n */\nasync function disableBadge(badgeId, badgeName, reportElement) {\n var request = {\n methodname: 'core_badges_disable_badges',\n args: {\n badgeids: [badgeId],\n }\n };\n\n try {\n const result = await Ajax.call([request])[0];\n if (reportElement) {\n // Report element is present, show the message in a toast and reload the table.\n showDisableResultToast(badgeName, result);\n dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement);\n } else {\n // Report element is not present, the page should be reloaded.\n document.location = document.location.pathname + `?id=${badgeId}`;\n }\n } catch (error) {\n Notification.exception(error);\n }\n}\n\n/**\n * Show the result of disabling a badge.\n *\n * @param {String} badgeName The name of the badge to disable.\n * @param {Object} result The result of disabling a badge.\n */\nfunction showDisableResultToast(badgeName, result) {\n if (result.result) {\n addToast(\n getString('deactivatesuccess', 'core_badges', badgeName),\n {type: 'success'}\n );\n } else if (result.warnings.length > 0) {\n addToast(\n result.warnings[0].message,\n {type: 'danger'}\n );\n }\n}\n"],"names":["registerEventListeners","document","addEventListener","event","enableOption","target","closest","selectors","actions","enablebadge","preventDefault","reportElement","reportSelectors","regions","report","triggerElement","querySelector","badgeId","dataset","badgeid","badgeName","badgename","saveCancelPromise","then","request","methodname","args","badgeids","pendingPromise","Pending","result","Ajax","call","length","type","awards","_result$result3","pop","warnings","message","showEnableResultToast","reportEvents","tableReload","preservePagination","_result$result","location","pathname","error","exception","resolve","enableBadge","catch","disableOption","disablebadge","showDisableResultToast","disableBadge"],"mappings":";;;;;;;4XAqCoB,mCACA,cAAe,CAC3B,gBACA,kBACA,oBACA,cACA,+CAEY,OAAQ,CACpB,UACA,WAGJA,gCAMEA,uBAAyB,KAC3BC,SAASC,iBAAiB,SAAUC,cAC1BC,aAAeD,MAAME,OAAOC,QAAQC,mBAAUC,QAAQC,gBAExDL,aAAc,CACdD,MAAMO,uBAGAC,cAAgBR,MAAME,OAAOC,QAAQM,gBAAgBC,QAAQC,QAC7DC,eAAiBJ,cAAgBP,aAAaE,QAAQ,aAAaU,cAAc,oBAAsB,KACvGC,QAAUb,aAAac,QAAQC,QAC/BC,UAAYhB,aAAac,QAAQG,gCAE1BC,mBACT,kBAAU,UAAW,SACrB,kBAAU,gBAAiB,cAAeF,YAC1C,kBAAU,SAAU,QACpB,CAACL,eAAAA,iBACHQ,MAAK,mBAyBQN,QAASG,UAAWT,mBACvCa,QAAU,CACVC,WAAY,4BACZC,KAAM,CACFC,SAAU,CAACV,iBAIbW,eAAiB,IAAIC,iBAAQ,gCAEzBC,aAAeC,cAAKC,KAAK,CAACR,UAAU,MACtCb,wBAqBmBS,UAAWU,wDAClCA,OAAOA,yDAAQG,QAAS,EAAG,qCAClB,kBAAU,kBAAmB,cAAeb,WAAY,CAACc,KAAM,kBAClEC,+BAASL,OAAOA,yCAAPM,gBAAeC,MAAMF,OACtB,QAAVA,uBACS,kBAAU,cAAe,cAAe,CAACd,UAAWD,aACtDe,OAAS,mBACP,kBAAU,eAAgB,cAAe,CAACd,UAAWD,UAAWe,OAAQA,eAE9EL,OAAOQ,SAASL,OAAS,kBACvBH,OAAOQ,SAAS,GAAGC,QAAS,CAACL,KAAM,WA9BxCM,CAAsBpB,UAAWU,4CAEnBW,aAAaC,YAAa,CAACC,oBAAoB,GAAOhC,mBACjE,0BAEGwB,8BAASL,OAAOA,wCAAPc,eAAeP,MAAMF,OACpClC,SAAS4C,SAAW5C,SAAS4C,SAASC,uBAAkB7B,2BAAkBkB,SAEhF,MAAOY,6BACQC,UAAUD,OAE3BnB,eAAeqB,UA/CIC,CAAYjC,QAASG,UAAWT,iBACxCwC,OAAM,eAKPC,cAAgBjD,MAAME,OAAOC,QAAQC,mBAAUC,QAAQ6C,iBACzDD,cAAe,CACfjD,MAAMO,iCAqEUO,QAASG,UAAWT,mBACxCa,QAAU,CACVC,WAAY,6BACZC,KAAM,CACFC,SAAU,CAACV,qBAKTa,aAAeC,cAAKC,KAAK,CAACR,UAAU,GACtCb,yBAmBoBS,UAAWU,QACnCA,OAAOA,uBAEH,kBAAU,oBAAqB,cAAeV,WAC9C,CAACc,KAAM,YAEJJ,OAAOQ,SAASL,OAAS,kBAE5BH,OAAOQ,SAAS,GAAGC,QACnB,CAACL,KAAM,WA1BPoB,CAAuBlC,UAAWU,4CACpBW,aAAaC,YAAa,CAACC,oBAAoB,GAAOhC,gBAGpEV,SAAS4C,SAAW5C,SAAS4C,SAASC,uBAAkB7B,SAE9D,MAAO8B,6BACQC,UAAUD,QApFnBQ,CAHgBH,cAAclC,QAAQC,QACpBiC,cAAclC,QAAQG,UAClBlB,MAAME,OAAOC,QAAQM,gBAAgBC,QAAQC"} \ No newline at end of file diff --git a/badges/amd/build/selectors.min.js b/badges/amd/build/selectors.min.js index 916edb19d8841..6651a0bd74594 100644 --- a/badges/amd/build/selectors.min.js +++ b/badges/amd/build/selectors.min.js @@ -1,3 +1,11 @@ -define("core_badges/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;var name,value,_default={actions:{deletebackpack:(name="action",value="deletebackpack","[data-".concat(name,'="').concat(value,'"]'))},elements:{clearsearch:".input-group-append .clear-icon",main:"#backpacklist",backpackurl:"[data-backpackurl]"}};return _exports.default=_default,_exports.default})); +define("core_badges/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0; +/** + * Define all of the selectors we will be using on the backpack interface. + * + * @module core_badges/selectors + * @copyright 2020 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +const getDataSelector=(name,value)=>"[data-".concat(name,'="').concat(value,'"]');var _default={actions:{deletebackpack:getDataSelector("action","deletebackpack"),enablebadge:getDataSelector("action","enablebadge"),disablebadge:getDataSelector("action","disablebadge")},elements:{clearsearch:".input-group-append .clear-icon",main:"#backpacklist",backpackurl:"[data-backpackurl]"}};return _exports.default=_default,_exports.default})); //# sourceMappingURL=selectors.min.js.map \ No newline at end of file diff --git a/badges/amd/build/selectors.min.js.map b/badges/amd/build/selectors.min.js.map index 36bcd53ad01d3..9b3f4c7903b2c 100644 --- a/badges/amd/build/selectors.min.js.map +++ b/badges/amd/build/selectors.min.js.map @@ -1 +1 @@ -{"version":3,"file":"selectors.min.js","sources":["../src/selectors.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 * Define all of the selectors we will be using on the backpack interface.\n *\n * @module core_badges/selectors\n * @copyright 2020 Sara Arjona \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * A small helper function to build queryable data selectors.\n *\n * @method getDataSelector\n * @param {String} name\n * @param {String} value\n * @return {string}\n */\nconst getDataSelector = (name, value) => {\n return `[data-${name}=\"${value}\"]`;\n};\n\nexport default {\n actions: {\n deletebackpack: getDataSelector('action', 'deletebackpack'),\n },\n elements: {\n clearsearch: '.input-group-append .clear-icon',\n main: '#backpacklist',\n backpackurl: '[data-backpackurl]',\n },\n};\n"],"names":["name","value","actions","deletebackpack","elements","clearsearch","main","backpackurl"],"mappings":"mJA+ByBA,KAAMC,eAIhB,CACXC,QAAS,CACLC,gBANiBH,KAMe,SANTC,MAMmB,iCAL9BD,kBAASC,cAOzBG,SAAU,CACNC,YAAa,kCACbC,KAAM,gBACNC,YAAa"} \ No newline at end of file +{"version":3,"file":"selectors.min.js","sources":["../src/selectors.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 * Define all of the selectors we will be using on the backpack interface.\n *\n * @module core_badges/selectors\n * @copyright 2020 Sara Arjona \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * A small helper function to build queryable data selectors.\n *\n * @method getDataSelector\n * @param {String} name\n * @param {String} value\n * @return {string}\n */\nconst getDataSelector = (name, value) => {\n return `[data-${name}=\"${value}\"]`;\n};\n\nexport default {\n actions: {\n deletebackpack: getDataSelector('action', 'deletebackpack'),\n enablebadge: getDataSelector('action', 'enablebadge'),\n disablebadge: getDataSelector('action', 'disablebadge'),\n },\n elements: {\n clearsearch: '.input-group-append .clear-icon',\n main: '#backpacklist',\n backpackurl: '[data-backpackurl]',\n },\n};\n"],"names":["getDataSelector","name","value","actions","deletebackpack","enablebadge","disablebadge","elements","clearsearch","main","backpackurl"],"mappings":";;;;;;;;MA+BMA,gBAAkB,CAACC,KAAMC,wBACXD,kBAASC,yBAGd,CACXC,QAAS,CACLC,eAAgBJ,gBAAgB,SAAU,kBAC1CK,YAAaL,gBAAgB,SAAU,eACvCM,aAAcN,gBAAgB,SAAU,iBAE5CO,SAAU,CACNC,YAAa,kCACbC,KAAM,gBACNC,YAAa"} \ No newline at end of file diff --git a/badges/amd/src/actions.js b/badges/amd/src/actions.js new file mode 100644 index 0000000000000..0cca5ddd17085 --- /dev/null +++ b/badges/amd/src/actions.js @@ -0,0 +1,194 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Various actions on badges - enabling, disabling, etc. + * + * @module core_badges/actions + * @copyright 2024 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import selectors from 'core_badges/selectors'; +import Notification from 'core/notification'; +import {prefetchStrings} from 'core/prefetch'; +import {getString} from 'core/str'; +import Ajax from 'core/ajax'; +import Pending from 'core/pending'; +import {dispatchEvent} from 'core/event_dispatcher'; +import {add as addToast} from 'core/toast'; +import * as reportEvents from 'core_reportbuilder/local/events'; +import * as reportSelectors from 'core_reportbuilder/local/selectors'; + +/** + * Initialize module. + */ +export const init = () => { + prefetchStrings('core_badges', [ + 'reviewconfirm', + 'activatesuccess', + 'deactivatesuccess', + 'awardoncron', + 'numawardstat', + ]); + prefetchStrings('core', [ + 'confirm', + 'enable', + ]); + + registerEventListeners(); +}; + +/** + * Register events for delete preset option in action menu. + */ +const registerEventListeners = () => { + document.addEventListener('click', (event) => { + const enableOption = event.target.closest(selectors.actions.enablebadge); + + if (enableOption) { + event.preventDefault(); + + // Use triggerElement to return focus to the action menu toggle. + const reportElement = event.target.closest(reportSelectors.regions.report); + const triggerElement = reportElement ? enableOption.closest('.dropdown').querySelector('.dropdown-toggle') : null; + const badgeId = enableOption.dataset.badgeid; + const badgeName = enableOption.dataset.badgename; + + Notification.saveCancelPromise( + getString('confirm', 'core'), + getString('reviewconfirm', 'core_badges', badgeName), + getString('enable', 'core'), + {triggerElement} + ).then(() => { + return enableBadge(badgeId, badgeName, reportElement); + }).catch(() => { + return; + }); + } + + const disableOption = event.target.closest(selectors.actions.disablebadge); + if (disableOption) { + event.preventDefault(); + const badgeId = disableOption.dataset.badgeid; + const badgeName = disableOption.dataset.badgename; + const reportElement = event.target.closest(reportSelectors.regions.report); + disableBadge(badgeId, badgeName, reportElement); + } + }); +}; + +/** + * Enable the badge. + * + * @param {Number} badgeId The id of the badge to enable. + * @param {String} badgeName The name of the badge to enable. + * @param {HTMLElement} reportElement the report element. + */ +async function enableBadge(badgeId, badgeName, reportElement) { + var request = { + methodname: 'core_badges_enable_badges', + args: { + badgeids: [badgeId], + } + }; + + const pendingPromise = new Pending('core_badges/enable'); + try { + const result = await Ajax.call([request])[0]; + if (reportElement) { + showEnableResultToast(badgeName, result); + // Report element is present, reload the table. + dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement); + } else { + // Report element is not present, add the parameters to the current page to display the message. + const awards = result.result?.pop().awards; + document.location = document.location.pathname + `?id=${badgeId}&awards=${awards}`; + } + } catch (error) { + Notification.exception(error); + } + pendingPromise.resolve(); +} + +/** + * Show the result of enabling a badge. + * + * @param {String} badgeName The name of the badge to enable. + * @param {Object} result The result of enabling a badge. + */ +function showEnableResultToast(badgeName, result) { + if (result.result?.length > 0) { + addToast(getString('activatesuccess', 'core_badges', badgeName), {type: 'success'}); + const awards = result.result?.pop().awards; + if (awards == 'cron') { + addToast(getString('awardoncron', 'core_badges', {badgename: badgeName})); + } else if (awards > 0) { + addToast(getString('numawardstat', 'core_badges', {badgename: badgeName, awards: awards})); + } + } else if (result.warnings.length > 0) { + addToast(result.warnings[0].message, {type: 'danger'}); + } +} + +/** + * Disable the badge. + * + * @param {Number} badgeId The id of the badge to disable. + * @param {String} badgeName The name of the badge to enable. + * @param {HTMLElement} reportElement the report element. + */ +async function disableBadge(badgeId, badgeName, reportElement) { + var request = { + methodname: 'core_badges_disable_badges', + args: { + badgeids: [badgeId], + } + }; + + try { + const result = await Ajax.call([request])[0]; + if (reportElement) { + // Report element is present, show the message in a toast and reload the table. + showDisableResultToast(badgeName, result); + dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement); + } else { + // Report element is not present, the page should be reloaded. + document.location = document.location.pathname + `?id=${badgeId}`; + } + } catch (error) { + Notification.exception(error); + } +} + +/** + * Show the result of disabling a badge. + * + * @param {String} badgeName The name of the badge to disable. + * @param {Object} result The result of disabling a badge. + */ +function showDisableResultToast(badgeName, result) { + if (result.result) { + addToast( + getString('deactivatesuccess', 'core_badges', badgeName), + {type: 'success'} + ); + } else if (result.warnings.length > 0) { + addToast( + result.warnings[0].message, + {type: 'danger'} + ); + } +} diff --git a/badges/amd/src/selectors.js b/badges/amd/src/selectors.js index a8b66a3461a82..b7a557a91cc93 100644 --- a/badges/amd/src/selectors.js +++ b/badges/amd/src/selectors.js @@ -36,6 +36,8 @@ const getDataSelector = (name, value) => { export default { actions: { deletebackpack: getDataSelector('action', 'deletebackpack'), + enablebadge: getDataSelector('action', 'enablebadge'), + disablebadge: getDataSelector('action', 'disablebadge'), }, elements: { clearsearch: '.input-group-append .clear-icon', diff --git a/badges/classes/assertion.php b/badges/classes/assertion.php index 9a54c1300c930..4021c9d1e91b5 100644 --- a/badges/classes/assertion.php +++ b/badges/classes/assertion.php @@ -14,22 +14,13 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Badge assertion library. - * - * @package core - * @subpackage badges - * @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @author Yuliya Bozhko - */ - defined('MOODLE_INTERNAL') || die(); /** - * Open Badges Assertions specification 1.0 {@link https://github.com/mozilla/openbadges-backpack/wiki/Assertions} + * Open Badges Assertions specification 2.0 + * {@link https://www.imsglobal.org/sites/default/files/Badges/OBv2p0Final/index.html#Assertion} * - * Badge asserion is defined by three parts: + * Badge assertion is defined by three parts: * - Badge Assertion (information regarding a specific badge that was awarded to a badge earner) * - Badge Class (general information about a badge and what it is intended to represent) * - Issuer Class (general information of an issuing organisation) @@ -40,6 +31,10 @@ /** * Class that represents badge assertion. * + * @package core_badges + * @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Yuliya Bozhko */ class core_badges_assertion { /** @var object Issued badge information from database */ @@ -205,7 +200,7 @@ public function get_badge_class($issued = true) { $badgeurl = new moodle_url('/badges/badgeclass.php', $params); $class['criteria'] = $badgeurl->out(false); // Currently badge URL. if ($issued) { - $params = ['id' => $this->get_badge_id(), 'obversion' => $this->_obversion]; + $params = ['id' => $this->get_badge_id()]; $issuerurl = new moodle_url('/badges/issuer_json.php', $params); $class['issuer'] = $issuerurl->out(false); } @@ -227,24 +222,9 @@ public function get_badge_class($issued = true) { * @return array Issuer information. */ public function get_issuer() { - global $CFG; - $issuer = array(); - if ($this->_data) { - // Required. - if ($this->_obversion == OPEN_BADGES_V1) { - $issuer['name'] = $this->_data->issuername; - $issuer['url'] = $this->_data->issuerurl; - // Optional. - if (!empty($this->_data->issuercontact)) { - $issuer['email'] = $this->_data->issuercontact; - } else { - $issuer['email'] = $CFG->badges_defaultissuercontact; - } - } else { - $badge = new badge($this->get_badge_id()); - $issuer = $badge->get_badge_issuer(); - } - } + $badge = new badge($this->get_badge_id()); + $issuer = $badge->get_badge_issuer(); + $this->embed_data_badge_version2($issuer, OPEN_BADGES_V2_TYPE_ISSUER); return $issuer; } diff --git a/badges/classes/backpack_api.php b/badges/classes/backpack_api.php index 08e5ba3399792..1b6901235c1e4 100644 --- a/badges/classes/backpack_api.php +++ b/badges/classes/backpack_api.php @@ -14,14 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Communicate with backpacks. - * - * @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @author Yuliya Bozhko - */ - namespace core_badges; defined('MOODLE_INTERNAL') || die(); @@ -30,11 +22,8 @@ use cache; use coding_exception; -use core_badges\external\assertion_exporter; -use core_badges\external\collection_exporter; use core_badges\external\issuer_exporter; use core_badges\external\badgeclass_exporter; -use curl; use stdClass; use context_system; @@ -47,7 +36,8 @@ /** * Class for communicating with backpacks. * - * @package core_badges + * @package core_badges + * @author Yuliya Bozhko * @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -71,7 +61,7 @@ class backpack_api { /** @var integer The id of the backpack we are talking to. */ private $backpackid; - /** @var \backpack_api_mapping[] List of apis for the user or site using api version 1 or 2. */ + /** @var \core_badges\backpack_api_mapping[] List of apis for the user or site using api version 2. */ private $mappings = []; /** @@ -108,199 +98,137 @@ public function __construct($sitebackpack, $userbackpack = false) { * Define the mappings supported by this usage and api version. */ private function define_mappings() { - if ($this->backpackapiversion == OPEN_BADGES_V2) { - if ($this->isuserbackpack) { - $mapping = []; - $mapping[] = [ - 'collections', // Action. - '[URL]/backpack/collections', // URL - [], // Post params. - '', // Request exporter. - 'core_badges\external\collection_exporter', // Response exporter. - true, // Multiple. - 'get', // Method. - true, // JSON Encoded. - true // Auth required. - ]; - $mapping[] = [ - 'user', // Action. - '[SCHEME]://[HOST]/o/token', // URL - ['username' => '[EMAIL]', 'password' => '[PASSWORD]'], // Post params. - '', // Request exporter. - 'oauth_token_response', // Response exporter. - false, // Multiple. - 'post', // Method. - false, // JSON Encoded. - false, // Auth required. - ]; - $mapping[] = [ - 'assertion', // Action. - // Badgr.io does not return the public information about a badge - // if the issuer is associated with another user. We need to pass - // the expand parameters which are not in any specification to get - // additional information about the assertion in a single request. - '[URL]/backpack/assertions/[PARAM2]?expand=badgeclass&expand=issuer', - [], // Post params. - '', // Request exporter. - 'core_badges\external\assertion_exporter', // Response exporter. - false, // Multiple. - 'get', // Method. - true, // JSON Encoded. - true // Auth required. - ]; - $mapping[] = [ - 'importbadge', // Action. - // Badgr.io does not return the public information about a badge - // if the issuer is associated with another user. We need to pass - // the expand parameters which are not in any specification to get - // additional information about the assertion in a single request. - '[URL]/backpack/import', - ['url' => '[PARAM]'], // Post params. - '', // Request exporter. - 'core_badges\external\assertion_exporter', // Response exporter. - false, // Multiple. - 'post', // Method. - true, // JSON Encoded. - true // Auth required. - ]; - $mapping[] = [ - 'badges', // Action. - '[URL]/backpack/collections/[PARAM1]', // URL - [], // Post params. - '', // Request exporter. - 'core_badges\external\collection_exporter', // Response exporter. - true, // Multiple. - 'get', // Method. - true, // JSON Encoded. - true // Auth required. - ]; - foreach ($mapping as $map) { - $map[] = true; // User api function. - $map[] = OPEN_BADGES_V2; // V2 function. - $this->mappings[] = new backpack_api_mapping(...$map); - } - } else { - $mapping = []; - $mapping[] = [ - 'user', // Action. - '[SCHEME]://[HOST]/o/token', // URL - ['username' => '[EMAIL]', 'password' => '[PASSWORD]'], // Post params. - '', // Request exporter. - 'oauth_token_response', // Response exporter. - false, // Multiple. - 'post', // Method. - false, // JSON Encoded. - false // Auth required. - ]; - $mapping[] = [ - 'issuers', // Action. - '[URL]/issuers', // URL - '[PARAM]', // Post params. - 'core_badges\external\issuer_exporter', // Request exporter. - 'core_badges\external\issuer_exporter', // Response exporter. - false, // Multiple. - 'post', // Method. - true, // JSON Encoded. - true // Auth required. - ]; - $mapping[] = [ - 'badgeclasses', // Action. - '[URL]/issuers/[PARAM2]/badgeclasses', // URL - '[PARAM]', // Post params. - 'core_badges\external\badgeclass_exporter', // Request exporter. - 'core_badges\external\badgeclass_exporter', // Response exporter. - false, // Multiple. - 'post', // Method. - true, // JSON Encoded. - true // Auth required. - ]; - $mapping[] = [ - 'assertions', // Action. - '[URL]/badgeclasses/[PARAM2]/assertions', // URL - '[PARAM]', // Post params. - 'core_badges\external\assertion_exporter', // Request exporter. - 'core_badges\external\assertion_exporter', // Response exporter. - false, // Multiple. - 'post', // Method. - true, // JSON Encoded. - true // Auth required. - ]; - $mapping[] = [ - 'updateassertion', // Action. - '[URL]/assertions/[PARAM2]?expand=badgeclass&expand=issuer', - '[PARAM]', // Post params. - 'core_badges\external\assertion_exporter', // Request exporter. - 'core_badges\external\assertion_exporter', // Response exporter. - false, // Multiple. - 'put', // Method. - true, // JSON Encoded. - true // Auth required. - ]; - foreach ($mapping as $map) { - $map[] = false; // Site api function. - $map[] = OPEN_BADGES_V2; // V2 function. - $this->mappings[] = new backpack_api_mapping(...$map); - } + if ($this->isuserbackpack) { + $mapping = []; + $mapping[] = [ + 'collections', // Action. + '[URL]/backpack/collections', // URL. + [], // Post params. + '', // Request exporter. + 'core_badges\external\collection_exporter', // Response exporter. + true, // Multiple. + 'get', // Method. + true, // JSON Encoded. + true, // Auth required. + ]; + $mapping[] = [ + 'user', // Action. + '[SCHEME]://[HOST]/o/token', // URL. + ['username' => '[EMAIL]', 'password' => '[PASSWORD]'], // Post params. + '', // Request exporter. + 'oauth_token_response', // Response exporter. + false, // Multiple. + 'post', // Method. + false, // JSON Encoded. + false, // Auth required. + ]; + $mapping[] = [ + 'assertion', // Action. + // Badgr.io does not return the public information about a badge + // if the issuer is associated with another user. We need to pass + // the expand parameters which are not in any specification to get + // additional information about the assertion in a single request. + '[URL]/backpack/assertions/[PARAM2]?expand=badgeclass&expand=issuer', + [], // Post params. + '', // Request exporter. + 'core_badges\external\assertion_exporter', // Response exporter. + false, // Multiple. + 'get', // Method. + true, // JSON Encoded. + true, // Auth required. + ]; + $mapping[] = [ + 'importbadge', // Action. + // Badgr.io does not return the public information about a badge + // if the issuer is associated with another user. We need to pass + // the expand parameters which are not in any specification to get + // additional information about the assertion in a single request. + '[URL]/backpack/import', + ['url' => '[PARAM]'], // Post params. + '', // Request exporter. + 'core_badges\external\assertion_exporter', // Response exporter. + false, // Multiple. + 'post', // Method. + true, // JSON Encoded. + true, // Auth required. + ]; + $mapping[] = [ + 'badges', // Action. + '[URL]/backpack/collections/[PARAM1]', // URL. + [], // Post params. + '', // Request exporter. + 'core_badges\external\collection_exporter', // Response exporter. + true, // Multiple. + 'get', // Method. + true, // JSON Encoded. + true, // Auth required. + ]; + foreach ($mapping as $map) { + $map[] = true; // User api function. + $map[] = OPEN_BADGES_V2; // V2 function. + $this->mappings[] = new backpack_api_mapping(...$map); } } else { - if ($this->isuserbackpack) { - $mapping = []; - $mapping[] = [ - 'user', // Action. - '[URL]/displayer/convert/email', // URL - ['email' => '[EMAIL]'], // Post params. - '', // Request exporter. - 'convert_email_response', // Response exporter. - false, // Multiple. - 'post', // Method. - false, // JSON Encoded. - false // Auth required. - ]; - $mapping[] = [ - 'groups', // Action. - '[URL]/displayer/[PARAM1]/groups.json', // URL - [], // Post params. - '', // Request exporter. - '', // Response exporter. - false, // Multiple. - 'get', // Method. - true, // JSON Encoded. - true // Auth required. - ]; - $mapping[] = [ - 'badges', // Action. - '[URL]/displayer/[PARAM2]/group/[PARAM1].json', // URL - [], // Post params. - '', // Request exporter. - '', // Response exporter. - false, // Multiple. - 'get', // Method. - true, // JSON Encoded. - true // Auth required. - ]; - foreach ($mapping as $map) { - $map[] = true; // User api function. - $map[] = OPEN_BADGES_V1; // V1 function. - $this->mappings[] = new backpack_api_mapping(...$map); - } - } else { - $mapping = []; - $mapping[] = [ - 'user', // Action. - '[URL]/displayer/convert/email', // URL - ['email' => '[EMAIL]'], // Post params. - '', // Request exporter. - 'convert_email_response', // Response exporter. - false, // Multiple. - 'post', // Method. - false, // JSON Encoded. - false // Auth required. - ]; - foreach ($mapping as $map) { - $map[] = false; // Site api function. - $map[] = OPEN_BADGES_V1; // V1 function. - $this->mappings[] = new backpack_api_mapping(...$map); - } + $mapping = []; + $mapping[] = [ + 'user', // Action. + '[SCHEME]://[HOST]/o/token', // URL. + ['username' => '[EMAIL]', 'password' => '[PASSWORD]'], // Post params. + '', // Request exporter. + 'oauth_token_response', // Response exporter. + false, // Multiple. + 'post', // Method. + false, // JSON Encoded. + false, // Auth required. + ]; + $mapping[] = [ + 'issuers', // Action. + '[URL]/issuers', // URL. + '[PARAM]', // Post params. + 'core_badges\external\issuer_exporter', // Request exporter. + 'core_badges\external\issuer_exporter', // Response exporter. + false, // Multiple. + 'post', // Method. + true, // JSON Encoded. + true, // Auth required. + ]; + $mapping[] = [ + 'badgeclasses', // Action. + '[URL]/issuers/[PARAM2]/badgeclasses', // URL. + '[PARAM]', // Post params. + 'core_badges\external\badgeclass_exporter', // Request exporter. + 'core_badges\external\badgeclass_exporter', // Response exporter. + false, // Multiple. + 'post', // Method. + true, // JSON Encoded. + true, // Auth required. + ]; + $mapping[] = [ + 'assertions', // Action. + '[URL]/badgeclasses/[PARAM2]/assertions', // URL. + '[PARAM]', // Post params. + 'core_badges\external\assertion_exporter', // Request exporter. + 'core_badges\external\assertion_exporter', // Response exporter. + false, // Multiple. + 'post', // Method. + true, // JSON Encoded. + true, // Auth required. + ]; + $mapping[] = [ + 'updateassertion', // Action. + '[URL]/assertions/[PARAM2]?expand=badgeclass&expand=issuer', + '[PARAM]', // Post params. + 'core_badges\external\assertion_exporter', // Request exporter. + 'core_badges\external\assertion_exporter', // Response exporter. + false, // Multiple. + 'put', // Method. + true, // JSON Encoded. + true, // Auth required. + ]; + foreach ($mapping as $map) { + $map[] = false; // Site api function. + $map[] = OPEN_BADGES_V2; // V2 function. + $this->mappings[] = new backpack_api_mapping(...$map); } } } @@ -315,20 +243,6 @@ private function define_mappings() { * @return mixed */ private function curl_request($action, $collection = null, $entityid = null, $postdata = null) { - global $CFG, $SESSION; - - $curl = new curl(); - $authrequired = false; - if ($this->backpackapiversion == OPEN_BADGES_V1) { - $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN); - if (isset($SESSION->$useridkey)) { - if ($collection == null) { - $collection = $SESSION->$useridkey; - } else { - $entityid = $SESSION->$useridkey; - } - } - } foreach ($this->mappings as $mapping) { if ($mapping->is_match($action)) { return $mapping->request( @@ -380,25 +294,6 @@ private function get_token_key($type) { return $prefix; } - /** - * Normalise the return from a missing user request. - * - * @param string $status - * @return mixed - */ - private function check_status($status) { - // V1 ONLY. - switch($status) { - case "missing": - $response = array( - 'status' => $status, - 'message' => get_string('error:nosuchuser', 'badges') - ); - return $response; - } - return false; - } - /** * Make an api request to get an assertion * @@ -406,11 +301,6 @@ private function check_status($status) { * @return mixed */ public function get_assertion($entityid) { - // V2 Only. - if ($this->backpackapiversion == OPEN_BADGES_V1) { - throw new coding_exception('Not supported in this backpack API'); - } - return $this->curl_request('assertion', null, $entityid); } @@ -422,11 +312,6 @@ public function get_assertion($entityid) { * @return mixed */ public function put_badgeclass_assertion($entityid, $data) { - // V2 Only. - if ($this->backpackapiversion == OPEN_BADGES_V1) { - throw new coding_exception('Not supported in this backpack API'); - } - return $this->curl_request('assertions', null, $entityid, $data); } @@ -438,11 +323,6 @@ public function put_badgeclass_assertion($entityid, $data) { * @return mixed */ public function update_assertion(string $entityid, array $data) { - // V2 Only. - if ($this->backpackapiversion == OPEN_BADGES_V1) { - throw new coding_exception('Not supported in this backpack API'); - } - return $this->curl_request('updateassertion', null, $entityid, $data); } @@ -454,11 +334,6 @@ public function update_assertion(string $entityid, array $data) { * @throws coding_exception */ public function import_badge_assertion(string $data) { - // V2 Only. - if ($this->backpackapiversion == OPEN_BADGES_V1) { - throw new coding_exception('Not supported in this backpack API'); - } - return $this->curl_request('importbadge', null, null, $data); } @@ -482,12 +357,8 @@ public function set_backpack_collections($backpackid, $collections) { foreach ($collections as $collection) { $obj = new stdClass(); $obj->backpackid = $backpackid; - if ($this->backpackapiversion == OPEN_BADGES_V1) { - $obj->collectionid = (int) $collection; - } else { - $obj->entityid = $collection; - $obj->collectionid = -1; - } + $obj->entityid = $collection; + $obj->collectionid = -1; if (!$DB->record_exists('badge_external', (array) $obj)) { $DB->insert_record('badge_external', $obj); } @@ -504,11 +375,6 @@ public function set_backpack_collections($backpackid, $collections) { * @return mixed */ public function put_badgeclass($entityid, $data) { - // V2 Only. - if ($this->backpackapiversion == OPEN_BADGES_V1) { - throw new coding_exception('Not supported in this backpack API'); - } - return $this->curl_request('badgeclasses', null, $entityid, $data); } @@ -519,11 +385,6 @@ public function put_badgeclass($entityid, $data) { * @return mixed */ public function put_issuer($data) { - // V2 Only. - if ($this->backpackapiversion == OPEN_BADGES_V1) { - throw new coding_exception('Not supported in this backpack API'); - } - return $this->curl_request('issuers', null, null, $data); } @@ -586,17 +447,8 @@ public function authenticate() { * @return stdClass[] The collections. */ public function get_collections() { - global $PAGE; - if ($this->authenticate()) { - if ($this->backpackapiversion == OPEN_BADGES_V1) { - $result = $this->curl_request('groups'); - if (isset($result->groups)) { - $result = $result->groups; - } - } else { - $result = $this->curl_request('collections'); - } + $result = $this->curl_request('collections'); if ($result) { return $result; } @@ -608,16 +460,12 @@ public function get_collections() { * Get one collection by id. * * @param integer $collectionid - * @return stdClass The collection. + * @return array The collection. */ public function get_collection_record($collectionid) { global $DB; - if ($this->backpackapiversion == OPEN_BADGES_V1) { - return $DB->get_fieldset_select('badge_external', 'collectionid', 'backpackid = :bid', array('bid' => $collectionid)); - } else { - return $DB->get_fieldset_select('badge_external', 'entityid', 'backpackid = :bid', array('bid' => $collectionid)); - } + return $DB->get_fieldset_select('badge_external', 'entityid', 'backpackid = :bid', ['bid' => $collectionid]); } /** @@ -652,11 +500,7 @@ public function disconnect_backpack($userid, $backpackid) { * @return string The collection id. */ public function get_collection_id_from_response($data) { - if ($this->backpackapiversion == OPEN_BADGES_V1) { - return $data->groupId; - } else { - return $data->entityId; - } + return $data->entityId; } /** @@ -679,52 +523,46 @@ public function get_badges($collection, $expanded = false) { global $PAGE; if ($this->authenticate()) { - if ($this->backpackapiversion == OPEN_BADGES_V1) { - if (empty($collection->collectionid)) { - return []; - } - $result = $this->curl_request('badges', $collection->collectionid); - return $result->badges; - } else { - if (empty($collection->entityid)) { - return []; - } - // Now we can make requests. - $badges = $this->curl_request('badges', $collection->entityid); - if (count($badges) == 0) { - return []; - } - $badges = $badges[0]; - if ($expanded) { - $publicassertions = []; - $context = context_system::instance(); - $output = $PAGE->get_renderer('core', 'badges'); - foreach ($badges->assertions as $assertion) { - $remoteassertion = $this->get_assertion($assertion); - // Remote badge was fetched nested in the assertion. - $remotebadge = $remoteassertion->badgeclass; - if (!$remotebadge) { - continue; - } - $apidata = badgeclass_exporter::map_external_data($remotebadge, $this->backpackapiversion); - $exporterinstance = new badgeclass_exporter($apidata, ['context' => $context]); - $remotebadge = $exporterinstance->export($output); - - $remoteissuer = $remotebadge->issuer; - $apidata = issuer_exporter::map_external_data($remoteissuer, $this->backpackapiversion); - $exporterinstance = new issuer_exporter($apidata, ['context' => $context]); - $remoteissuer = $exporterinstance->export($output); - - $badgeclone = clone $remotebadge; - $badgeclone->issuer = $remoteissuer; - $remoteassertion->badge = $badgeclone; - $remotebadge->assertion = $remoteassertion; - $publicassertions[] = $remotebadge; + if (empty($collection->entityid)) { + return []; + } + // Now we can make requests. + $badges = $this->curl_request('badges', $collection->entityid); + if (count($badges) == 0) { + return []; + } + $badges = $badges[0]; + if ($expanded) { + $publicassertions = []; + $context = context_system::instance(); + $output = $PAGE->get_renderer('core', 'badges'); + foreach ($badges->assertions as $assertion) { + $remoteassertion = $this->get_assertion($assertion); + // Remote badge was fetched nested in the assertion. + $remotebadge = $remoteassertion->badgeclass; + if (!$remotebadge) { + continue; } - $badges = $publicassertions; + $apidata = badgeclass_exporter::map_external_data($remotebadge, $this->backpackapiversion); + $exporterinstance = new badgeclass_exporter($apidata, ['context' => $context]); + $remotebadge = $exporterinstance->export($output); + + $remoteissuer = $remotebadge->issuer; + $apidata = issuer_exporter::map_external_data($remoteissuer, $this->backpackapiversion); + $exporterinstance = new issuer_exporter($apidata, ['context' => $context]); + $remoteissuer = $exporterinstance->export($output); + + $badgeclone = clone $remotebadge; + $badgeclone->issuer = $remoteissuer; + $remoteassertion->badge = $badgeclone; + $remotebadge->assertion = $remoteassertion; + $publicassertions[] = $remotebadge; } - return $badges; + $badges = $publicassertions; } + return $badges; } + + return []; } } diff --git a/badges/classes/backpack_api_mapping.php b/badges/classes/backpack_api_mapping.php index 36ec0bc2e2865..10802a93cafb3 100644 --- a/badges/classes/backpack_api_mapping.php +++ b/badges/classes/backpack_api_mapping.php @@ -31,10 +31,6 @@ require_once($CFG->libdir . '/filelib.php'); use context_system; -use core_badges\external\assertion_exporter; -use core_badges\external\collection_exporter; -use core_badges\external\issuer_exporter; -use core_badges\external\badgeclass_exporter; use curl; /** @@ -82,7 +78,7 @@ class backpack_api_mapping { /** @var mixed List of parameters for this method. */ protected $postparams; - /** @var int OpenBadges version 1 or 2. */ + /** @var int OpenBadges version. */ protected $backpackapiversion; /** @@ -98,7 +94,7 @@ class backpack_api_mapping { * @param boolean $json json decode the response. * @param boolean $authrequired Authentication is required for this request. * @param boolean $isuserbackpack user backpack or a site backpack. - * @param integer $backpackapiversion OpenBadges version 1 or 2. + * @param integer $backpackapiversion OpenBadges version. */ public function __construct($action, $url, $postparams, $requestexporter, $responseexporter, $multiple, $method, $json, $authrequired, $isuserbackpack, $backpackapiversion) { @@ -233,32 +229,6 @@ private function get_post_params($email, $password, $param) { return $request; } - /** - * Read the response from a V1 user request and save the userID. - * - * @param string $response The request response. - * @param integer $backpackid The backpack id. - * @return mixed - */ - private function convert_email_response($response, $backpackid) { - global $SESSION; - - if (isset($response->status) && $response->status == 'okay') { - - // Remember the tokens. - $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN); - $backpackidkey = $this->get_token_key(BADGE_BACKPACK_ID_TOKEN); - - $SESSION->$useridkey = $response->userId; - $SESSION->$backpackidkey = $backpackid; - return $response->userId; - } - if (!empty($response->error)) { - self::set_authentication_error($response->error); - } - return false; - } - /** * Get the user id from a previous user request. * diff --git a/badges/classes/badge.php b/badges/classes/badge.php index be27fe6d17743..6ecc87fee30a5 100644 --- a/badges/classes/badge.php +++ b/badges/classes/badge.php @@ -14,16 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Badge assertion library. - * - * @package core - * @subpackage badges - * @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @author Yuliya Bozhko - */ - namespace core_badges; defined('MOODLE_INTERNAL') || die(); @@ -44,8 +34,10 @@ /** * Class that represents badge. * + * @package core_badges * @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Yuliya Bozhko */ class badge { /** @var int Badge id */ @@ -168,14 +160,6 @@ public function __construct($badgeid) { } } - if (badges_open_badges_backpack_api() != OPEN_BADGES_V1) { - // For Open Badges 2 we need to use a single site issuer with no exceptions. - $issuer = badges_get_default_issuer(); - $this->issuername = $issuer['name']; - $this->issuercontact = $issuer['email']; - $this->issuerurl = $issuer['url']; - } - $this->criteria = self::get_criteria(); } @@ -962,25 +946,14 @@ public function markdown_badge_criteria() { * @return array Issuer informations of the badge. */ public function get_badge_issuer(?int $obversion = null) { - global $DB; - - $issuer = []; - if ($obversion == OPEN_BADGES_V1) { - $data = $DB->get_record('badge', ['id' => $this->id]); - $issuer['name'] = $data->issuername; - $issuer['url'] = $data->issuerurl; - $issuer['email'] = $data->issuercontact; - } else { - $issuer['name'] = $this->issuername; - $issuer['url'] = $this->issuerurl; - $issuer['email'] = $this->issuercontact; - $issuer['@context'] = OPEN_BADGES_V2_CONTEXT; - $issueridurl = new moodle_url('/badges/issuer_json.php', array('id' => $this->id)); - $issuer['id'] = $issueridurl->out(false); - $issuer['type'] = OPEN_BADGES_V2_TYPE_ISSUER; - } - - return $issuer; + return [ + 'name' => $this->issuername, + 'url' => $this->issuerurl, + 'email' => $this->issuercontact, + '@context' => OPEN_BADGES_V2_CONTEXT, + 'id' => (new moodle_url('/badges/issuer_json.php', ['id' => $this->id]))->out(false), + 'type' => OPEN_BADGES_V2_TYPE_ISSUER, + ]; } /** @@ -991,4 +964,136 @@ public function get_badge_issuer(?int $obversion = null) { public function get_badge_tags(): array { return array_values(\core_tag_tag::get_item_tags_array('core_badges', 'badge', $this->id)); } + + /** + * Create a badge, to store it in the database. + * + * @param stdClass $data Data to create a badge. + * @param int|null $courseid The course where the badge will be added. + * @return badge The badge object created. + */ + public static function create_badge(stdClass $data, ?int $courseid = null): badge { + global $DB, $USER, $CFG; + + $now = time(); + + $fordb = new stdClass(); + $fordb->id = null; + $fordb->courseid = $courseid; + $fordb->type = $courseid ? BADGE_TYPE_COURSE : BADGE_TYPE_SITE; + $fordb->name = trim($data->name); + $fordb->version = $data->version; + $fordb->language = $data->language; + $fordb->description = $data->description; + $fordb->imageauthorname = $data->imageauthorname; + $fordb->imageauthoremail = $data->imageauthoremail; + $fordb->imageauthorurl = $data->imageauthorurl; + $fordb->imagecaption = $data->imagecaption; + $fordb->timecreated = $now; + $fordb->timemodified = $now; + $fordb->usercreated = $USER->id; + $fordb->usermodified = $USER->id; + $fordb->issuername = $data->issuername; + $fordb->issuerurl = $data->issuerurl; + $fordb->issuercontact = $data->issuercontact; + + if (!property_exists($data, 'expiry')) { + $data->expiry = 0; + } + $fordb->expiredate = ($data->expiry == 1) ? $data->expiredate : null; + $fordb->expireperiod = ($data->expiry == 2) ? $data->expireperiod : null; + $fordb->messagesubject = get_string('messagesubject', 'badges'); + $fordb->message = get_string('messagebody', 'badges', + html_writer::link($CFG->wwwroot . '/badges/mybadges.php', get_string('managebadges', 'badges'))); + $fordb->attachment = 1; + $fordb->notification = BADGE_MESSAGE_NEVER; + $fordb->status = BADGE_STATUS_INACTIVE; + + $badgeid = $DB->insert_record('badge', $fordb, true); + + if ($courseid) { + $course = get_course($courseid); + $context = context_course::instance($course->id); + } else { + $context = context_system::instance(); + } + + // Trigger event, badge created. + $eventparams = [ + 'objectid' => $badgeid, + 'context' => $context, + ]; + $event = \core\event\badge_created::create($eventparams); + $event->trigger(); + + $badge = new badge($badgeid); + if (property_exists($data, 'tags')) { + \core_tag_tag::set_item_tags('core_badges', 'badge', $badgeid, $context, $data->tags); + } + + return $badge; + } + + /** + * Update badge data. + * + * @param stdClass $data Data to update a badge. + * @return bool A status for update a badge. + */ + public function update(stdClass $data): bool { + global $USER; + + $this->name = trim($data->name); + $this->version = trim($data->version); + $this->language = $data->language; + $this->description = $data->description; + $this->imageauthorname = $data->imageauthorname; + $this->imageauthoremail = $data->imageauthoremail; + $this->imageauthorurl = $data->imageauthorurl; + $this->imagecaption = $data->imagecaption; + $this->usermodified = $USER->id; + $this->issuername = $data->issuername; + $this->issuerurl = $data->issuerurl; + $this->issuercontact = $data->issuercontact; + $this->expiredate = ($data->expiry == 1) ? $data->expiredate : null; + $this->expireperiod = ($data->expiry == 2) ? $data->expireperiod : null; + + // Need to unset message_editor options to avoid errors on form edit. + unset($this->messageformat); + unset($this->message_editor); + + if (!$this->save()) { + return false; + } + + \core_tag_tag::set_item_tags('core_badges', 'badge', $this->id, $this->get_context(), $data->tags); + + return true; + } + + /** + * Update the message of badge. + * + * @param stdClass $data Data to update a badge message. + * @return bool A status for update a badge message. + */ + public function update_message(stdClass $data): bool { + // Calculate next message cron if form data is different from original badge data. + if ($data->notification != $this->notification) { + if ($data->notification > BADGE_MESSAGE_ALWAYS) { + $this->nextcron = badges_calculate_message_schedule($data->notification); + } else { + $this->nextcron = null; + } + } + + $this->message = clean_text($data->message_editor['text'], FORMAT_HTML); + $this->messagesubject = $data->messagesubject; + $this->notification = $data->notification; + $this->attachment = $data->attachment; + + unset($this->messageformat); + unset($this->message_editor); + return $this->save(); + } } diff --git a/badges/classes/external/collection_exporter.php b/badges/classes/external/collection_exporter.php index ac79f98ee4cfd..866dc59d40f9c 100644 --- a/badges/classes/external/collection_exporter.php +++ b/badges/classes/external/collection_exporter.php @@ -14,14 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Contains class for displaying a collection. - * - * @package core_badges - * @copyright 2019 Damyon Wiese - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace core_badges\external; defined('MOODLE_INTERNAL') || die(); @@ -30,7 +22,7 @@ use stdClass; /** - * Class for displaying a badge competency. + * Class for displaying a badge collection. * * @package core_badges * @copyright 2019 Damyon Wiese @@ -46,15 +38,6 @@ class collection_exporter extends exporter { * @return stdClass */ public static function map_external_data($data, $apiversion) { - if ($apiversion == OPEN_BADGES_V1) { - $result = new stdClass(); - $result->entityType = 'BackpackCollection'; - $result->entityId = $data->groupId; - $result->name = $data->name; - $result->description = $data->description; - $result->assertions = []; - return $result; - } return $data; } diff --git a/badges/classes/external/disable_badges.php b/badges/classes/external/disable_badges.php new file mode 100644 index 0000000000000..e3c2bb7f27b40 --- /dev/null +++ b/badges/classes/external/disable_badges.php @@ -0,0 +1,126 @@ +. + +namespace core_badges\external; + +use core_badges\badge; +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_single_structure; +use core_external\external_multiple_structure; +use core_external\external_value; +use core_external\external_warnings; +use moodle_exception; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/badgeslib.php'); + +/** + * External service to disable badges. + * + * @package core_badges + * @category external + * @copyright 2024 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 4.5 + */ +class disable_badges extends external_api { + + /** + * Describes the parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'badgeids' => new external_multiple_structure( + new external_value(PARAM_TEXT, 'The badge identifiers to update', VALUE_REQUIRED), + ), + ]); + } + + /** + * Disable the given badges. + * + * @param array $badgeids List of badge identifiers to disable. + * @return array List of results and warnings. + */ + public static function execute(array $badgeids): array { + global $CFG, $DB; + + $warnings = []; + + [ + 'badgeids' => $badgeids, + ] = self::validate_parameters(self::execute_parameters(), [ + 'badgeids' => $badgeids, + ]); + + // Check if badges are enabled. + if (empty($CFG->enablebadges)) { + throw new moodle_exception('badgesdisabled', 'badges'); + } + + foreach ($badgeids as $badgeid) { + $badge = new badge($badgeid); + + // Check capabilities. + $context = $badge->get_context(); + self::validate_context($context); + if (!has_capability('moodle/badges:configurecriteria', $context)) { + $warnings[] = [ + 'item' => $badgeid, + 'warningcode' => 'nopermissions', + 'message' => get_string('nopermissions', 'error'), + ]; + continue; + } + + // Check if course badges are enabled. + if (empty($CFG->badges_allowcoursebadges) && ($badge->type == BADGE_TYPE_COURSE)) { + $warnings[] = [ + 'item' => $badgeid, + 'warningcode' => 'coursebadgesdisabled', + 'message' => get_string('coursebadgesdisabled', 'badges'), + ]; + continue; + } + + $status = ($badge->status == BADGE_STATUS_ACTIVE) ? BADGE_STATUS_INACTIVE : BADGE_STATUS_INACTIVE_LOCKED; + // Deactivate the badge. + $badge->set_status($status); + } + + return [ + 'result' => empty($warnings), + 'warnings' => $warnings, + ]; + } + + /** + * Describe the return structure of the external service. + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'result' => new external_value(PARAM_BOOL, 'The processing result'), + 'warnings' => new external_warnings(), + ]); + } +} diff --git a/badges/classes/external/enable_badges.php b/badges/classes/external/enable_badges.php new file mode 100644 index 0000000000000..ddf9f9be3c2e3 --- /dev/null +++ b/badges/classes/external/enable_badges.php @@ -0,0 +1,183 @@ +. + +namespace core_badges\external; + +use core_badges\badge; +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_single_structure; +use core_external\external_multiple_structure; +use core_external\external_value; +use core_external\external_warnings; +use moodle_exception; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/badgeslib.php'); + +/** + * External service to enable badges. + * + * @package core_badges + * @category external + * @copyright 2024 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 4.5 + */ +class enable_badges extends external_api { + + /** + * Describes the parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'badgeids' => new external_multiple_structure( + new external_value(PARAM_TEXT, 'The badge identifiers to update', VALUE_REQUIRED), + ), + ]); + } + + /** + * Enable the given badges. + * + * @param array $badgeids List of badge identifiers to enable. + * @return array The number of awarded users for each badge or 'cron' when there are more than 1000 users. + */ + public static function execute(array $badgeids): array { + global $CFG, $DB; + + $result = []; + $warnings = []; + + [ + 'badgeids' => $badgeids, + ] = self::validate_parameters(self::execute_parameters(), [ + 'badgeids' => $badgeids, + ]); + + // Check if badges are enabled. + if (empty($CFG->enablebadges)) { + throw new moodle_exception('badgesdisabled', 'badges'); + } + + foreach ($badgeids as $badgeid) { + $badge = new badge($badgeid); + + // Check capabilities. + $context = $badge->get_context(); + self::validate_context($context); + if (!has_capability('moodle/badges:configurecriteria', $context)) { + $warnings[] = [ + 'item' => $badgeid, + 'warningcode' => 'nopermissions', + 'message' => get_string('nopermissions', 'error'), + ]; + continue; + } + + // Check if course badges are enabled. + if (empty($CFG->badges_allowcoursebadges) && ($badge->type == BADGE_TYPE_COURSE)) { + $warnings[] = [ + 'item' => $badgeid, + 'warningcode' => 'coursebadgesdisabled', + 'message' => get_string('coursebadgesdisabled', 'badges'), + ]; + continue; + } + + // Check if the badge has criteria. + if (!$badge->has_criteria()) { + $warnings[] = [ + 'item' => $badgeid, + 'warningcode' => 'nocriteria', + 'message' => get_string('nocriteria', 'badges'), + ]; + continue; + } + + // Activate the badge. + $status = ($badge->status == BADGE_STATUS_INACTIVE) ? BADGE_STATUS_ACTIVE : BADGE_STATUS_ACTIVE_LOCKED; + $badge->set_status($status); + $awards = self::review_criteria($badge); + + $result[] = [ + 'badgeid' => $badgeid, + 'awards' => $awards, + ]; + } + + return [ + 'result' => $result, + 'warnings' => $warnings, + ]; + } + + /** + * Describe the return structure of the external service. + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'result' => new external_multiple_structure( + new external_single_structure([ + 'badgeid' => new external_value(PARAM_INT, 'The badge identifier'), + 'awards' => new external_value(PARAM_ALPHANUM, 'The processing result'), + ]), + ), + 'warnings' => new external_warnings(), + ]); + } + + /** + * Review the criteria of the badge. + * + * @param \core_badges\badge $badge The badge to review the criteria. + * @return string The number of awarded users or 'cron' when there are more than 1000 users. + */ + private static function review_criteria(badge $badge): string { + global $CFG, $DB; + + if ($badge->type == BADGE_TYPE_SITE) { + // Review on cron if there are more than 1000 users who can earn a site-level badge. + $sql = 'SELECT COUNT(u.id) as num + FROM {user} u + LEFT JOIN {badge_issued} bi + ON u.id = bi.userid AND bi.badgeid = :badgeid + WHERE bi.badgeid IS NULL AND u.id != :guestid AND u.deleted = 0'; + $toearn = $DB->get_record_sql( + $sql, + [ + 'badgeid' => $badge->id, + 'guestid' => $CFG->siteguest, + ], + ); + if ($toearn->num < 1000) { + $awards = $badge->review_all_criteria(); + } else { + $awards = 'cron'; + } + } else { + $awards = $badge->review_all_criteria(); + } + + return $awards; + } +} diff --git a/badges/classes/external/issuer_exporter.php b/badges/classes/external/issuer_exporter.php index d741031e58e04..3529d33e15fcf 100644 --- a/badges/classes/external/issuer_exporter.php +++ b/badges/classes/external/issuer_exporter.php @@ -14,14 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Contains class for displaying a issuer. - * - * @package core_badges - * @copyright 2019 Damyon Wiese - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace core_badges\external; defined('MOODLE_INTERNAL') || die(); @@ -38,17 +30,13 @@ class issuer_exporter extends exporter { /** - * Either map version 1 data to version 2 or return it untouched. + * Map data depending on the version. * - * @param stdClass $data The remote data. + * @param \stdClass $data The remote data. * @param string $apiversion The backpack version used to communicate remotely. - * @return stdClass + * @return \stdClass */ public static function map_external_data($data, $apiversion) { - if ($apiversion == OPEN_BADGES_V1) { - $result = new \stdClass(); - return $result; - } $mapped = new \stdClass(); if (isset($data->entityType)) { $mapped->type = $data->entityType; diff --git a/badges/classes/form/badge.php b/badges/classes/form/badge.php index f64f201e42c6b..8774830657e6e 100644 --- a/badges/classes/form/badge.php +++ b/badges/classes/form/badge.php @@ -14,16 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Form classes for editing badges - * - * @package core - * @subpackage badges - * @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @author Yuliya Bozhko - */ - namespace core_badges\form; defined('MOODLE_INTERNAL') || die(); @@ -35,8 +25,12 @@ use moodleform; /** - * Form to edit badge details. + * Form classes for editing badges * + * @package core_badges + * @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Yuliya Bozhko */ class badge extends moodleform { @@ -44,20 +38,29 @@ class badge extends moodleform { * Defines the form */ public function definition() { - global $CFG; + global $CFG, $SITE; $mform = $this->_form; $badge = (isset($this->_customdata['badge'])) ? $this->_customdata['badge'] : false; $action = $this->_customdata['action']; + if (array_key_exists('courseid', $this->_customdata)) { + $courseid = $this->_customdata['courseid']; + } else if (array_key_exists('badge', $this->_customdata)) { + $courseid = $this->_customdata['badge']->courseid; + } + if (!empty($courseid)) { + $mform->addElement('hidden', 'courseid', $courseid); + $mform->setType('courseid', PARAM_INT); + } $mform->addElement('header', 'badgedetails', get_string('badgedetails', 'badges')); - $mform->addElement('text', 'name', get_string('name'), array('size' => '70')); + $mform->addElement('text', 'name', get_string('name'), ['size' => '70']); // When downloading badge, it will be necessary to clean the name as PARAM_FILE. $mform->setType('name', PARAM_TEXT); $mform->addRule('name', null, 'required'); $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); - $mform->addElement('text', 'version', get_string('version', 'badges'), array('size' => '70')); + $mform->addElement('text', 'version', get_string('version', 'badges'), ['size' => '70']); $mform->setType('version', PARAM_TEXT); $mform->addHelpButton('version', 'version', 'badges'); @@ -70,7 +73,7 @@ public function definition() { $mform->addRule('description', null, 'required'); $str = $action == 'new' ? get_string('badgeimage', 'badges') : get_string('newimage', 'badges'); - $imageoptions = array('maxbytes' => 262144, 'accepted_types' => array('optimised_image')); + $imageoptions = ['maxbytes' => 262144, 'accepted_types' => ['optimised_image']]; $mform->addElement('filepicker', 'image', $str, null, $imageoptions); if ($action == 'new') { @@ -80,57 +83,56 @@ public function definition() { $mform->insertElementBefore($currentimage, 'image'); } $mform->addHelpButton('image', 'badgeimage', 'badges'); - $mform->addElement('text', 'imageauthorname', get_string('imageauthorname', 'badges'), array('size' => '70')); + $mform->addElement('text', 'imageauthorname', get_string('imageauthorname', 'badges'), ['size' => '70']); $mform->setType('imageauthorname', PARAM_TEXT); $mform->addHelpButton('imageauthorname', 'imageauthorname', 'badges'); - $mform->addElement('text', 'imageauthoremail', get_string('imageauthoremail', 'badges'), array('size' => '70')); + $mform->addElement('text', 'imageauthoremail', get_string('imageauthoremail', 'badges'), ['size' => '70']); $mform->setType('imageauthoremail', PARAM_TEXT); $mform->addHelpButton('imageauthoremail', 'imageauthoremail', 'badges'); - $mform->addElement('text', 'imageauthorurl', get_string('imageauthorurl', 'badges'), array('size' => '70')); + $mform->addElement('text', 'imageauthorurl', get_string('imageauthorurl', 'badges'), ['size' => '70']); $mform->setType('imageauthorurl', PARAM_URL); $mform->addHelpButton('imageauthorurl', 'imageauthorurl', 'badges'); - $mform->addElement('text', 'imagecaption', get_string('imagecaption', 'badges'), array('size' => '70')); + $mform->addElement('text', 'imagecaption', get_string('imagecaption', 'badges'), ['size' => '70']); $mform->setType('imagecaption', PARAM_TEXT); $mform->addHelpButton('imagecaption', 'imagecaption', 'badges'); $mform->addElement('tags', 'tags', get_string('tags', 'badges'), ['itemtype' => 'badge', 'component' => 'core_badges']); - if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) { - $mform->addElement('header', 'issuerdetails', get_string('issuerdetails', 'badges')); + $mform->addElement('header', 'issuerdetails', get_string('issuerdetails', 'badges')); - $mform->addElement('text', 'issuername', get_string('name'), array('size' => '70')); - $mform->setType('issuername', PARAM_NOTAGS); - $mform->addRule('issuername', null, 'required'); - if (isset($CFG->badges_defaultissuername)) { - $mform->setDefault('issuername', $CFG->badges_defaultissuername); - } - $mform->addHelpButton('issuername', 'issuername', 'badges'); + $mform->addElement('text', 'issuername', get_string('issuername', 'badges'), ['size' => '70']); + $mform->setType('issuername', PARAM_NOTAGS); + $mform->addRule('issuername', null, 'required'); + $site = get_site(); + $issuername = $CFG->badges_defaultissuername ?: $site->fullname; + $mform->setDefault('issuername', $issuername); + $mform->addHelpButton('issuername', 'issuername', 'badges'); - $mform->addElement('text', 'issuercontact', get_string('contact', 'badges'), array('size' => '70')); - if (isset($CFG->badges_defaultissuercontact)) { - $mform->setDefault('issuercontact', $CFG->badges_defaultissuercontact); - } - $mform->setType('issuercontact', PARAM_RAW); - $mform->addHelpButton('issuercontact', 'contact', 'badges'); - // Set issuer URL. - // Have to parse URL because badge issuer origin cannot be a subfolder in wwwroot. - $url = parse_url($CFG->wwwroot); - $mform->addElement('hidden', 'issuerurl', $url['scheme'] . '://' . $url['host']); - $mform->setType('issuerurl', PARAM_URL); + $mform->addElement('text', 'issuercontact', get_string('contact', 'badges'), ['size' => '70']); + if (isset($CFG->badges_defaultissuercontact)) { + $mform->setDefault('issuercontact', $CFG->badges_defaultissuercontact); } + $mform->setType('issuercontact', PARAM_RAW); + $mform->addRule('issuercontact', null, 'email'); + $mform->addHelpButton('issuercontact', 'contact', 'badges'); + // Set issuer URL. + // Have to parse URL because badge issuer origin cannot be a subfolder in wwwroot. + $url = parse_url($CFG->wwwroot); + $mform->addElement('hidden', 'issuerurl', $url['scheme'] . '://' . $url['host']); + $mform->setType('issuerurl', PARAM_URL); $mform->addElement('header', 'issuancedetails', get_string('issuancedetails', 'badges')); - $issuancedetails = array(); - $issuancedetails[] =& $mform->createElement('radio', 'expiry', '', get_string('never', 'badges'), 0); - $issuancedetails[] =& $mform->createElement('static', 'none_break', null, '
    '); - $issuancedetails[] =& $mform->createElement('radio', 'expiry', '', get_string('fixed', 'badges'), 1); - $issuancedetails[] =& $mform->createElement('date_selector', 'expiredate', ''); - $issuancedetails[] =& $mform->createElement('static', 'expirydate_break', null, '
    '); - $issuancedetails[] =& $mform->createElement('radio', 'expiry', '', get_string('relative', 'badges'), 2); - $issuancedetails[] =& $mform->createElement('duration', 'expireperiod', '', array('defaultunit' => 86400, 'optional' => false)); - $issuancedetails[] =& $mform->createElement('static', 'expiryperiods_break', null, get_string('after', 'badges')); - - $mform->addGroup($issuancedetails, 'expirydategr', get_string('expirydate', 'badges'), array(' '), false); + $issuancedetails = []; + $issuancedetails[] = $mform->createElement('radio', 'expiry', '', get_string('never', 'badges'), 0); + $issuancedetails[] = $mform->createElement('static', 'none_break', null, '
    '); + $issuancedetails[] = $mform->createElement('radio', 'expiry', '', get_string('fixed', 'badges'), 1); + $issuancedetails[] = $mform->createElement('date_selector', 'expiredate', ''); + $issuancedetails[] = $mform->createElement('static', 'expirydate_break', null, '
    '); + $issuancedetails[] = $mform->createElement('radio', 'expiry', '', get_string('relative', 'badges'), 2); + $issuancedetails[] = $mform->createElement('duration', 'expireperiod', '', ['defaultunit' => 86400, 'optional' => false]); + $issuancedetails[] = $mform->createElement('static', 'expiryperiods_break', null, get_string('after', 'badges')); + + $mform->addGroup($issuancedetails, 'expirydategr', get_string('expirydate', 'badges'), [' '], false); $mform->addHelpButton('expirydategr', 'expirydate', 'badges'); $mform->setDefault('expiry', 0); $mform->setDefault('expiredate', strtotime('+1 year')); @@ -165,7 +167,7 @@ public function definition() { // Freeze all elements if badge is active or locked. if ($badge->is_active() || $badge->is_locked()) { - $mform->hardFreezeAllVisibleExcept(array()); + $mform->hardFreezeAllVisibleExcept([]); } } } @@ -173,11 +175,11 @@ public function definition() { /** * Load in existing data as form defaults * - * @param stdClass|array $badge object or array of default values + * @param \core_badges\badge $badge object or array of default values */ public function set_data($badge) { $defaultvalues = []; - parent::set_data($badge); + parent::set_data((object) $badge); if (!empty($badge->expiredate)) { $defaultvalues['expiry'] = 1; @@ -186,6 +188,11 @@ public function set_data($badge) { $defaultvalues['expiry'] = 2; $defaultvalues['expireperiod'] = $badge->expireperiod; } + + if (!empty($badge->name)) { + $defaultvalues['name'] = trim($badge->name); + } + $defaultvalues['tags'] = \core_tag_tag::get_item_tags_array('core_badges', 'badge', $badge->id); $defaultvalues['currentimage'] = print_badge_image($badge, $badge->get_context(), 'large'); @@ -196,14 +203,12 @@ public function set_data($badge) { * Validates form data */ public function validation($data, $files) { - global $DB; - $errors = parent::validation($data, $files); + global $DB, $SITE; - if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) { - if (!empty($data['issuercontact']) && !validate_email($data['issuercontact'])) { - $errors['issuercontact'] = get_string('invalidemail'); - } - } + // Trim badge name (to guarantee no badges are created with the same name but some extra spaces). + $data['name'] = trim($data['name']); + + $errors = parent::validation($data, $files); if ($data['expiry'] == 2 && $data['expireperiod'] <= 0) { $errors['expirydategr'] = get_string('error:invalidexpireperiod', 'badges'); @@ -217,19 +222,6 @@ public function validation($data, $files) { $errors['imageauthoremail'] = get_string('invalidemail'); } - // Check for duplicate badge names. - if ($data['action'] == 'new') { - $duplicate = $DB->record_exists_select('badge', 'name = :name AND status != :deleted', - array('name' => $data['name'], 'deleted' => BADGE_STATUS_ARCHIVED)); - } else { - $duplicate = $DB->record_exists_select('badge', 'name = :name AND id != :badgeid AND status != :deleted', - array('name' => $data['name'], 'badgeid' => $data['id'], 'deleted' => BADGE_STATUS_ARCHIVED)); - } - - if ($duplicate) { - $errors['name'] = get_string('error:duplicatename', 'badges'); - } - if ($data['imageauthorurl'] && !preg_match('@^https?://.+@', $data['imageauthorurl'])) { $errors['imageauthorurl'] = get_string('invalidurl', 'badges'); } @@ -237,4 +229,3 @@ public function validation($data, $files) { return $errors; } } - diff --git a/badges/classes/form/collections.php b/badges/classes/form/collections.php index d9f08368f4576..74a08325e4e95 100644 --- a/badges/classes/form/collections.php +++ b/badges/classes/form/collections.php @@ -14,16 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Form class for mybackpack.php - * - * @package core - * @subpackage badges - * @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @author Yuliya Bozhko - */ - namespace core_badges\form; defined('MOODLE_INTERNAL') || die(); @@ -37,8 +27,10 @@ /** * Form to select backpack collections. * + * @package core_badges * @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Yuliya Bozhko */ class collections extends moodleform { @@ -79,22 +71,12 @@ public function definition() { foreach ($groups as $group) { $count = 0; // Handle attributes based on backpack's supported version. - if ($sitebackpack->apiversion == OPEN_BADGES_V2) { - // OpenBadges v2 data attributes. - if (empty($group->published)) { - // Only public collections. - continue; - } - - // Get the number of badges associated with this collection from the assertions array returned. - $count = count($group->assertions); - } else { - // OpenBadges v1 data attributes. - $group->entityId = $group->groupId; - - // Get the number of badges associated with this collection. In that case, the number is returned directly. - $count = $group->badges; + if (empty($group->published)) { + // Only public collections. + continue; } + // Get the number of badges associated with this collection from the assertions array returned. + $count = count($group->assertions); if (!$hasgroups) { $mform->addElement('static', 'selectgroup', '', get_string('selectgroup_start', 'badges')); diff --git a/badges/classes/form/external_backpack.php b/badges/classes/form/external_backpack.php index af099e14bec09..57da76ba4c288 100644 --- a/badges/classes/form/external_backpack.php +++ b/badges/classes/form/external_backpack.php @@ -14,15 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * External backpack form - * - * @package core_badges - * @copyright 2019 Damyon Wiese - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace core_badges\form; + defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/formslib.php'); @@ -93,8 +86,8 @@ public function definition() { $mform->hideIf('backpackemail', 'includeauthdetails'); $mform->hideIf('backpackemail', 'apiversion', 'in', [OPEN_BADGES_V2P1]); $mform->hideIf('password', 'includeauthdetails'); - $mform->hideIf('password', 'apiversion', 'in', [OPEN_BADGES_V1, OPEN_BADGES_V2P1]); - $mform->hideIf('backpackapiurl', 'apiversion', 'in', [OPEN_BADGES_V1, OPEN_BADGES_V2P1]); + $mform->hideIf('password', 'apiversion', 'in', [OPEN_BADGES_V2P1]); + $mform->hideIf('backpackapiurl', 'apiversion', 'in', [OPEN_BADGES_V2P1]); // Disable short forms. $mform->setDisableShortforms(); diff --git a/badges/classes/output/standard_action_bar.php b/badges/classes/output/standard_action_bar.php index 01609e2a992a5..619eb7719bd84 100644 --- a/badges/classes/output/standard_action_bar.php +++ b/badges/classes/output/standard_action_bar.php @@ -88,8 +88,19 @@ public function export_for_template(renderer_base $output): array { } if ($this->showaddbadge && has_capability('moodle/badges:createbadge', $this->page->context)) { - $buttons[] = new single_button(new moodle_url('/badges/newbadge.php', $params), - get_string('newbadge', 'core_badges'), 'post', single_button::BUTTON_PRIMARY); + $editparams = ['action' => 'new']; + if (array_key_exists('id', $params)) { + $editparams['courseid'] = $params['id']; + } + $buttons[] = new single_button( + new moodle_url( + '/badges/edit.php', + $editparams, + ), + get_string('newbadge', 'core_badges'), + 'post', + single_button::BUTTON_PRIMARY, + ); } foreach ($buttons as $key => $button) { diff --git a/badges/classes/reportbuilder/local/entities/badge.php b/badges/classes/reportbuilder/local/entities/badge.php index 46d31a1da434c..526c441382c39 100644 --- a/badges/classes/reportbuilder/local/entities/badge.php +++ b/badges/classes/reportbuilder/local/entities/badge.php @@ -175,13 +175,11 @@ protected function get_all_columns(): array { ->add_join("LEFT JOIN {context} {$contextalias} ON {$contextalias}.contextlevel = " . CONTEXT_COURSE . " AND {$contextalias}.instanceid = {$badgealias}.courseid") - ->set_type(column::TYPE_INTEGER) ->add_fields("{$badgealias}.id, {$badgealias}.type, {$badgealias}.courseid") ->add_field($DB->sql_cast_to_char("{$badgealias}.imagecaption"), 'imagecaption') ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) - ->set_disabled_aggregation_all() - ->add_callback(static function(?int $badgeid, stdClass $badge): string { - if (!$badgeid) { + ->add_callback(static function($value, stdClass $badge): string { + if ($badge->id === null) { return ''; } if ($badge->type == BADGE_TYPE_SITE) { @@ -191,7 +189,7 @@ protected function get_all_columns(): array { $context = context_course::instance($badge->courseid); } - $badgeimage = moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $badgeid, '/', 'f2'); + $badgeimage = moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id, '/', 'f2'); return html_writer::img($badgeimage, $badge->imagecaption); }); @@ -207,7 +205,7 @@ protected function get_all_columns(): array { ->set_is_sortable(true) ->add_callback(static function($language): string { $languages = get_string_manager()->get_list_of_languages(); - return $languages[$language] ?? $language ?? ''; + return (string) ($languages[$language] ?? $language); }); // Version. diff --git a/badges/classes/reportbuilder/local/systemreports/badges.php b/badges/classes/reportbuilder/local/systemreports/badges.php index 5a8da89262f81..cbe8384a1ecdb 100644 --- a/badges/classes/reportbuilder/local/systemreports/badges.php +++ b/badges/classes/reportbuilder/local/systemreports/badges.php @@ -158,21 +158,19 @@ protected function add_filters(): void { protected function add_actions(): void { // Activate badge. $this->add_action((new action( - new moodle_url('/badges/action.php', [ - 'id' => ':id', - 'activate' => true, - 'return' => ':return', - ]), + new moodle_url('#'), new pix_icon('t/show', '', 'core'), - [], + [ + 'data-action' => 'enablebadge', + 'data-badgeid' => ':id', + 'data-badgename' => ':badgename', + 'data-courseid' => ':courseid', + ], false, new lang_string('activate', 'badges') ))->add_callback(static function(stdclass $row): bool { $badge = new \core_badges\badge($row->id); - - // Populate the return URL. - $row->return = (new moodle_url('/badges/index.php', - ['type' => $badge->type, 'id' => (int) $badge->courseid]))->out_as_local_url(false); + $row->badgename = $badge->name; return has_capability('moodle/badges:configuredetails', $badge->get_context()) && $badge->has_criteria() && @@ -182,18 +180,19 @@ protected function add_actions(): void { // Deactivate badge. $this->add_action((new action( - new moodle_url('/badges/index.php', [ - 'lock' => ':id', - 'sesskey' => sesskey(), - 'type' => ':type', - 'id' => ':courseid', - ]), + new moodle_url('#'), new pix_icon('t/hide', '', 'core'), - [], + [ + 'data-action' => 'disablebadge', + 'data-badgeid' => ':id', + 'data-badgename' => ':badgename', + 'data-courseid' => ':courseid', + ], false, new lang_string('deactivate', 'badges') ))->add_callback(static function(stdclass $row): bool { $badge = new \core_badges\badge($row->id); + $row->badgename = $badge->name; return has_capability('moodle/badges:configuredetails', $badge->get_context()) && $badge->has_criteria() && $row->status != BADGE_STATUS_INACTIVE && $row->status != BADGE_STATUS_INACTIVE_LOCKED; diff --git a/badges/edit.php b/badges/edit.php index 86f6c9f910c51..a65fdd9b1283f 100644 --- a/badges/edit.php +++ b/badges/edit.php @@ -15,10 +15,9 @@ // along with Moodle. If not, see . /** - * Editing badge details, criteria, messages + * Editing badge details, criteria, messages. * - * @package core - * @subpackage badges + * @package core_badges * @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Yuliya Bozhko @@ -28,7 +27,14 @@ require_once($CFG->libdir . '/badgeslib.php'); require_once($CFG->libdir . '/filelib.php'); -$badgeid = required_param('id', PARAM_INT); +// Used only for creating new badge. +$courseid = optional_param('courseid', 0, PARAM_INT); +if ($courseid === 0 ) { + $courseid = null; +} + +// Used for editing existing badge. +$badgeid = optional_param('id', null, PARAM_INT); $action = optional_param('action', 'badge', PARAM_TEXT); require_login(); @@ -37,24 +43,54 @@ throw new \moodle_exception('badgesdisabled', 'badges'); } -$badge = new badge($badgeid); -$context = $badge->get_context(); -$navurl = new moodle_url('/badges/index.php', array('type' => $badge->type)); +if (!empty($badgeid)) { + // Existing badge. + $badge = new badge($badgeid); -if ($action == 'message') { - require_capability('moodle/badges:configuremessages', $context); + if ($badge->courseid) { + $course = get_course($badge->courseid); + } + $params = ['id' => $badgeid, 'action' => $action]; + $badgename = $badge->name; + + // Check capabilities. + $context = $badge->get_context(); + if ($action == 'message') { + require_capability('moodle/badges:configuremessages', $context); + } else { + require_capability('moodle/badges:configuredetails', $context); + } } else { - require_capability('moodle/badges:configuredetails', $context); + // New badge. + if ($courseid) { + $course = get_course($courseid); + $context = context_course::instance($course->id); + } else { + $context = context_system::instance(); + } + + $badge = new stdClass(); + $badge->id = null; + $badge->type = $courseid ? BADGE_TYPE_COURSE : BADGE_TYPE_SITE; + $badge->courseid = $courseid; + + $params = ['courseid' => $courseid]; + $badgename = get_string('create', 'badges'); + + // Check capabilities. + require_capability('moodle/badges:createbadge', $context); } +// Check if course badges are enabled. +if (empty($CFG->badges_allowcoursebadges) && ($badge->type == BADGE_TYPE_COURSE)) { + throw new \moodle_exception('coursebadgesdisabled', 'badges'); +} + +$navurl = new moodle_url('/badges/index.php', ['type' => $badge->type]); if ($badge->type == BADGE_TYPE_COURSE) { - if (empty($CFG->badges_allowcoursebadges)) { - throw new \moodle_exception('coursebadgesdisabled', 'badges'); - } require_login($badge->courseid); - $course = get_course($badge->courseid); $heading = format_string($course->fullname, true, ['context' => $context]); - $navurl = new moodle_url('/badges/index.php', array('type' => $badge->type, 'id' => $badge->courseid)); + $navurl = new moodle_url('/badges/index.php', ['type' => $badge->type, 'id' => $badge->courseid]); $PAGE->set_pagelayout('incourse'); navigation_node::override_active_url($navurl); } else { @@ -63,106 +99,104 @@ navigation_node::override_active_url($navurl, true); } -$currenturl = new moodle_url('/badges/edit.php', array('id' => $badge->id, 'action' => $action)); +$currenturl = new moodle_url('/badges/edit.php', $params); $PAGE->set_context($context); $PAGE->set_url($currenturl); $PAGE->set_heading($heading); -$PAGE->set_title($badge->name); +$PAGE->set_title($badgename); $PAGE->add_body_class('limitedwidth'); -$PAGE->navbar->add($badge->name); +$PAGE->navbar->add($badgename); +/** @var \core_badges_renderer $output*/ $output = $PAGE->get_renderer('core', 'badges'); $statusmsg = ''; $errormsg = ''; -$badge->message = clean_text($badge->message, FORMAT_HTML); -$editoroptions = array( +$editoroptions = []; +if ($badge->id && $action == 'message') { + $badge->message = clean_text($badge->message, FORMAT_HTML); + $editoroptions = [ 'subdirs' => 0, 'maxbytes' => 0, 'maxfiles' => 0, 'changeformat' => 0, 'context' => $context, 'noclean' => false, - 'trusttext' => false - ); -$badge = file_prepare_standard_editor($badge, 'message', $editoroptions, $context); + 'trusttext' => false, + ]; + $badge = file_prepare_standard_editor($badge, 'message', $editoroptions, $context); +} -$formclass = '\core_badges\form' . '\\' . $action; -$form = new $formclass($currenturl, array('badge' => $badge, 'action' => $action, 'editoroptions' => $editoroptions)); +$formclass = '\core_badges\form' . '\\' . ($action == 'new' ? 'badge' : $action); +$params = [ + 'action' => $action, +]; +if ($badge->id) { + $params['badge'] = $badge; + $params['editoroptions'] = $editoroptions; +} else { + $params['courseid'] = $courseid; +} +$form = new $formclass($currenturl, $params); if ($form->is_cancelled()) { - redirect(new moodle_url('/badges/overview.php', array('id' => $badgeid))); + redirect(new moodle_url('/badges/overview.php', ['id' => $badgeid])); } else if ($form->is_submitted() && $form->is_validated() && ($data = $form->get_data())) { - if ($action == 'badge') { - $badge->name = $data->name; - $badge->version = trim($data->version); - $badge->language = $data->language; - $badge->description = $data->description; - $badge->imageauthorname = $data->imageauthorname; - $badge->imageauthoremail = $data->imageauthoremail; - $badge->imageauthorurl = $data->imageauthorurl; - $badge->imagecaption = $data->imagecaption; - $badge->usermodified = $USER->id; - if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) { - $badge->issuername = $data->issuername; - $badge->issuerurl = $data->issuerurl; - $badge->issuercontact = $data->issuercontact; - } - $badge->expiredate = ($data->expiry == 1) ? $data->expiredate : null; - $badge->expireperiod = ($data->expiry == 2) ? $data->expireperiod : null; - - // Need to unset message_editor options to avoid errors on form edit. - unset($badge->messageformat); - unset($badge->message_editor); - - if ($badge->save()) { - core_tag_tag::set_item_tags('core_badges', 'badge', $badge->id, $context, $data->tags); + switch ($action) { + case 'new': + // Create new badge. + $badge = badge::create_badge($data, $courseid); + $badgeid = $badge->id; badges_process_badge_image($badge, $form->save_temp_file('image')); - $form->set_data($badge); - $statusmsg = get_string('changessaved'); - } else { - $errormsg = get_string('error:save', 'badges'); - } - } else if ($action == 'message') { - // Calculate next message cron if form data is different from original badge data. - if ($data->notification != $badge->notification) { - if ($data->notification > BADGE_MESSAGE_ALWAYS) { - $badge->nextcron = badges_calculate_message_schedule($data->notification); + + // If a user can configure badge criteria, they will be redirected to the criteria page. + if (has_capability('moodle/badges:configurecriteria', $context)) { + redirect(new moodle_url('/badges/criteria.php', ['id' => $badgeid])); + } + redirect(new moodle_url('/badges/overview.php', ['id' => $badgeid])); + break; + + case 'badge': + // Edit existing badge. + if ($badge->update($data)) { + badges_process_badge_image($badge, $form->save_temp_file('image')); + $form->set_data($badge); + $statusmsg = get_string('changessaved'); } else { - $badge->nextcron = null; + $errormsg = get_string('error:save', 'badges'); } - } - - $badge->message = clean_text($data->message_editor['text'], FORMAT_HTML); - $badge->messagesubject = $data->messagesubject; - $badge->notification = $data->notification; - $badge->attachment = $data->attachment; - - unset($badge->messageformat); - unset($badge->message_editor); - if ($badge->save()) { - $statusmsg = get_string('changessaved'); - } else { - $errormsg = get_string('error:save', 'badges'); - } + break; + + case 'message': + // Update badge message. + if ($badge->update_message($data)) { + $statusmsg = get_string('changessaved'); + } else { + $errormsg = get_string('error:save', 'badges'); + } + break; } } -echo $OUTPUT->header(); -$actionbar = new \core_badges\output\manage_badge_action_bar($badge, $PAGE); -echo $output->render_tertiary_navigation($actionbar); +echo $output->header(); -echo $OUTPUT->heading(print_badge_image($badge, $context, 'small') . ' ' . $badge->name); +if ($badge->id) { + $actionbar = new \core_badges\output\manage_badge_action_bar($badge, $PAGE); + echo $output->render_tertiary_navigation($actionbar); + echo $output->heading(print_badge_image($badge, $context, 'small') . ' ' . $badge->name); -if ($errormsg !== '') { - echo $OUTPUT->notification($errormsg); + if ($errormsg !== '') { + echo $output->notification($errormsg); -} else if ($statusmsg !== '') { - echo $OUTPUT->notification($statusmsg, 'notifysuccess'); + } else if ($statusmsg !== '') { + echo $output->notification($statusmsg, 'notifysuccess'); + } + echo $output->print_badge_status_box($badge); +} else { + echo $output->heading($badgename); } -echo $output->print_badge_status_box($badge); $form->display(); -echo $OUTPUT->footer(); +echo $output->footer(); diff --git a/badges/index.php b/badges/index.php index df043ec35e87d..df0f3ad3d7b3d 100644 --- a/badges/index.php +++ b/badges/index.php @@ -161,5 +161,6 @@ $report->set_default_no_results_notice(new lang_string('nobadges', 'badges')); echo $report->output(); +$PAGE->requires->js_call_amd('core_badges/actions', 'init'); echo $OUTPUT->footer(); diff --git a/badges/issuer_json.php b/badges/issuer_json.php index 87fe6bd417e7c..366df384f14fb 100644 --- a/badges/issuer_json.php +++ b/badges/issuer_json.php @@ -17,8 +17,7 @@ /** * Serve Issuer JSON for related badge or default Issuer if no badge is defined. * - * @package core - * @subpackage badges + * @package core_badges * @copyright 2020 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -28,10 +27,7 @@ require_once(__DIR__ . '/../config.php'); require_once($CFG->libdir . '/badgeslib.php'); - $id = optional_param('id', null, PARAM_INT); -// OB specification version. If it's not defined, the site will be used as default. -$obversion = optional_param('obversion', badges_open_badges_backpack_api(), PARAM_INT); if (empty($id)) { // Get the default issuer for this site. @@ -40,7 +36,7 @@ // Get the issuer for this badge. $badge = new badge($id); if ($badge->status != BADGE_STATUS_INACTIVE) { - $json = $badge->get_badge_issuer($obversion); + $json = $badge->get_badge_issuer(); } else { // The badge doen't exist or not accessible for the users. header("HTTP/1.0 410 Gone"); diff --git a/badges/newbadge.php b/badges/newbadge.php index fe60d03e04581..0658ebf4fa867 100644 --- a/badges/newbadge.php +++ b/badges/newbadge.php @@ -17,117 +17,19 @@ /** * First step page for creating a new badge * - * @package core - * @subpackage badges + * @package core_badges * @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Yuliya Bozhko + * @deprecated since 4.5. Use badges/edit.php instead. + * @todo MDL-82383 This file will be deleted in Moodle 6.0. */ require_once(__DIR__ . '/../config.php'); -require_once($CFG->libdir . '/badgeslib.php'); -$type = required_param('type', PARAM_INT); $courseid = optional_param('id', 0, PARAM_INT); require_login(); -if (empty($CFG->enablebadges)) { - throw new \moodle_exception('badgesdisabled', 'badges'); -} - -if (empty($CFG->badges_allowcoursebadges) && ($type == BADGE_TYPE_COURSE)) { - throw new \moodle_exception('coursebadgesdisabled', 'badges'); -} - -$title = get_string('create', 'badges'); -$PAGE->add_body_class('limitedwidth'); - -if (($type == BADGE_TYPE_COURSE) && ($course = $DB->get_record('course', array('id' => $courseid)))) { - require_login($course); - $coursecontext = context_course::instance($course->id); - $PAGE->set_context($coursecontext); - $PAGE->set_pagelayout('incourse'); - $PAGE->set_url('/badges/newbadge.php', array('type' => $type, 'id' => $course->id)); - $heading = format_string($course->fullname, true, array('context' => $coursecontext)) . ": " . $title; - $PAGE->set_heading($heading); - $PAGE->set_title($heading); -} else { - $PAGE->set_context(context_system::instance()); - $PAGE->set_pagelayout('admin'); - $PAGE->set_url('/badges/newbadge.php', array('type' => $type)); - $PAGE->set_heading($title); - $PAGE->set_title($title); -} - -require_capability('moodle/badges:createbadge', $PAGE->context); - -$fordb = new stdClass(); -$fordb->id = null; - -$form = new \core_badges\form\badge($PAGE->url, array('action' => 'new')); - -if ($form->is_cancelled()) { - redirect(new moodle_url('/badges/index.php', array('type' => $type, 'id' => $courseid))); -} else if ($data = $form->get_data()) { - // Creating new badge here. - $now = time(); - - $fordb->name = $data->name; - $fordb->version = $data->version; - $fordb->language = $data->language; - $fordb->description = $data->description; - $fordb->imageauthorname = $data->imageauthorname; - $fordb->imageauthoremail = $data->imageauthoremail; - $fordb->imageauthorurl = $data->imageauthorurl; - $fordb->imagecaption = $data->imagecaption; - $fordb->timecreated = $now; - $fordb->timemodified = $now; - $fordb->usercreated = $USER->id; - $fordb->usermodified = $USER->id; - - if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) { - $fordb->issuername = $data->issuername; - $fordb->issuerurl = $data->issuerurl; - $fordb->issuercontact = $data->issuercontact; - } else { - $url = parse_url($CFG->wwwroot); - $fordb->issuerurl = $url['scheme'] . '://' . $url['host']; - $fordb->issuername = $CFG->badges_defaultissuername; - $fordb->issuercontact = $CFG->badges_defaultissuercontact; - } - - $fordb->expiredate = ($data->expiry == 1) ? $data->expiredate : null; - $fordb->expireperiod = ($data->expiry == 2) ? $data->expireperiod : null; - $fordb->type = $type; - $fordb->courseid = ($type == BADGE_TYPE_COURSE) ? $courseid : null; - $fordb->messagesubject = get_string('messagesubject', 'badges'); - $fordb->message = get_string('messagebody', 'badges', - html_writer::link($CFG->wwwroot . '/badges/mybadges.php', get_string('managebadges', 'badges'))); - $fordb->attachment = 1; - $fordb->notification = BADGE_MESSAGE_NEVER; - $fordb->status = BADGE_STATUS_INACTIVE; - - $newid = $DB->insert_record('badge', $fordb, true); - - // Trigger event, badge created. - $eventparams = array('objectid' => $newid, 'context' => $PAGE->context); - $event = \core\event\badge_created::create($eventparams); - $event->trigger(); - - $newbadge = new badge($newid); - core_tag_tag::set_item_tags('core_badges', 'badge', $newid, $PAGE->context, $data->tags); - badges_process_badge_image($newbadge, $form->save_temp_file('image')); - // If a user can configure badge criteria, they will be redirected to the criteria page. - if (has_capability('moodle/badges:configurecriteria', $PAGE->context)) { - redirect(new moodle_url('/badges/criteria.php', array('id' => $newid))); - } - redirect(new moodle_url('/badges/overview.php', array('id' => $newid))); -} - -echo $OUTPUT->header(); -echo $OUTPUT->box('', 'notifyproblem hide', 'check_connection'); - -$form->display(); - -echo $OUTPUT->footer(); +$newpageurl = new moodle_url('/badges/edit.php', ['courseid' => $courseid, 'action' => 'new']); +redirect($newpageurl, get_string('newbadgedeprecated', 'core_badges')); diff --git a/badges/overview.php b/badges/overview.php index 87ea1d76948ee..e401eb5264288 100644 --- a/badges/overview.php +++ b/badges/overview.php @@ -74,9 +74,9 @@ echo $OUTPUT->heading(print_badge_image($badge, $context, 'small') . ' ' . $badge->name); if ($awards == 'cron') { - echo $OUTPUT->notification(get_string('awardoncron', 'badges'), 'notifysuccess'); + echo $OUTPUT->notification(get_string('awardoncron', 'badges', ['badgename' => $badge->name]), 'info'); } else if ((int)$awards > 0) { - echo $OUTPUT->notification(get_string('numawardstat', 'badges', $awards), 'notifysuccess'); + echo $OUTPUT->notification(get_string('numawardstat', 'badges', ['badgename' => $badge->name, 'awards' => $awards]), 'info'); } echo $output->print_badge_status_box($badge); echo $output->print_badge_overview($badge, $context); diff --git a/badges/renderer.php b/badges/renderer.php index 8740f66f0f350..050f7c1c52d2a 100644 --- a/badges/renderer.php +++ b/badges/renderer.php @@ -201,6 +201,11 @@ public function print_badge_overview($badge, $context) { $dl = array(); $dl[get_string('issuername', 'badges')] = $badge->issuername; $dl[get_string('contact', 'badges')] = html_writer::tag('a', $badge->issuercontact, array('href' => 'mailto:' . $badge->issuercontact)); + $dl[get_string('issuerurl', 'badges')] = html_writer::tag( + 'a', + $badge->issuerurl, + ['href' => $badge->issuerurl, 'target' => '_blank'], + ); $display .= $this->definition_list($dl); // Issuance details if any. @@ -242,6 +247,7 @@ public function print_badge_overview($badge, $context) { if ($badge->has_awards()) { $url = new moodle_url('/badges/recipients.php', array('id' => $badge->id)); $a = new stdClass(); + $a->badgename = $badge->name; $a->link = $url->out(); $a->count = count($badge->get_awards()); $display .= get_string('numawards', 'badges', $a); @@ -573,17 +579,34 @@ public function print_badge_status_box(badge $badge) { $message = $status . $action; } else { + $this->page->requires->js_call_amd('core_badges/actions', 'init'); + $status = get_string('statusmessage_' . $badge->status, 'badges'); if ($badge->is_active()) { - $action = $this->output->single_button(new moodle_url('/badges/action.php', - array('id' => $badge->id, 'lock' => 1, 'sesskey' => sesskey(), - 'return' => $this->page->url->out_as_local_url(false))), - get_string('deactivate', 'badges'), 'POST', array('class' => 'activatebadge')); + $action = $this->output->single_button( + new moodle_url('#'), + get_string('deactivate', 'badges'), + 'POST', + [ + 'class' => 'activatebadge', + 'data-action' => 'disablebadge', + 'data-badgeid' => $badge->id, + 'data-badgename' => $badge->name, + 'data-courseid' => $badge->courseid, + ], + ); } else { - $action = $this->output->single_button(new moodle_url('/badges/action.php', - array('id' => $badge->id, 'activate' => 1, 'sesskey' => sesskey(), - 'return' => $this->page->url->out_as_local_url(false))), - get_string('activate', 'badges'), 'POST', array('class' => 'activatebadge')); + $action = $this->output->single_button( + new moodle_url('#'), + get_string('activate', 'badges'), + 'POST', + [ + 'class' => 'activatebadge', + 'data-action' => 'enablebadge', + 'data-badgeid' => $badge->id, + 'data-badgename' => $badge->name, + 'data-courseid' => $badge->courseid, + ]); } $message = $status . $this->output->help_icon('status', 'badges') . $action; diff --git a/badges/tests/badge_test.php b/badges/tests/badge_test.php new file mode 100644 index 0000000000000..b98d6b53e683d --- /dev/null +++ b/badges/tests/badge_test.php @@ -0,0 +1,298 @@ +. + +declare(strict_types=1); + +namespace core_badges; + +use core_badges_generator; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once("{$CFG->libdir}/badgeslib.php"); + +/** + * Unit tests for badge class. + * + * @package core_badges + * @covers \core_badges\badge + * @copyright 2024 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class badge_test extends \advanced_testcase { + + /** + * Test create_badge. + * + * @dataProvider badges_provider + * @param bool $iscourse Whether the badge is a course badge or not. + * @param array $data Badge data. It will override the default data. + */ + public function test_create_badge(bool $iscourse = false, array $data = []): void { + global $DB; + + $this->resetAfterTest(); + + $courseid = null; + if ($iscourse) { + $course = $this->getDataGenerator()->create_course(); + $courseid = (int) $course->id; + } + + $user1 = $this->getDataGenerator()->create_user(); + $this->setUser($user1); + + // Check no badges exist. + $this->assertEquals(0, $DB->count_records('badge')); + + $data = (object) array_merge($this->get_badge(), $data); + + // Trigger and capture events. + $sink = $this->redirectEvents(); + + $badge = badge::create_badge($data, $courseid); + // Check the badge was created with the correct data. + $this->assertEquals(1, $DB->count_records('badge')); + $this->assertNotEmpty($badge->id); + if ($iscourse) { + $this->assertEquals(BADGE_TYPE_COURSE, $badge->type); + $this->assertEquals($course->id, $badge->courseid); + } else { + $this->assertEquals(BADGE_TYPE_SITE, $badge->type); + $this->assertNull($badge->courseid); + } + // Badges are always inactive by default, regardless the given status. + $this->assertEquals(BADGE_STATUS_INACTIVE, $badge->status); + + if (property_exists($data, 'tags')) { + $this->assertEquals($data->tags, $badge->get_badge_tags()); + } + + // Check that the event was triggered. + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\badge_created', $event); + $this->assertEquals($badge->usercreated, $event->userid); + $this->assertEquals($badge->id, $event->objectid); + $this->assertDebuggingNotCalled(); + $sink->close(); + } + + /** + * Test update() in badge class. + * + * @dataProvider badges_provider + * @param bool $iscourse Whether the badge is a course badge or not. + * @param array $data Badge data to update the badge with. It will override the default data. + */ + public function test_udpate_badge(bool $iscourse = false, array $data = []): void { + global $USER, $DB; + + $this->resetAfterTest(); + + $record = []; + if ($iscourse) { + $course = $this->getDataGenerator()->create_course(); + $record['type'] = BADGE_TYPE_COURSE; + $record['courseid'] = $course->id; + } + + /** @var core_badges_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_badges'); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $this->setUser($user1); + + /** @var badge $badge */ + $badge = $generator->create_badge($record); + $data = (object) array_merge($this->get_badge(), $data); + + // Check the badge has been created. + $this->assertEquals(1, $DB->count_records('badge')); + $this->assertNotEquals($data->name, $badge->name); + $this->assertEmpty($badge->get_badge_tags()); + + // Trigger and capture events. + $sink = $this->redirectEvents(); + + $this->setUser($user2); + $this->assertTrue($badge->update($data)); + // Check the badge was updated with the correct data. + $this->assertEquals(1, $DB->count_records('badge')); + $this->assertNotEmpty($badge->id); + $this->assertEquals($data->name, $badge->name); + if ($iscourse) { + $this->assertEquals(BADGE_TYPE_COURSE, $badge->type); + $this->assertEquals($course->id, $badge->courseid); + } else { + $this->assertEquals(BADGE_TYPE_SITE, $badge->type); + $this->assertNull($badge->courseid); + } + $this->assertEquals(BADGE_STATUS_ACTIVE, $badge->status); + $this->assertEquals($USER->id, $badge->usermodified); + + if (property_exists($data, 'tags')) { + $this->assertEquals($data->tags, $badge->get_badge_tags()); + } + + // Check that the event was triggered. + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\badge_updated', $event); + $this->assertEquals($badge->usermodified, $event->userid); + $this->assertEquals($badge->id, $event->objectid); + $this->assertDebuggingNotCalled(); + $sink->close(); + } + + /** + * Test update_message() in badge class. + * + * @dataProvider badges_provider + * @param bool $iscourse Whether the badge is a course badge or not. + * @param array $data Badge data to update the badge with. It will override the default data. + */ + public function test_udpate_message_badge(bool $iscourse = false, array $data = []): void { + global $USER, $DB; + + $this->resetAfterTest(); + + $record = []; + if ($iscourse) { + $course = $this->getDataGenerator()->create_course(); + $record['type'] = BADGE_TYPE_COURSE; + $record['courseid'] = $course->id; + } + + /** @var core_badges_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_badges'); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $this->setUser($user1); + + /** @var badge $badge */ + $badge = $generator->create_badge($record); + $data = (object) array_merge($this->get_badge(), $data); + + // Check the badge has been created. + $this->assertEquals(1, $DB->count_records('badge')); + $this->assertNotEquals($data->name, $badge->name); + $this->assertNotEquals($data->messagesubject, $badge->messagesubject); + $this->assertNotEquals($data->message_editor['text'], $badge->message); + $this->assertEmpty($badge->get_badge_tags()); + + // Trigger and capture events. + $sink = $this->redirectEvents(); + + $this->setUser($user2); + $this->assertTrue($badge->update_message($data)); + // Check the badge was updated with the correct data. + $this->assertEquals(1, $DB->count_records('badge')); + $this->assertNotEmpty($badge->id); + $this->assertNotEquals($data->name, $badge->name); + $this->assertEquals($data->messagesubject, $badge->messagesubject); + $this->assertEquals($data->message_editor['text'], $badge->message); + if ($iscourse) { + $this->assertEquals(BADGE_TYPE_COURSE, $badge->type); + $this->assertEquals($course->id, $badge->courseid); + } else { + $this->assertEquals(BADGE_TYPE_SITE, $badge->type); + $this->assertNull($badge->courseid); + } + $this->assertEquals(BADGE_STATUS_ACTIVE, $badge->status); + $this->assertEquals($user1->id, $badge->usermodified); + + // Check that the event was triggered. + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\badge_updated', $event); + $this->assertEquals($USER->id, $event->userid); + $this->assertEquals($badge->id, $event->objectid); + $this->assertDebuggingNotCalled(); + $sink->close(); + } + + /** + * Data provider for badge tests. + * + * @return array + */ + public static function badges_provider(): array { + return [ + 'Site badge' => [ + ], + 'Site badge with tags' => [ + 'iscourse' => false, + 'data' => [ + 'tags' => ['tag1', 'tag2'], + ], + ], + 'Course badge' => [ + 'iscourse' => true, + ], + ]; + } + + /** + * Get default badge data for testing purpose. + * + * @return array Badge data. + */ + private function get_badge(): array { + global $USER; + + return [ + 'name' => 'My test badge', + 'description' => 'Testing badge description', + 'timecreated' => time(), + 'timemodified' => time(), + 'usercreated' => $USER->id, + 'usermodified' => $USER->id, + 'issuername' => 'Test issuer', + 'issuerurl' => 'http://issuer-url.domain.co.nz', + 'issuercontact' => 'issuer@example.com', + 'expiry' => 0, + 'expiredate' => null, + 'expireperiod' => null, + 'type' => BADGE_TYPE_SITE, + 'courseid' => null, + 'messagesubject' => 'The new test message subject', + 'messageformat' => '1', + 'message_editor' => [ + 'text' => 'The new test message body', + ], + 'attachment' => 1, + 'notification' => 0, + 'status' => BADGE_STATUS_ACTIVE_LOCKED, + 'version' => OPEN_BADGES_V2, + 'language' => 'en', + 'imageauthorname' => 'Image author', + 'imageauthoremail' => 'author@example.com', + 'imageauthorurl' => 'http://image.example.com/', + 'imagecaption' => 'Image caption', + 'tags' => [], + ]; + } +} diff --git a/badges/tests/badgeslib_test.php b/badges/tests/badgeslib_test.php index 834c026fa5c67..1b14b5a961f8b 100644 --- a/badges/tests/badgeslib_test.php +++ b/badges/tests/badgeslib_test.php @@ -14,16 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Unit tests for badges - * - * @package core - * @subpackage badges - * @copyright 2013 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @author Yuliya Bozhko - */ - defined('MOODLE_INTERNAL') || die(); global $CFG; @@ -33,6 +23,14 @@ use core_badges\helper; use core\task\manager; +/** + * Unit tests for badges + * + * @package core_badges + * @copyright 2013 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Yuliya Bozhko + */ class badgeslib_test extends advanced_testcase { protected $badgeid; protected $course; @@ -907,10 +905,10 @@ public function test_badges_assertion(): void { $badge = new badge($this->coursebadge); $this->assertFalse($badge->is_issued($this->user->id)); - $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id)); - $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY)); - $criteria_overall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE, 'badgeid' => $badge->id)); - $criteria_overall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address')); + $criteriaoverall = award_criteria::build(['criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id]); + $criteriaoverall->save(['agg' => BADGE_CRITERIA_AGGREGATION_ANY]); + $criteriaoverall1 = award_criteria::build(['criteriatype' => BADGE_CRITERIA_TYPE_PROFILE, 'badgeid' => $badge->id]); + $criteriaoverall1->save(['agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address']); $this->user->address = 'Test address'; $sink = $this->redirectEmails(); @@ -922,17 +920,7 @@ public function test_badges_assertion(): void { $awards = $badge->get_awards(); $this->assertCount(1, $awards); - // Get assertion. - $award = reset($awards); - $assertion = new core_badges_assertion($award->uniquehash, OPEN_BADGES_V1); - $testassertion = $this->assertion; - - // Make sure JSON strings have the same structure. - $this->assertStringMatchesFormat($testassertion->badge, json_encode($assertion->get_badge_assertion())); - $this->assertStringMatchesFormat($testassertion->class, json_encode($assertion->get_badge_class())); - $this->assertStringMatchesFormat($testassertion->issuer, json_encode($assertion->get_issuer())); - - // Test Openbadge specification version 2. + // Test Openbadge specification version 2.0. // Get assertion version 2. $award = reset($awards); $assertion2 = new core_badges_assertion($award->uniquehash, OPEN_BADGES_V2); diff --git a/badges/tests/behat/add_badge.feature b/badges/tests/behat/add_badge.feature index 561d661242da6..d5d8f0ec0258f 100644 --- a/badges/tests/behat/add_badge.feature +++ b/badges/tests/behat/add_badge.feature @@ -20,20 +20,22 @@ Feature: Add badges to the system Then I should see "There are currently no badges available for users to earn." @javascript @_file_upload - Scenario: Add a badge - Given I navigate to "Badges > Badges settings" in site administration - And I set the field "Badge issuer name" to "Test Badge Site" - And I set the field "Badge issuer email address" to "testuser@example.com" - And I press "Save changes" + Scenario: Add a site badge + Given the following config values are set as admin: + | badges_defaultissuername | Test Badge Site | + | badges_defaultissuercontact | testuser@example.com | And I navigate to "Badges > Add a new badge" in site administration + And the field "Issuer name" matches value "Test Badge Site" + And the field "Issuer contact" matches value "testuser@example.com" And I set the following fields to these values: - | Name | Test badge with 'apostrophe' and other friends (<>&@#) | - | Version | v1 | - | Language | English | - | Description | Test badge description | - | Image author | http://author.example.com | - | Image caption | Test caption image | - | Tags | Math, Physics | + | Name | Test badge with 'apostrophe' and other friends (<>&@#) | + | Version | v1 | + | Language | English | + | Description | Test badge description | + | Image author | http://author.example.com | + | Image caption | Test caption image | + | Tags | Math, Physics | + | Issuer contact | issuer@example.com | And I upload "badges/tests/behat/badge.png" file to "Image" filemanager When I press "Create badge" Then I should see "Edit details" @@ -42,11 +44,11 @@ Feature: Add badges to the system And I should see "Related badges (0)" And I should see "Alignments (0)" And I should not see "Create badge" - And I should not see "Issuer details" And I select "Overview" from the "jump" singleselect And I should see "Issuer details" And I should see "Test Badge Site" - And I should see "testuser@example.com" + And I should see "issuer@example.com" + And I should not see "testuser@example.com" And I should see "Tags" And I should see "Math" And I should see "Physics" @@ -172,37 +174,49 @@ Feature: Add badges to the system And I should see "Add a new badge" @javascript @_file_upload - Scenario: Edit a badge - Given I navigate to "Badges > Badges settings" in site administration - And I set the field "Badge issuer name" to "Test Badge Site" - And I set the field "Badge issuer email address" to "testuser@example.com" - And I press "Save changes" - And I navigate to "Badges > Add a new badge" in site administration - And I set the following fields to these values: - | Name | Test badge with 'apostrophe' and other friends (<>&@#) | - | Version | firstversion | - | Language | English | - | Description | Test badge description | - | Image author | http://author.example.com | - | Image caption | Test caption image | - | Tags | Math, Physics | - And I upload "badges/tests/behat/badge.png" file to "Image" filemanager - And I press "Create badge" - When I select "Edit details" from the "jump" singleselect - And I should see "Test badge with 'apostrophe' and other friends (&@#)" - And I should not see "Issuer details" - And I should see "Math" - And I should see "Physics" + Scenario: Edit a site badge + Given the following "core_badges > Badge" exists: + | name | Site badge | + | status | inactive | + | version | 1 | + | language | ca | + | description | Test badge description | + | image | badges/tests/behat/badge.png | + | imageauthorurl | http://imtheauthor.example.com | + | imagecaption | My caption image | + | issuercontact | testuser@example.com | + And the following "core_badges > Criterias" exist: + | badge | role | + | Site badge | editingteacher | + And I navigate to "Badges > Manage badges" in site administration + When I press "Edit" action in the "Site badge" report row + And I should see "Site badge" + And the field "Issuer contact" matches value "testuser@example.com" And I set the following fields to these values: - | Name | Test badge renamed | - | Version | secondversion | - | Tags | Math, History | + | Name | Test badge with 'apostrophe' and other friends (<>&@#) | + | Version | secondversion | + | Language | English | + | Description | Modified test badge description | + | Image author | http://author.example.com | + | Image caption | Test caption image | + | Tags | Math, History | + | Issuer contact | issuer@invalid.cat | And I press "Save changes" And I select "Overview" from the "jump" singleselect - Then I should not see "Test badge with 'apostrophe' and other friends (&@#)" - And I should not see "firstversion" - And I should not see "Math, Physics" - And I should see "Test badge renamed" + And I expand all fieldsets + Then I should see "Test badge with 'apostrophe' and other friends (&@#)" + And I should not see "Site badge" And I should see "secondversion" + And I should not see "firstversion" And I should see "Math" And I should see "History" + And I should see "issuer@invalid.cat" + And I should not see "testuser@example.com" + + Scenario: Default value for issuer name + When I navigate to "Badges > Add a new badge" in site administration + Then the field "Issuer name" matches value "Acceptance test site" + But the following config values are set as admin: + | badges_defaultissuername | Test Badge Site | + And I navigate to "Badges > Add a new badge" in site administration + And the field "Issuer name" matches value "Test Badge Site" diff --git a/badges/tests/behat/award_badge.feature b/badges/tests/behat/award_badge.feature index 2ce8fcbfe37b9..3a6a99a951109 100644 --- a/badges/tests/behat/award_badge.feature +++ b/badges/tests/behat/award_badge.feature @@ -29,37 +29,23 @@ Feature: Award badges @javascript Scenario: Award badge on other badges as criteria - Given I log in as "teacher1" - And I am on "Course 1" course homepage - # Create course badge 1. - And I navigate to "Badges > Add a new badge" in current page administration - And I set the following fields to these values: - | Name | Course Badge 1 | - | Description | Course badge 1 description | - And I upload "badges/tests/behat/badge.png" file to "Image" filemanager - And I press "Create badge" - And I set the field "type" to "Manual issue by role" - And I expand all fieldsets - # Set to ANY of the roles awards badge. - And I set the field "Teacher" to "1" - And I set the field "Any of the selected roles awards the badge" to "1" - And I press "Save" - And I press "Enable access" - And I press "Continue" - # Badge #2 - And I am on "Course 1" course homepage - And I navigate to "Badges > Add a new badge" in current page administration - And I set the following fields to these values: - | Name | Course Badge 2 | - | Description | Course badge 2 description | - And I upload "badges/tests/behat/badge.png" file to "Image" filemanager - And I press "Create badge" - # Set "course badge 1" as criteria + Given the following "core_badges > Badges" exist: + | name | course | description | image | status | type | + | Course Badge 1 | C1 | Course badge 1 description | badges/tests/behat/badge.png | active | 2 | + | Course Badge 2 | C1 | Course badge 2 description | badges/tests/behat/badge.png | 0 | 2 | + And the following "core_badges > Criteria" exists: + | badge | Course Badge 1 | + | role | editingteacher | + And I am on the "Course 1" "course" page logged in as "teacher1" + And I navigate to "Badges > Manage badges" in current page administration + And I follow "Course Badge 2" + And I select "Criteria" from the "jump" singleselect + # Set "course badge 1" as criteria for Badge 2. And I set the field "type" to "Awarded badges" And I set the field "id_badge_badges" to "Course Badge 1" And I press "Save" And I press "Enable access" - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" And I am on "Course 1" course homepage And I navigate to "Badges > Manage badges" in current page administration And I follow "Course Badge 1" @@ -110,13 +96,16 @@ Feature: Award badges @javascript Scenario: Award profile badge + Given the following "core_badges > Badge" exists: + | name | Profile Badge | + | description | Test badge description | + | image | badges/tests/behat/badge.png | + | status | 0 | + | type | 1 | Given I log in as "admin" - And I navigate to "Badges > Add a new badge" in site administration - And I set the following fields to these values: - | Name | Profile Badge | - | Description | Test badge description | - And I upload "badges/tests/behat/badge.png" file to "Image" filemanager - And I press "Create badge" + And I navigate to "Badges > Manage badges" in site administration + And I follow "Profile Badge" + And I select "Criteria" from the "jump" singleselect And I set the field "type" to "Profile completion" And I expand all fieldsets And I set the field "First name" to "1" @@ -131,7 +120,7 @@ Feature: Award badges And I should see "Criterion description" And I should not see "Criteria for this badge have not been set up yet." And I press "Enable access" - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" And I open my profile in edit mode And I expand all fieldsets And I set the field "Phone" to "123456789" @@ -142,18 +131,18 @@ Feature: Award badges @javascript Scenario: Award site badge - Given I log in as "admin" - And I navigate to "Badges > Add a new badge" in site administration - And I set the following fields to these values: - | Name | Site Badge | - | Description | Site badge description | - And I upload "badges/tests/behat/badge.png" file to "Image" filemanager - And I press "Create badge" - And I set the field "type" to "Manual issue by role" - And I set the field "Teacher" to "1" - And I press "Save" - And I press "Enable access" - And I press "Continue" + Given the following "core_badges > Badge" exists: + | name | Site Badge | + | description | Site badge description | + | image | badges/tests/behat/badge.png | + | status | active | + | type | 1 | + And the following "core_badges > Criteria" exists: + | badge | Site Badge | + | role | editingteacher | + And I log in as "admin" + And I navigate to "Badges > Manage badges" in site administration + And I follow "Site Badge" And I select "Recipients (0)" from the "jump" singleselect And I press "Award badge" And I set the field "potentialrecipients[]" to "Teacher 1 (teacher1@example.com)" @@ -170,19 +159,19 @@ Feature: Award badges @javascript Scenario: Award course badge - Given I log in as "teacher1" - And I am on "Course 1" course homepage - And I navigate to "Badges > Add a new badge" in current page administration - And I set the following fields to these values: - | Name | Course Badge | - | Description | Course badge description | - And I upload "badges/tests/behat/badge.png" file to "Image" filemanager - And I press "Create badge" - And I set the field "type" to "Manual issue by role" - And I set the field "Teacher" to "1" - And I press "Save" - And I press "Enable access" - And I press "Continue" + Given the following "core_badges > Badge" exists: + | name | Course Badge | + | course | C1 | + | description | Course badge description | + | image | badges/tests/behat/badge.png | + | status | active | + | type | 2 | + And the following "core_badges > Criteria" exists: + | badge | Course Badge | + | role | editingteacher | + And I am on the "Course 1" "course" page logged in as "teacher1" + And I navigate to "Badges > Manage badges" in current page administration + And I follow "Course Badge" And I select "Recipients (0)" from the "jump" singleselect And I press "Award badge" And I set the field "potentialrecipients[]" to "Student 2 (student2@example.com)" @@ -202,20 +191,22 @@ Feature: Award badges @javascript Scenario: Award badge on activity completion - Given I log in as "teacher1" - And I am on "Course 1" course homepage - And I change window size to "large" - And I navigate to "Badges > Add a new badge" in current page administration - And I set the following fields to these values: - | Name | Course Badge | - | Description | Course badge description | - And I upload "badges/tests/behat/badge.png" file to "Image" filemanager - And I press "Create badge" + Given the following "core_badges > Badge" exists: + | name | Course Badge | + | course | C1 | + | description | Course badge description | + | image | badges/tests/behat/badge.png | + | status | 0 | + | type | 2 | + And I am on the "Course 1" "course" page logged in as "teacher1" + And I navigate to "Badges > Manage badges" in current page administration + And I follow "Course Badge" + And I select "Criteria" from the "jump" singleselect And I set the field "type" to "Activity completion" And I set the field "Test assignment name" to "1" And I press "Save" And I press "Enable access" - When I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" And I log out And I log in as "student1" And I follow "Profile" in the user menu @@ -236,25 +227,28 @@ Feature: Award badges | section | 1 | | completion | 2 | | completionview | 1 | - And I log in as "teacher1" - And I am on "Course 1" course homepage + Given the following "core_badges > Badge" exists: + | name | Course Badge | + | course | C1 | + | description | Course badge description | + | image | badges/tests/behat/badge.png | + | status | 0 | + | type | 2 | + And I am on the "Course 1" "course" page logged in as "teacher1" And I navigate to "Course completion" in current page administration And I set the field "id_overall_aggregation" to "2" And I click on "Condition: Activity completion" "link" And I set the field "Assignment - Music history" to "1" And I press "Save changes" And I am on "Course 1" course homepage - And I navigate to "Badges > Add a new badge" in current page administration - And I set the following fields to these values: - | Name | Course Badge | - | Description | Course badge description | - And I upload "badges/tests/behat/badge.png" file to "Image" filemanager - And I press "Create badge" + And I navigate to "Badges > Manage badges" in current page administration + And I follow "Course Badge" + And I select "Criteria" from the "jump" singleselect And I set the field "type" to "Course completion" And I set the field with xpath ".//*[contains(., 'Minimum grade required')]/ancestor::*[contains(concat(' ', @class, ' '), ' fitem ')]//input[1]" to "0" And I press "Save" And I press "Enable access" - When I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" And I log out And I log in as "student1" And I follow "Profile" in the user menu @@ -276,23 +270,19 @@ Feature: Award badges @javascript Scenario: All of the selected roles can award badges - Given I log in as "teacher1" - And I am on "Course 1" course homepage - # Create course badge 1. - And I navigate to "Badges > Add a new badge" in current page administration - And I set the following fields to these values: - | Name | Course Badge 1 | - | Description | Course badge description | - And I upload "badges/tests/behat/badge.png" file to "Image" filemanager - And I press "Create badge" - And I set the field "type" to "Manual issue by role" - And I expand all fieldsets - # Set to ANY of the roles awards badge. - And I set the field "Teacher" to "1" - And I set the field "Any of the selected roles awards the badge" to "1" - And I press "Save" - And I press "Enable access" - And I press "Continue" + Given the following "core_badges > Badge" exists: + | name | Course Badge 1 | + | course | C1 | + | description | Course badge description | + | image | badges/tests/behat/badge.png | + | status | active | + | type | 2 | + And the following "core_badges > Criteria" exists: + | badge | Course Badge 1 | + | role | editingteacher | + And I am on the "Course 1" "course" page logged in as "teacher1" + And I navigate to "Badges > Manage badges" in current page administration + And I follow "Course Badge 1" And I select "Recipients (0)" from the "jump" singleselect And I press "Award badge" # Award course badge 1 to student 1. @@ -304,13 +294,16 @@ Feature: Award badges And I select "Recipients (1)" from the "jump" singleselect Then I should see "Recipients (1)" # Add course badge 2. - And I am on "Course 1" course homepage - And I navigate to "Badges > Add a new badge" in current page administration - And I set the following fields to these values: - | Name | Course Badge 2 | - | Description | Course badge description | - And I upload "badges/tests/behat/badge.png" file to "Image" filemanager - And I press "Create badge" + Given the following "core_badges > Badge" exists: + | name | Course Badge 2 | + | course | C1 | + | description | Course badge description | + | image | badges/tests/behat/badge.png | + | status | 0 | + | type | 2 | + And I navigate to "Badges > Manage badges" in current page administration + And I follow "Course Badge 2" + And I select "Criteria" from the "jump" singleselect And I set the field "type" to "Manual issue by role" And I expand all fieldsets # Set to ALL of the selected roles award badge. @@ -318,7 +311,7 @@ Feature: Award badges And I set the field "All of the selected roles award the badge" to "1" And I press "Save" And I press "Enable access" - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" And I select "Recipients (0)" from the "jump" singleselect And I press "Award badge" # Award course badge 2 to student 2. @@ -347,19 +340,19 @@ Feature: Award badges @javascript Scenario: Revoke badge - Given I log in as "teacher1" - And I am on "Course 1" course homepage - And I navigate to "Badges > Add a new badge" in current page administration - And I set the following fields to these values: - | Name | Course Badge | - | Description | Course badge description | - And I upload "badges/tests/behat/badge.png" file to "Image" filemanager - And I press "Create badge" - And I set the field "type" to "Manual issue by role" - And I set the field "Teacher" to "1" - And I press "Save" - And I press "Enable access" - And I press "Continue" + Given the following "core_badges > Badge" exists: + | name | Course Badge | + | course | C1 | + | description | Course badge description | + | image | badges/tests/behat/badge.png | + | status | active | + | type | 2 | + And the following "core_badges > Criteria" exists: + | badge | Course Badge | + | role | editingteacher | + And I am on the "Course 1" "course" page logged in as "teacher1" + And I navigate to "Badges > Manage badges" in current page administration + And I follow "Course Badge" And I select "Recipients (0)" from the "jump" singleselect And I press "Award badge" And I set the field "potentialrecipients[]" to "Student 2 (student2@example.com)" diff --git a/badges/tests/behat/award_badge_groups.feature b/badges/tests/behat/award_badge_groups.feature index 8dfbe06169266..798787636ebf2 100644 --- a/badges/tests/behat/award_badge_groups.feature +++ b/badges/tests/behat/award_badge_groups.feature @@ -13,7 +13,7 @@ Feature: Award badges with separate groups | student2 | Student | 2 | student2@example.com | And the following "courses" exist: | fullname | shortname | category | groupmode | - | Course 1 | C1 | 0 | 1 | + | Course 1 | C1 | 0 | 1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | @@ -30,16 +30,17 @@ Feature: Award badges with separate groups | teacher1 | CB | | student2 | CA | | teacher2 | CA | - And I am on the "Course 1" "course editing" page logged in as "teacher1" - And I expand all fieldsets - And I set the field "Group mode" to "Separate groups" - And I press "Save and display" - And I navigate to "Badges > Add a new badge" in current page administration - And I set the following fields to these values: - | Name | Course Badge | - | Description | Course badge description | - And I upload "badges/tests/behat/badge.png" file to "Image" filemanager - And I press "Create badge" + And the following "core_badges > Badge" exists: + | name | Course Badge | + | course | C1 | + | description | Course badge description | + | image | badges/tests/behat/badge.png | + | status | 0 | + | type | 2 | + And I am on the "Course 1" "course" page logged in as "teacher1" + And I navigate to "Badges > Manage badges" in current page administration + And I follow "Course Badge" + And I select "Criteria" from the "jump" singleselect And I set the field "type" to "Manual issue by role" And I expand all fieldsets And I set the field "Teacher" to "1" @@ -48,8 +49,7 @@ Feature: Award badges with separate groups And I set the field "Any of the selected roles awards the badge" to "1" And I press "Save" And I press "Enable access" - And I press "Continue" - And I log out + And I click on "Enable" "button" in the "Confirm" "dialogue" @javascript Scenario: Award course badge as non-editing teacher with only one group diff --git a/badges/tests/behat/backpack.feature b/badges/tests/behat/backpack.feature index edebf072857cc..8a152fa991faf 100644 --- a/badges/tests/behat/backpack.feature +++ b/badges/tests/behat/backpack.feature @@ -150,13 +150,9 @@ Feature: Backpack badges Then "Include authentication details with the backpack" "checkbox" should not be visible And I should not see "Badge issuer email address" And I should not see "Badge issuer password" - And I set the field "apiversion" to "1" - And "Include authentication details with the backpack" "checkbox" should be visible - And I click on "includeauthdetails" "checkbox" - And I should see "Badge issuer email address" - And I should not see "Badge issuer password" And I set the field "apiversion" to "2" And "Include authentication details with the backpack" "checkbox" should be visible + And I click on "includeauthdetails" "checkbox" And I should see "Badge issuer email address" And I should see "Badge issuer password" And I set the field "backpackemail" to "test@test.com" diff --git a/badges/tests/behat/badge_navigation.feature b/badges/tests/behat/badge_navigation.feature index a057b29e55aad..7d80f81a42dac 100644 --- a/badges/tests/behat/badge_navigation.feature +++ b/badges/tests/behat/badge_navigation.feature @@ -86,7 +86,7 @@ Feature: Test tertiary navigation as various users And I navigate to "Badges" in current page administration And I click on "Manage badges" "button" And I press "Enable access" action in the "Testing course badge" report row - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" And I log out # Now student should see the Badges link. And I am on the "C1" "Course" page logged in as "student1" diff --git a/badges/tests/behat/criteria_activity.feature b/badges/tests/behat/criteria_activity.feature index 81459ae760928..0d9f8818b01d2 100644 --- a/badges/tests/behat/criteria_activity.feature +++ b/badges/tests/behat/criteria_activity.feature @@ -59,7 +59,7 @@ Feature: Award badges based on activity completion And I set the field "Quiz - Test quiz name 1" to "1" And I press "Save" And I press "Enable access" - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" And I should see "Recipients (0)" # Pass grade for student1. Activity is considered complete because student1 got a passing grade. And user "student1" has attempted "Test quiz name 1" with responses: @@ -85,7 +85,7 @@ Feature: Award badges based on activity completion And I set the field "Quiz - Test quiz name 2" to "1" And I press "Save" And I press "Enable access" - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" # Pass grade for student1. And user "student1" has attempted "Test quiz name 2" with responses: | slot | response | @@ -119,7 +119,7 @@ Feature: Award badges based on activity completion And I press "Save" # Enable badge access once students have completed the activity. When I press "Enable access" - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" # Only student1 should earn the badge because student2 did not pass the quiz. Then I should see "Recipients (1)" And I select "Recipients (1)" from the "jump" singleselect diff --git a/badges/tests/behat/criteria_cohort.feature b/badges/tests/behat/criteria_cohort.feature index 2670408553ee8..5fc5504a2e2fa 100644 --- a/badges/tests/behat/criteria_cohort.feature +++ b/badges/tests/behat/criteria_cohort.feature @@ -31,7 +31,7 @@ Feature: Award badges based on cohort And I set the field "id_cohort_cohorts" to "One Cohort" And I press "Save" And I press "Enable access" - When I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" Then I should see "Recipients (1)" And I log out And I log in as "user1" @@ -70,7 +70,7 @@ Feature: Award badges based on cohort And I set the field "id_agg_1" to "1" And I press "Save" When I press "Enable access" - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" Then I should see "Recipients (1)" And I log out And I log in as "user1" @@ -110,7 +110,7 @@ Feature: Award badges based on cohort And I set the field "id_cohort_cohorts" to "One Cohort" And I press "Save" When I press "Enable access" - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" Then I should see "Recipients (2)" And I log out And I log in as "user1" @@ -153,7 +153,7 @@ Feature: Award badges based on cohort And I set the field "Any of the selected roles awards the badge" to "1" And I press "Save" When I press "Enable access" - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" And I select "Recipients (0)" from the "jump" singleselect And I press "Award badge" And I set the field "potentialrecipients[]" to "First User (first@example.com)" @@ -207,7 +207,7 @@ Feature: Award badges based on cohort And I press "Save" And I set the field "update" to "Any" When I press "Enable access" - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" And I select "Recipients (1)" from the "jump" singleselect And I press "Award badge" And I set the field "potentialrecipients[]" to "First User (first@example.com)" @@ -266,7 +266,7 @@ Feature: Award badges based on cohort And I press "Save" And I set the field "update" to "Any" When I press "Enable access" - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" And I select "Recipients (1)" from the "jump" singleselect And I press "Award badge" And I set the field "potentialrecipients[]" to "First User (first@example.com)" @@ -327,7 +327,7 @@ Feature: Award badges based on cohort And I press "Save" And I set the field "update" to "All" When I press "Enable access" - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" And I select "Recipients (0)" from the "jump" singleselect And I press "Award badge" And I set the field "potentialrecipients[]" to "First User (first@example.com)" @@ -378,7 +378,7 @@ Feature: Award badges based on cohort And I set the field "id_cohort_cohorts" to "One Cohort" And I press "Save" And I press "Enable access" - When I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" And I should see "Recipients (1)" And I navigate to "Badges > Manage badges" in site administration And I press "Edit" action in the "Site Badge 2" report row @@ -387,7 +387,7 @@ Feature: Award badges based on cohort And I set the field "id_cohort_cohorts" to "Two Cohort" And I press "Save" And I press "Enable access" - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" Then I should see "Recipients (2)" And I log out And I log in as "user1" @@ -436,8 +436,8 @@ Feature: Award badges based on cohort And I set the field "id_cohort_cohorts" to "One Cohort,Two Cohort" And I set the field "id_agg_1" to "1" And I press "Save" - And I press "Enable access" - When I press "Continue" + When I press "Enable access" + And I click on "Enable" "button" in the "Confirm" "dialogue" And I should see "Recipients (1)" And I navigate to "Badges > Manage badges" in site administration And I press "Edit" action in the "Site Badge 2" report row @@ -448,7 +448,7 @@ Feature: Award badges based on cohort And I set the field "id_agg_1" to "1" And I press "Save" And I press "Enable access" - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" And I should see "Recipients (1)" And I log out And I log in as "user1" diff --git a/badges/tests/behat/criteria_competency.feature b/badges/tests/behat/criteria_competency.feature index 2404932b309a9..778468a4edec7 100644 --- a/badges/tests/behat/criteria_competency.feature +++ b/badges/tests/behat/criteria_competency.feature @@ -59,7 +59,7 @@ Feature: Award badges based on competency completion And I wait until the page is ready # Enable the badge And I press "Enable access" - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" # Rate the competency in the course And I am on "Course 1" course homepage And I navigate to "Competencies" in current page administration @@ -112,7 +112,7 @@ Feature: Award badges based on competency completion # Enable the badge And I wait until the page is ready And I press "Enable access" - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" # Rate the competency in the course And I am on "Course 1" course homepage And I navigate to "Competencies" in current page administration @@ -167,7 +167,7 @@ Feature: Award badges based on competency completion # Enable the badge And I wait until the page is ready And I press "Enable access" - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" # Rate the competency in the course And I am on "Course 1" course homepage And I navigate to "Competencies" in current page administration diff --git a/badges/tests/behat/criteria_profile.feature b/badges/tests/behat/criteria_profile.feature index a3ce8b55fc792..a3a5d90c2a565 100644 --- a/badges/tests/behat/criteria_profile.feature +++ b/badges/tests/behat/criteria_profile.feature @@ -22,7 +22,7 @@ Feature: Award badges based on user profile field And I set the field "id_field_picture" to "1" And I press "Save" And I press "Enable access" - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" And I log out When I log in as "user1" And I follow "Profile" in the user menu diff --git a/badges/tests/behat/manage_badges.feature b/badges/tests/behat/manage_badges.feature index 61648dc45257a..a4e5f1c6c641c 100644 --- a/badges/tests/behat/manage_badges.feature +++ b/badges/tests/behat/manage_badges.feature @@ -77,14 +77,14 @@ Feature: Manage badges And I press "Save" And I navigate to "Badges > Manage badges" in site administration And I press "Enable access" action in the "Badge #1" report row - And I should see "Changes in badge access" - And I press "Continue" - And I should see "Access to the badges was successfully enabled" + And I should see "This will make your badge visible to users and allow them to start earning it." + And I click on "Enable" "button" in the "Confirm" "dialogue" + And I should see "Access to badge 'Badge #1' enabled" Then the following should exist in the "reportbuilder-table" table: | Name | Badge status | | Badge #1 | Available | And I press "Disable access" action in the "Badge #1" report row - And I should see "Access to the badges was successfully disabled" + And I should see "Access to badge 'Badge #1' disabled" And the following should exist in the "reportbuilder-table" table: | Name | Badge status | | Badge #1 | Not available | @@ -99,7 +99,7 @@ Feature: Manage badges And I press "Save" And I navigate to "Badges > Manage badges" in site administration And I press "Enable access" action in the "Badge #1" report row - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" And I press "Award badge" action in the "Badge #1" report row And I set the field "potentialrecipients[]" to "Admin User (moodle@example.com)" And I press "Award badge" @@ -129,3 +129,33 @@ Feature: Manage badges | Badge #1 | Not available | 2 | | Badge #2 | Available | 1 | | Badge #3 | Available | 0 | + + @_file_upload + Scenario: Badge names are not unique anymore + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "core_badges > Badge" exists: + | name | Badge #2 | + | status | 0 | + | course | C1 | + | type | 1 | + | version | 1.0 | + | language | en | + | description | Test badge description | + | image | badges/tests/behat/badge.png | + | imageauthorurl | http://author.example.com | + | imagecaption | Test caption image | + And I log in as "admin" + And I navigate to "Badges > Add a new badge" in site administration + And I set the following fields to these values: + | name | Badge #1 | + | description | Test badge description | + And I upload "badges/tests/behat/badge.png" file to "Image" filemanager + When I press "Create badge" + Then I should see "Criteria for this badge have not been set up yet." + And I select "Edit details" from the "jump" singleselect + # Set name for a site badge with existing badge name in a course is also allowed. + And I set the field "name" to "Badge #2" + And I press "Save changes" + And I should see "Changes saved" diff --git a/badges/tests/behat/nobadge_navigation.feature b/badges/tests/behat/nobadge_navigation.feature index 714062d974da9..119e2d9298ce2 100644 --- a/badges/tests/behat/nobadge_navigation.feature +++ b/badges/tests/behat/nobadge_navigation.feature @@ -52,7 +52,7 @@ Feature: Manage badges is not shown when there are no existing badges. And I should not see "Testing course badge" And I click on "Manage badges" "button" And I press "Enable access" action in the "Testing course badge" report row - And I click on "Continue" "button" + And I click on "Enable" "button" in the "Confirm" "dialogue" And I should see "Testing course badge" And I click on "Back" "button" And "Manage badges" "button" should exist @@ -92,7 +92,7 @@ Feature: Manage badges is not shown when there are no existing badges. And I navigate to "Badges" in current page administration And I click on "Manage badges" "button" And I press "Enable access" action in the "Testing course badge" report row - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" And I log out # Now student should see the Badges link. And I am on the "C1" "Course" page logged in as "student1" diff --git a/badges/tests/behat/view_badge.feature b/badges/tests/behat/view_badge.feature index 2a82efa3efa0d..bb1bcddf34d33 100644 --- a/badges/tests/behat/view_badge.feature +++ b/badges/tests/behat/view_badge.feature @@ -9,29 +9,27 @@ Feature: Display badges | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | # Create system badge and define a criterion. + And the following "core_badges > Badge" exists: + | name | Testing system badge | + | status | inactive | + | version | 1.1 | + | language | ca | + | description | Testing system badge description | + | image | badges/tests/behat/badge.png | + | imageauthorurl | http://author.example.com | + | imagecaption | My caption image | + And the following "core_badges > Criterias" exist: + | badge | role | + | Testing system badge | editingteacher | And I log in as "admin" - And I navigate to "Badges > Add a new badge" in site administration - And I set the following fields to these values: - | Name | Testing system badge | - | Version | 1.1 | - | Language | Catalan | - | Description | Testing system badge description | - | Image author | http://author.example.com | - | Image caption | Test caption image | - And I upload "badges/tests/behat/badge.png" file to "Image" filemanager - And I press "Create badge" - And I set the field "type" to "Manual issue by role" - And I expand all fieldsets - And I set the field "Teacher" to "1" - And I press "Save" + And I navigate to "Badges > Manage badges" in site administration Scenario: Display badge without expired date # Enable the badge. - Given I press "Enable access" - And I press "Continue" + Given I press "Enable access" action in the "Testing system badge" report row + And I click on "Enable" "button" in the "Confirm" "dialogue" # Award badge to student1. - And I select "Recipients (0)" from the "jump" singleselect - And I press "Award badge" + When I press "Award badge" action in the "Testing system badge" report row And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)" And I press "Award badge" # Check badge details are displayed. @@ -49,11 +47,13 @@ Feature: Display badges Scenario: Display badge with ALL criteria # Add another criterion and enable the badge. - Given I set the field "type" to "Profile completion" + Given I follow "Testing system badge" + And I select "Criteria" from the "jump" singleselect + And I set the field "type" to "Profile completion" And I set the field "id_field_firstname" to "1" And I press "Save" And I press "Enable access" - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" # Award badge to student1. And I select "Recipients (0)" from the "jump" singleselect And I press "Award badge" @@ -63,7 +63,7 @@ Feature: Display badges And I navigate to "Badges > Manage badges" in site administration And I follow "Testing system badge" And I select "Recipients (1)" from the "jump" singleselect - And I press "View issued badge" action in the "Student 1" report row + When I press "View issued badge" action in the "Student 1" report row Then I should see "Awarded to Student 1" And I should see "Complete ALL of the listed requirements." And I should see "This badge has to be awarded by a user with the following role:" @@ -76,15 +76,17 @@ Feature: Display badges Scenario: Display badge with ANY criteria # Add another criterion and enable the badge. - Given I set the field "type" to "Profile completion" + Given I follow "Testing system badge" + And I select "Criteria" from the "jump" singleselect + And I set the field "type" to "Profile completion" And I set the field "id_field_firstname" to "1" And I press "Save" And I set the field "update" to "2" And I press "Enable access" - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" # Check badge details are displayed. And I select "Recipients (2)" from the "jump" singleselect - And I press "View issued badge" action in the "Student 1" report row + When I press "View issued badge" action in the "Student 1" report row Then I should see "Awarded to Student 1" And I should see "Complete ANY of the listed requirements." And I should see "This badge has to be awarded by a user with the following role:" @@ -97,12 +99,15 @@ Feature: Display badges Scenario: Display badge with expiration date but not expired yet # Set expired date to badge (future date). - Given I select "Edit details" from the "jump" singleselect + Given I press "Edit" action in the "Testing system badge" report row + And I expand all fieldsets When I click on "Relative date" "radio" And I set the field "expireperiod[number]" to "1" And I press "Save changes" + And I should see "Changes saved" + And I select "Overview" from the "jump" singleselect And I press "Enable access" - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" # Award badge to student1. And I select "Recipients (0)" from the "jump" singleselect And I press "Award badge" @@ -118,13 +123,16 @@ Feature: Display badges Scenario: Display expired badge # Set expired date to badge (relative date 1 seconds after the date of issue it). - Given I select "Edit details" from the "jump" singleselect + Given I press "Edit" action in the "Testing system badge" report row + And I expand all fieldsets When I click on "Relative date" "radio" And I set the field "expireperiod[timeunit]" to "1" And I set the field "expireperiod[number]" to "1" And I press "Save changes" + And I should see "Changes saved" + And I select "Overview" from the "jump" singleselect And I press "Enable access" - And I press "Continue" + And I click on "Enable" "button" in the "Confirm" "dialogue" # Award badge to student1. And I select "Recipients (0)" from the "jump" singleselect And I press "Award badge" diff --git a/badges/tests/external/disable_badges_test.php b/badges/tests/external/disable_badges_test.php new file mode 100644 index 0000000000000..397dc6f2a0cdb --- /dev/null +++ b/badges/tests/external/disable_badges_test.php @@ -0,0 +1,224 @@ +. + +namespace core_badges\external; + +use core_badges_generator; +use core_badges\badge; +use externallib_advanced_testcase; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); +require_once($CFG->libdir . '/badgeslib.php'); + +/** + * Tests for external function disable_badges. + * + * @package core_badges + * @copyright 2024 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 4.5 + * @covers \core_badges\external\disable_badges + */ +final class disable_badges_test extends externallib_advanced_testcase { + + /** + * Test execute method. + */ + public function test_execute(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $data = $this->prepare_test_data(); + + $this->assertTrue($data['sitebadge']->is_active()); + $this->assertTrue($data['coursebadge']->is_active()); + + $result = disable_badges::execute([ + $data['sitebadge']->id, + $data['coursebadge']->id, + ]); + $result = \core_external\external_api::clean_returnvalue(disable_badges::execute_returns(), $result); + $this->assertTrue($result['result']); + $this->assertEmpty($result['warnings']); + + $sitebadge = new badge($data['sitebadge']->id); + $coursebadge = new badge($data['coursebadge']->id); + $this->assertFalse($sitebadge->is_active()); + $this->assertFalse($coursebadge->is_active()); + } + + /** + * Test execute method when badges are disabled. + */ + public function test_execute_badgesdisabled(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + // Disable course badges. + set_config('enablebadges', 0); + + $data = $this->prepare_test_data(); + + $this->expectException(\moodle_exception::class); + $this->expectExceptionMessage(get_string('badgesdisabled', 'core_badges')); + $result = disable_badges::execute([ + $data['sitebadge']->id, + $data['coursebadge']->id, + ]); + $result = \core_external\external_api::clean_returnvalue(disable_badges::execute_returns(), $result); + } + + /** + * Test execute method when course badges are disabled. + */ + public function test_execute_coursebadgesdisabled(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + // Disable course badges. + set_config('badges_allowcoursebadges', 0); + + $data = $this->prepare_test_data(); + + $this->assertTrue($data['sitebadge']->is_active()); + $this->assertTrue($data['coursebadge']->is_active()); + + $result = disable_badges::execute([ + $data['sitebadge']->id, + $data['coursebadge']->id, + ]); + $result = \core_external\external_api::clean_returnvalue(disable_badges::execute_returns(), $result); + // Course badge can't be disabled because course badges are disabled. + $this->assertFalse($result['result']); + $this->assertNotEmpty($result['warnings']); + $this->assertEquals($data['coursebadge']->id, $result['warnings'][0]['item']); + $this->assertEquals('coursebadgesdisabled', $result['warnings'][0]['warningcode']); + + $sitebadge = new badge($data['sitebadge']->id); + $coursebadge = new badge($data['coursebadge']->id); + $this->assertFalse($sitebadge->is_active()); + $this->assertTrue($coursebadge->is_active()); + } + + /** + * Test execute method when the user doesn't have the capability to disable badges. + */ + public function test_execute_without_capability(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $data = $this->prepare_test_data(); + $this->setUser($data['teacher']); + + $this->assertTrue($data['sitebadge']->is_active()); + $this->assertTrue($data['coursebadge']->is_active()); + + $result = disable_badges::execute([ + $data['sitebadge']->id, + $data['coursebadge']->id, + ]); + $result = \core_external\external_api::clean_returnvalue(disable_badges::execute_returns(), $result); + // Teacher doesn't have capability to disable site badges. + $this->assertFalse($result['result']); + $this->assertNotEmpty($result['warnings']); + $this->assertEquals($data['sitebadge']->id, $result['warnings'][0]['item']); + $this->assertEquals('nopermissions', $result['warnings'][0]['warningcode']); + + $sitebadge = new badge($data['sitebadge']->id); + $coursebadge = new badge($data['coursebadge']->id); + $this->assertTrue($sitebadge->is_active()); + $this->assertFalse($coursebadge->is_active()); + } + + /** + * Test execute method when the badge is already disabled. + */ + public function test_execute_disabledisabledbadge(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $data = $this->prepare_test_data(); + + $this->assertTrue($data['coursebadge']->is_active()); + $data['coursebadge']->set_status(BADGE_STATUS_INACTIVE); + $this->assertFalse($data['coursebadge']->is_active()); + + $result = disable_badges::execute([ + $data['coursebadge']->id, + ]); + $result = \core_external\external_api::clean_returnvalue(disable_badges::execute_returns(), $result); + // Disabled badges can be disabled again. + $this->assertTrue($result['result']); + $this->assertEmpty($result['warnings']); + + $coursebadge = new badge($data['coursebadge']->id); + $this->assertFalse($coursebadge->is_active()); + } + + /** + * Prepare the test, creating a few users and badges. + * + * @return array Test data. + */ + private function prepare_test_data(): array { + global $DB; + + // Setup test data. + $course = $this->getDataGenerator()->create_course(); + + // Create users and enrolments. + $student1 = $this->getDataGenerator()->create_and_enrol($course); + $student2 = $this->getDataGenerator()->create_and_enrol($course); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); + + /** @var core_badges_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_badges'); + $sitebadge = $generator->create_badge([ + 'name' => 'Site badge', + 'description' => 'Site badge', + 'status' => BADGE_STATUS_ACTIVE, + ]); + $coursebadge = $generator->create_badge([ + 'name' => 'Course badge', + 'description' => 'Course badge', + 'type' => BADGE_TYPE_COURSE, + 'courseid' => $course->id, + 'status' => BADGE_STATUS_ACTIVE, + ]); + + // Create criteria for manually awarding by role. + $managerrole = $DB->get_field('role', 'id', ['shortname' => 'manager']); + $generator->create_criteria(['badgeid' => $sitebadge->id, 'roleid' => $managerrole]); + $generator->create_criteria(['badgeid' => $coursebadge->id, 'roleid' => $managerrole]); + + // Issue badges to student1. + $sitebadge->issue($student1->id, true); + $coursebadge->issue($student1->id, true); + + return [ + 'course' => $course, + 'student1' => $student1, + 'student2' => $student2, + 'teacher' => $teacher, + 'sitebadge' => $sitebadge, + 'coursebadge' => $coursebadge, + ]; + } +} diff --git a/badges/tests/external/enable_badges_test.php b/badges/tests/external/enable_badges_test.php new file mode 100644 index 0000000000000..891ac305bd601 --- /dev/null +++ b/badges/tests/external/enable_badges_test.php @@ -0,0 +1,312 @@ +. + +namespace core_badges\external; + +use core_badges_generator; +use core_badges\badge; +use externallib_advanced_testcase; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); +require_once($CFG->libdir . '/badgeslib.php'); + +/** + * Tests for external function enable_badges. + * + * @package core_badges + * @copyright 2024 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 4.5 + * @covers \core_badges\external\enable_badges + */ +final class enable_badges_test extends externallib_advanced_testcase { + + /** + * Test execute method. + */ + public function test_execute(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $data = $this->prepare_test_data(); + + $this->assertFalse($data['sitebadge']->is_active()); + $this->assertFalse($data['coursebadge']->is_active()); + + $result = enable_badges::execute([ + $data['sitebadge']->id, + $data['coursebadge']->id, + ]); + $result = \core_external\external_api::clean_returnvalue(enable_badges::execute_returns(), $result); + $this->assertEquals($data['sitebadge']->id, $result['result'][0]['badgeid']); + $this->assertEquals($data['coursebadge']->id, $result['result'][1]['badgeid']); + $this->assertEmpty($result['warnings']); + + $sitebadge = new badge($data['sitebadge']->id); + $coursebadge = new badge($data['coursebadge']->id); + $this->assertTrue($sitebadge->is_active()); + $this->assertTrue($coursebadge->is_active()); + } + + /** + * Test execute method when badges are disabled. + */ + public function test_execute_badgesdisabled(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + // Disable course badges. + set_config('enablebadges', 0); + + $data = $this->prepare_test_data(); + + $this->expectException(\moodle_exception::class); + $this->expectExceptionMessage(get_string('badgesdisabled', 'core_badges')); + $result = enable_badges::execute([ + $data['sitebadge']->id, + $data['coursebadge']->id, + ]); + $result = \core_external\external_api::clean_returnvalue(enable_badges::execute_returns(), $result); + } + + /** + * Test execute method when course badges are disabled. + */ + public function test_execute_coursebadgesdisabled(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + // Disable course badges. + set_config('badges_allowcoursebadges', 0); + + $data = $this->prepare_test_data(); + + $this->assertFalse($data['sitebadge']->is_active()); + $this->assertFalse($data['coursebadge']->is_active()); + + $result = enable_badges::execute([ + $data['sitebadge']->id, + $data['coursebadge']->id, + ]); + $result = \core_external\external_api::clean_returnvalue(enable_badges::execute_returns(), $result); + $this->assertEquals($data['sitebadge']->id, $result['result'][0]['badgeid']); + // Course badge can't be enabled because course badges are disabled. + $this->assertNotEmpty($result['warnings']); + $this->assertEquals($data['coursebadge']->id, $result['warnings'][0]['item']); + $this->assertEquals('coursebadgesdisabled', $result['warnings'][0]['warningcode']); + + $sitebadge = new badge($data['sitebadge']->id); + $coursebadge = new badge($data['coursebadge']->id); + $this->assertTrue($sitebadge->is_active()); + $this->assertFalse($coursebadge->is_active()); + } + + /** + * Test execute method when badge doesn't have criteria. + */ + public function test_execute_badgewithoutcriteria(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $data = $this->prepare_test_data(); + + $this->assertFalse($data['nocriteriabadge']->has_criteria()); + $this->assertFalse($data['nocriteriabadge']->is_active()); + + $result = enable_badges::execute([ + $data['nocriteriabadge']->id, + ]); + $result = \core_external\external_api::clean_returnvalue(enable_badges::execute_returns(), $result); + $this->assertEmpty($result['result']); + // Badges without criteria can't be enabled. + $this->assertNotEmpty($result['warnings']); + $this->assertEquals($data['nocriteriabadge']->id, $result['warnings'][0]['item']); + $this->assertEquals('nocriteria', $result['warnings'][0]['warningcode']); + + $nocriteriabadge = new badge($data['nocriteriabadge']->id); + $this->assertFalse($nocriteriabadge->is_active()); + } + + /** + * Test execute method when the badge is already enabled. + */ + public function test_execute_enableenabledbadge(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $data = $this->prepare_test_data(); + + $this->assertFalse($data['coursebadge']->is_active()); + $data['coursebadge']->set_status(BADGE_STATUS_ACTIVE); + $this->assertTrue($data['coursebadge']->is_active()); + + $result = enable_badges::execute([ + $data['coursebadge']->id, + ]); + $result = \core_external\external_api::clean_returnvalue(enable_badges::execute_returns(), $result); + $this->assertEquals($data['coursebadge']->id, $result['result'][0]['badgeid']); + // Enabled badges can be enabled again. + $this->assertEmpty($result['warnings']); + + $coursebadge = new badge($data['coursebadge']->id); + $this->assertTrue($coursebadge->is_active()); + } + + /** + * Test execute method when badgeid doesn't exist. + */ + public function test_execute_wrongbadgeid(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $badgeid = 1234; + $this->expectException(\moodle_exception::class); + $this->expectExceptionMessage(get_string('error:nosuchbadge', 'core_badges', $badgeid)); + $result = enable_badges::execute([ + $badgeid, + ]); + $result = \core_external\external_api::clean_returnvalue(enable_badges::execute_returns(), $result); + } + + /** + * Test execute method when the user doesn't have the capability to enable badges. + */ + public function test_execute_without_capability(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $data = $this->prepare_test_data(); + $this->setUser($data['teacher']); + + $this->assertFalse($data['sitebadge']->is_active()); + $this->assertFalse($data['coursebadge']->is_active()); + + $result = enable_badges::execute([ + $data['sitebadge']->id, + $data['coursebadge']->id, + ]); + $result = \core_external\external_api::clean_returnvalue(enable_badges::execute_returns(), $result); + $this->assertEquals($data['coursebadge']->id, $result['result'][0]['badgeid']); + // Teacher doesn't have capability to enable site badges. + $this->assertNotEmpty($result['warnings']); + $this->assertEquals($data['sitebadge']->id, $result['warnings'][0]['item']); + $this->assertEquals('nopermissions', $result['warnings'][0]['warningcode']); + + $sitebadge = new badge($data['sitebadge']->id); + $coursebadge = new badge($data['coursebadge']->id); + $this->assertFalse($sitebadge->is_active()); + $this->assertTrue($coursebadge->is_active()); + } + + /** + * Test execute method when the badge has the same name as another enabled badge. + */ + public function test_execute_duplicatedname(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $data = $this->prepare_test_data(); + + $this->assertFalse($data['coursebadge']->is_active()); + $this->assertFalse($data['coursebadge2']->is_active()); + + $result = enable_badges::execute([ + $data['coursebadge']->id, + $data['coursebadge2']->id, + ]); + $result = \core_external\external_api::clean_returnvalue(enable_badges::execute_returns(), $result); + // Badge can be enabled because badges with the same name are now allowed. + $this->assertEquals($data['coursebadge']->id, $result['result'][0]['badgeid']); + $this->assertEquals($data['coursebadge2']->id, $result['result'][1]['badgeid']); + $this->assertEmpty($result['warnings']); + + $coursebadge = new badge($data['coursebadge']->id); + $coursebadge2 = new badge($data['coursebadge2']->id); + $this->assertTrue($coursebadge->is_active()); + $this->assertTrue($coursebadge2->is_active()); + } + + /** + * Prepare the test, creating a few users and badges. + * + * @return array Test data. + */ + private function prepare_test_data(): array { + global $DB; + + // Setup test data. + $course = $this->getDataGenerator()->create_course(); + + // Create users and enrolments. + $student1 = $this->getDataGenerator()->create_and_enrol($course); + $student2 = $this->getDataGenerator()->create_and_enrol($course); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); + + /** @var core_badges_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_badges'); + $sitebadge = $generator->create_badge([ + 'name' => 'Site badge', + 'description' => 'Site badge', + 'status' => BADGE_STATUS_INACTIVE, + ]); + $coursebadge = $generator->create_badge([ + 'name' => 'Course badge', + 'description' => 'Course badge', + 'type' => BADGE_TYPE_COURSE, + 'courseid' => $course->id, + 'status' => BADGE_STATUS_INACTIVE, + ]); + $coursebadge2 = $generator->create_badge([ + 'name' => 'Course badge', + 'description' => 'Course badge', + 'type' => BADGE_TYPE_COURSE, + 'courseid' => $course->id, + 'status' => BADGE_STATUS_INACTIVE, + ]); + $nocriteriabadge = $generator->create_badge([ + 'name' => 'No criteria badge', + 'description' => 'No criteria badge', + 'type' => BADGE_TYPE_COURSE, + 'courseid' => $course->id, + 'status' => BADGE_STATUS_INACTIVE, + ]); + + // Create criteria for manually awarding by role. + $managerrole = $DB->get_field('role', 'id', ['shortname' => 'manager']); + $generator->create_criteria(['badgeid' => $sitebadge->id, 'roleid' => $managerrole]); + $generator->create_criteria(['badgeid' => $coursebadge->id, 'roleid' => $managerrole]); + $generator->create_criteria(['badgeid' => $coursebadge2->id, 'roleid' => $managerrole]); + + // Issue badges to student1. + $sitebadge->issue($student1->id, true); + $coursebadge->issue($student1->id, true); + + return [ + 'course' => $course, + 'student1' => $student1, + 'student2' => $student2, + 'teacher' => $teacher, + 'sitebadge' => $sitebadge, + 'coursebadge' => $coursebadge, + 'coursebadge2' => $coursebadge2, + 'nocriteriabadge' => $nocriteriabadge, + ]; + } +} diff --git a/blocks/amd/build/events.min.js b/blocks/amd/build/events.min.js index 0a95a08cf4c2f..5b69ff2584717 100644 --- a/blocks/amd/build/events.min.js +++ b/blocks/amd/build/events.min.js @@ -15,6 +15,6 @@ define("core_block/events",["exports","core/event_dispatcher"],(function(_export * window.console.log(e.detail.instanceId); // The instanceId of the block that was updated. * }); */ -const eventTypes={blockContentUpdated:"core_block/contentUpdated"};_exports.eventTypes=eventTypes;_exports.notifyBlockContentUpdated=element=>(0,_event_dispatcher.dispatchEvent)(eventTypes.blockContentUpdated,{instanceId:element.dataset.instanceId},element);let legacyEventsRegistered=!1;legacyEventsRegistered||(Y.use("event","moodle-core-event",(Y=>{document.addEventListener(eventTypes.blockContentUpdated,(e=>{Y.Global.fire(M.core.event.BLOCK_CONTENT_UPDATED,{instanceid:e.detail.instanceId})}))})),legacyEventsRegistered=!0)})); +const eventTypes={blockContentUpdated:"core_block/contentUpdated"};_exports.eventTypes=eventTypes;_exports.notifyBlockContentUpdated=element=>(0,_event_dispatcher.dispatchEvent)(eventTypes.blockContentUpdated,{instanceId:element.dataset.instanceId},element)})); //# sourceMappingURL=events.min.js.map \ No newline at end of file diff --git a/blocks/amd/build/events.min.js.map b/blocks/amd/build/events.min.js.map index aa41e06a66b71..6c43101b8549f 100644 --- a/blocks/amd/build/events.min.js.map +++ b/blocks/amd/build/events.min.js.map @@ -1 +1 @@ -{"version":3,"file":"events.min.js","sources":["../src/events.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/ //\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 * Javascript events for the `core_block` subsystem.\n *\n * @module core_block/events\n * @copyright 2021 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 4.0\n *\n * @example Example of listening to a block event.\n * import {eventTypes as blockEventTypes} from 'core_block/events';\n *\n * document.addEventListener(blockEventTypes.blockContentUpdated, e => {\n * window.console.log(e.target); // The HTMLElement relating to the block whose content was updated.\n * window.console.log(e.detail.instanceId); // The instanceId of the block that was updated.\n * });\n */\n\nimport {dispatchEvent} from 'core/event_dispatcher';\n\n/**\n * Events for `core_block`.\n *\n * @constant\n * @property {String} blockContentUpdated See {@link event:blockContentUpdated}\n */\nexport const eventTypes = {\n /**\n * An event triggered when the content of a block has changed.\n *\n * @event blockContentUpdated\n * @type {CustomEvent}\n * @property {HTMLElement} target The block element that was updated\n * @property {object} detail\n * @property {number} detail.instanceId The block instance id\n */\n blockContentUpdated: 'core_block/contentUpdated',\n};\n\n/**\n * Trigger an event to indicate that the content of a block was updated.\n *\n * @method notifyBlockContentUpdated\n * @param {HTMLElement} element The HTMLElement containing the updated block.\n * @returns {CustomEvent}\n * @fires blockContentUpdated\n */\nexport const notifyBlockContentUpdated = element => dispatchEvent(\n eventTypes.blockContentUpdated,\n {\n instanceId: element.dataset.instanceId,\n },\n element\n);\n\nlet legacyEventsRegistered = false;\nif (!legacyEventsRegistered) {\n // The following event triggers are legacy and will be removed in the future.\n // The following approach provides a backwards-compatability layer for the new events.\n // Code should be updated to make use of native events.\n\n Y.use('event', 'moodle-core-event', Y => {\n // Provide a backwards-compatability layer for YUI Events.\n document.addEventListener(eventTypes.blockContentUpdated, e => {\n // Trigger the legacy YUI event.\n Y.Global.fire(M.core.event.BLOCK_CONTENT_UPDATED, {instanceid: e.detail.instanceId});\n });\n });\n\n legacyEventsRegistered = true;\n}\n"],"names":["eventTypes","blockContentUpdated","element","instanceId","dataset","legacyEventsRegistered","Y","use","document","addEventListener","e","Global","fire","M","core","event","BLOCK_CONTENT_UPDATED","instanceid","detail"],"mappings":";;;;;;;;;;;;;;;;;MAuCaA,WAAa,CAUtBC,oBAAqB,+FAWgBC,UAAW,mCAChDF,WAAWC,oBACX,CACIE,WAAYD,QAAQE,QAAQD,YAEhCD,aAGAG,wBAAyB,EACxBA,yBAKDC,EAAEC,IAAI,QAAS,qBAAqBD,IAEhCE,SAASC,iBAAiBT,WAAWC,qBAAqBS,IAEtDJ,EAAEK,OAAOC,KAAKC,EAAEC,KAAKC,MAAMC,sBAAuB,CAACC,WAAYP,EAAEQ,OAAOf,mBAIhFE,wBAAyB"} \ No newline at end of file +{"version":3,"file":"events.min.js","sources":["../src/events.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/ //\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 * Javascript events for the `core_block` subsystem.\n *\n * @module core_block/events\n * @copyright 2021 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 4.0\n *\n * @example Example of listening to a block event.\n * import {eventTypes as blockEventTypes} from 'core_block/events';\n *\n * document.addEventListener(blockEventTypes.blockContentUpdated, e => {\n * window.console.log(e.target); // The HTMLElement relating to the block whose content was updated.\n * window.console.log(e.detail.instanceId); // The instanceId of the block that was updated.\n * });\n */\n\nimport {dispatchEvent} from 'core/event_dispatcher';\n\n/**\n * Events for `core_block`.\n *\n * @constant\n * @property {String} blockContentUpdated See {@link event:blockContentUpdated}\n */\nexport const eventTypes = {\n /**\n * An event triggered when the content of a block has changed.\n *\n * @event blockContentUpdated\n * @type {CustomEvent}\n * @property {HTMLElement} target The block element that was updated\n * @property {object} detail\n * @property {number} detail.instanceId The block instance id\n */\n blockContentUpdated: 'core_block/contentUpdated',\n};\n\n/**\n * Trigger an event to indicate that the content of a block was updated.\n *\n * @method notifyBlockContentUpdated\n * @param {HTMLElement} element The HTMLElement containing the updated block.\n * @returns {CustomEvent}\n * @fires blockContentUpdated\n */\nexport const notifyBlockContentUpdated = element => dispatchEvent(\n eventTypes.blockContentUpdated,\n {\n instanceId: element.dataset.instanceId,\n },\n element\n);\n"],"names":["eventTypes","blockContentUpdated","element","instanceId","dataset"],"mappings":";;;;;;;;;;;;;;;;;MAuCaA,WAAa,CAUtBC,oBAAqB,+FAWgBC,UAAW,mCAChDF,WAAWC,oBACX,CACIE,WAAYD,QAAQE,QAAQD,YAEhCD"} \ No newline at end of file diff --git a/blocks/amd/src/events.js b/blocks/amd/src/events.js index 81e00fd3e8868..bcc3b4b468dc5 100644 --- a/blocks/amd/src/events.js +++ b/blocks/amd/src/events.js @@ -65,20 +65,3 @@ export const notifyBlockContentUpdated = element => dispatchEvent( }, element ); - -let legacyEventsRegistered = false; -if (!legacyEventsRegistered) { - // The following event triggers are legacy and will be removed in the future. - // The following approach provides a backwards-compatability layer for the new events. - // Code should be updated to make use of native events. - - Y.use('event', 'moodle-core-event', Y => { - // Provide a backwards-compatability layer for YUI Events. - document.addEventListener(eventTypes.blockContentUpdated, e => { - // Trigger the legacy YUI event. - Y.Global.fire(M.core.event.BLOCK_CONTENT_UPDATED, {instanceid: e.detail.instanceId}); - }); - }); - - legacyEventsRegistered = true; -} diff --git a/blocks/badges/tests/behat/block_badges_course.feature b/blocks/badges/tests/behat/block_badges_course.feature index 5cdacf20f678b..59869476631e3 100644 --- a/blocks/badges/tests/behat/block_badges_course.feature +++ b/blocks/badges/tests/behat/block_badges_course.feature @@ -14,37 +14,25 @@ Feature: Enable Block Badges in a course And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | - And I log in as "teacher1" - And I am on "Course 1" course homepage - # Issue badge 1 of 2 - And I navigate to "Badges > Add a new badge" in current page administration - And I set the following fields to these values: - | id_name | Badge 1 | - | id_description | Badge 1 | - And I upload "blocks/badges/tests/fixtures/badge.png" file to "Image" filemanager - And I press "Create badge" - And I select "Manual issue by role" from the "Add badge criteria" singleselect - And I set the field "Teacher" to "1" - And I press "Save" - And I press "Enable access" - And I press "Continue" + And the following "core_badges > Badges" exist: + | name | course | description | image | status | type | + | Badge 1 | C1 | Badge 1 | badges/tests/behat/badge.png | active | 2 | + | Badge 2 | C1 | Badge 2 | badges/tests/behat/badge.png | active | 2 | + And the following "core_badges > Criterias" exist: + | badge | role | + | Badge 1 | editingteacher | + | Badge 2 | editingteacher | + And I am on the "Course 1" "course" page logged in as "teacher1" + And I navigate to "Badges > Manage badges" in current page administration + And I follow "Badge 1" And I select "Recipients (0)" from the "jump" singleselect And I press "Award badge" And I set the field "potentialrecipients[]" to "Teacher 1 (teacher1@example.com)" And I press "Award badge" # Issue Badge 2 of 2 And I am on "Course 1" course homepage - And I navigate to "Badges > Add a new badge" in current page administration - And I set the following fields to these values: - | id_name | Badge 2 | - | id_description | Badge 2 | - And I upload "blocks/badges/tests/fixtures/badge.png" file to "Image" filemanager - And I press "Create badge" - And I select "Manual issue by role" from the "Add badge criteria" singleselect - And I set the field "Teacher" to "1" - And I press "Save" - And I press "Enable access" - And I press "Continue" + And I navigate to "Badges > Manage badges" in current page administration + And I follow "Badge 2" And I select "Recipients (0)" from the "jump" singleselect And I press "Award badge" And I set the field "potentialrecipients[]" to "Teacher 1 (teacher1@example.com)" diff --git a/blocks/badges/tests/behat/block_badges_dashboard.feature b/blocks/badges/tests/behat/block_badges_dashboard.feature index 3c7b6c34f6116..c7251dc7bc8fd 100644 --- a/blocks/badges/tests/behat/block_badges_dashboard.feature +++ b/blocks/badges/tests/behat/block_badges_dashboard.feature @@ -17,20 +17,19 @@ Feature: Enable Block Badges on the dashboard and view awarded badges And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | badges | System | 1 | my-index | side-post | - And I log in as "teacher1" - And I am on "Course 1" course homepage - # Issue badge 1 of 2 - And I navigate to "Badges > Add a new badge" in current page administration - And I set the following fields to these values: - | id_name | Badge 1 | - | id_description | Badge 1 | - And I upload "blocks/badges/tests/fixtures/badge.png" file to "Image" filemanager - And I press "Create badge" - And I select "Manual issue by role" from the "Add badge criteria" singleselect - And I set the field "Teacher" to "1" - And I press "Save" - And I press "Enable access" - And I press "Continue" + And the following "core_badges > Badge" exists: + | name | Badge 1 | + | course | C1 | + | description | Badge 1 | + | image | badges/tests/behat/badge.png | + | status | active | + | type | 2 | + And the following "core_badges > Criteria" exists: + | badge | Badge 1 | + | role | editingteacher | + And I am on the "Course 1" "course" page logged in as "teacher1" + And I navigate to "Badges > Manage badges" in current page administration + And I follow "Badge 1" And I select "Recipients (0)" from the "jump" singleselect And I press "Award badge" And I set the field "potentialrecipients[]" to "Teacher 1 (teacher1@example.com)" diff --git a/blocks/badges/tests/behat/block_badges_frontpage.feature b/blocks/badges/tests/behat/block_badges_frontpage.feature index f57104148875f..e2e113dabc09a 100644 --- a/blocks/badges/tests/behat/block_badges_frontpage.feature +++ b/blocks/badges/tests/behat/block_badges_frontpage.feature @@ -17,19 +17,19 @@ Feature: Enable Block Badges on the frontpage and view awarded badges And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | badges | System | 1 | site-index | side-pre | - And I am on the "Course 1" course page logged in as teacher1 - # Issue badge 1 of 2 - And I navigate to "Badges > Add a new badge" in current page administration - And I set the following fields to these values: - | id_name | Badge 1 | - | id_description | Badge 1 | - And I upload "blocks/badges/tests/fixtures/badge.png" file to "Image" filemanager - And I press "Create badge" - And I select "Manual issue by role" from the "Add badge criteria" singleselect - And I set the field "Teacher" to "1" - And I press "Save" - And I press "Enable access" - And I press "Continue" + And the following "core_badges > Badge" exists: + | name | Badge 1 | + | course | C1 | + | description | Badge 1 | + | image | badges/tests/behat/badge.png | + | status | active | + | type | 2 | + And the following "core_badges > Criteria" exists: + | badge | Badge 1 | + | role | editingteacher | + And I am on the "Course 1" "course" page logged in as "teacher1" + And I navigate to "Badges > Manage badges" in current page administration + And I follow "Badge 1" And I select "Recipients (0)" from the "jump" singleselect And I press "Award badge" And I set the field "potentialrecipients[]" to "Teacher 1 (teacher1@example.com)" diff --git a/blocks/calendar_month/tests/behat/block_calendar_month.feature b/blocks/calendar_month/tests/behat/block_calendar_month.feature index 691f5b95afd8c..6239d9da25c22 100644 --- a/blocks/calendar_month/tests/behat/block_calendar_month.feature +++ b/blocks/calendar_month/tests/behat/block_calendar_month.feature @@ -106,6 +106,6 @@ Feature: Enable the calendar block in a course and test it's functionality And I press "Save changes" When I am on "Course 1" course homepage And I follow "Course calendar" - And I click on today in the mini-calendar block + And I click on today in the mini-calendar block to view the detail Then I should see "Site Event" in the "Calendar" "block" And ".popover" "css_element" should not exist diff --git a/blocks/rss_client/block_rss_client.php b/blocks/rss_client/block_rss_client.php index 3f3e292f17526..b745aef7a6737 100644 --- a/blocks/rss_client/block_rss_client.php +++ b/blocks/rss_client/block_rss_client.php @@ -61,19 +61,6 @@ function specialization() { protected function get_footer($feedrecords) { $footer = null; - if ($this->config->block_rss_client_show_channel_link) { - global $CFG; - require_once($CFG->libdir.'/simplepie/moodle_simplepie.php'); - - $feedrecord = array_pop($feedrecords); - $feed = new moodle_simplepie($feedrecord->url); - $channellink = new moodle_url($feed->get_link()); - - if (!empty($channellink)) { - $footer = new block_rss_client\output\footer($channellink); - } - } - if ($this->hasfailedfeeds) { if (has_any_capability(['block/rss_client:manageownfeeds', 'block/rss_client:manageanyfeeds'], $this->context)) { if ($footer === null) { @@ -104,6 +91,15 @@ function get_content() { return $this->content; } + $managefeedfooterlink = ''; + if (has_any_capability(['block/rss_client:manageanyfeeds', 'block/rss_client:manageownfeeds'], $this->context)) { + $managefeedfooterlink = html_writer::link( + new moodle_url('/blocks/rss_client/managefeeds.php', ['courseid' => $this->page->course->id]), + get_string('managefeeds', 'block_rss_client'), + ['class' => 'btn btn-primary', 'role' => 'button'], + ); + } + if (!isset($this->config)) { // The block has yet to be configured - just display configure message in // the block if user has permission to configure it @@ -112,6 +108,8 @@ function get_content() { $this->content->text = get_string('feedsconfigurenewinstance2', 'block_rss_client'); } + $this->content->footer = $managefeedfooterlink; + return $this->content; } @@ -156,6 +154,8 @@ function get_content() { $this->content->footer = $renderer->render_footer($footer); } + $this->content->footer .= $managefeedfooterlink; + return $this->content; } @@ -259,6 +259,12 @@ public function get_feed($feedrecord, $maxentries, $showtitle) { } } + // Feed channel link. + if ($this->config->block_rss_client_show_channel_link) { + $channellink = $simplepiefeed->get_link(); + $feed->set_channellink($channellink ? new moodle_url($channellink) : null); + } + return $feed; } diff --git a/blocks/rss_client/classes/output/feed.php b/blocks/rss_client/classes/output/feed.php index 28ea2cf35940e..61b9ae8f995b8 100644 --- a/blocks/rss_client/classes/output/feed.php +++ b/blocks/rss_client/classes/output/feed.php @@ -44,6 +44,13 @@ class feed implements \renderable, \templatable { */ protected $title = null; + /** + * The feed's channel link. + * + * @var string|null + */ + protected ?string $channellink; + /** * An array of renderable feed items * @@ -78,11 +85,13 @@ class feed implements \renderable, \templatable { * @param string $title The title of the RSS feed * @param boolean $showtitle Whether to show the title * @param boolean $showimage Whether to show the channel image + * @param string|null $channellink The channel link of the RSS feed */ - public function __construct($title, $showtitle = true, $showimage = true) { + public function __construct($title, $showtitle = true, $showimage = true, ?string $channellink = null) { $this->title = $title; $this->showtitle = $showtitle; $this->showimage = $showimage; + $this->channellink = $channellink; } /** @@ -97,6 +106,7 @@ public function export_for_template(\renderer_base $output) { 'title' => $this->showtitle ? $this->title : null, 'image' => null, 'items' => array(), + 'channellink' => $this->channellink ?? null, ); if ($this->showimage && $this->image) { @@ -131,6 +141,15 @@ public function get_title() { return $this->title; } + /** + * Set the feed channel link. + * + * @param \moodle_url|null $channellink the URL to the channel website. + */ + public function set_channellink(?\moodle_url $channellink) { + $this->channellink = $channellink; + } + /** * Add an RSS item * diff --git a/blocks/rss_client/classes/output/footer.php b/blocks/rss_client/classes/output/footer.php index c864df3d68693..7ff348ad28919 100644 --- a/blocks/rss_client/classes/output/footer.php +++ b/blocks/rss_client/classes/output/footer.php @@ -100,7 +100,6 @@ public function get_channelurl() { */ public function export_for_template(\renderer_base $output) { $data = new \stdClass(); - $data->channellink = clean_param($this->channelurl, PARAM_URL); if ($this->manageurl) { $data->hasfailedfeeds = true; $data->manageurl = clean_param($this->manageurl, PARAM_URL); diff --git a/blocks/rss_client/edit_form.php b/blocks/rss_client/edit_form.php index 6f481abf83de2..dc6e9ef5b2c23 100644 --- a/blocks/rss_client/edit_form.php +++ b/blocks/rss_client/edit_form.php @@ -22,6 +22,10 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir .'/simplepie/moodle_simplepie.php'); + /** * Form for editing RSS client block instances. * @@ -29,24 +33,36 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class block_rss_client_edit_form extends block_edit_form { + + /** @var stdClass|null The new RSS feed URL object. */ + private ?stdClass $newrss = null; + protected function specific_definition($mform) { global $CFG, $DB, $USER; // Fields for editing block contents. $mform->addElement('header', 'configheader', get_string('blocksettings', 'block')); - $mform->addElement('selectyesno', 'config_display_description', get_string('displaydescriptionlabel', 'block_rss_client')); - $mform->setDefault('config_display_description', 0); + $radiogroup = [ + $mform->createElement('radio', 'config_method', + get_string('configmethodexisting', 'block_rss_client'), null, 'existing'), + $mform->createElement('radio', 'config_method', + get_string('configmethodnew', 'block_rss_client'), null, 'new'), + ]; + $mform->addGroup( + elements: $radiogroup, + name: 'config_method_group', + separator: ['   '], + appendName: false, + ); + $mform->setDefault('config_method', 'existing'); - $mform->addElement('text', 'config_shownumentries', get_string('shownumentrieslabel', 'block_rss_client'), array('size' => 5)); - $mform->setType('config_shownumentries', PARAM_INT); - $mform->addRule('config_shownumentries', null, 'numeric', null, 'client'); - if (!empty($CFG->block_rss_client_num_entries)) { - $mform->setDefault('config_shownumentries', $CFG->block_rss_client_num_entries); - } else { - $mform->setDefault('config_shownumentries', 5); - } + // Add new RSS feed. + $mform->addElement('text', 'config_feedurl', get_string('feedurl', 'block_rss_client')); + $mform->setType('config_feedurl', PARAM_URL); + $mform->hideIf('config_feedurl', 'config_method', 'ne', 'new'); + // Select existing RSS feed. $insql = ''; $params = array('userid' => $USER->id); if (!empty($this->block->config) && !empty($this->block->config->rssid)) { @@ -68,21 +84,33 @@ protected function specific_definition($mform) { if ($rssfeeds) { $select = $mform->addElement('select', 'config_rssid', get_string('choosefeedlabel', 'block_rss_client'), $rssfeeds); $select->setMultiple(true); - + $mform->hideIf('config_rssid', 'config_method', 'ne', 'existing'); } else { $mform->addElement('static', 'config_rssid_no_feeds', get_string('choosefeedlabel', 'block_rss_client'), get_string('nofeeds', 'block_rss_client')); + $mform->hideIf('config_rssid_no_feeds', 'config_method', 'ne', 'existing'); } - if (has_any_capability(array('block/rss_client:manageanyfeeds', 'block/rss_client:manageownfeeds'), $this->block->context)) { - $mform->addElement('static', 'nofeedmessage', '', - '
    ' . - get_string('feedsaddedit', 'block_rss_client') . ''); - } + // Subheading: Display settings for RSS feed. + $startsubheading = '

    '; + $endsubheading = '

    '; + $mform->addElement('html', $startsubheading . get_string('displaysettings', 'block_rss_client') . $endsubheading); $mform->addElement('text', 'config_title', get_string('uploadlabel')); $mform->setType('config_title', PARAM_NOTAGS); + $mform->addElement('selectyesno', 'config_display_description', get_string('displaydescriptionlabel', 'block_rss_client')); + $mform->setDefault('config_display_description', 0); + + $mform->addElement('text', 'config_shownumentries', get_string('shownumentrieslabel', 'block_rss_client'), ['size' => 5]); + $mform->setType('config_shownumentries', PARAM_INT); + $mform->addRule('config_shownumentries', null, 'numeric', null, 'client'); + if (!empty($CFG->block_rss_client_num_entries)) { + $mform->setDefault('config_shownumentries', $CFG->block_rss_client_num_entries); + } else { + $mform->setDefault('config_shownumentries', 5); + } + $mform->addElement('selectyesno', 'config_block_rss_client_show_channel_link', get_string('clientshowchannellinklabel', 'block_rss_client')); $mform->setDefault('config_block_rss_client_show_channel_link', 0); @@ -90,6 +118,91 @@ protected function specific_definition($mform) { $mform->setDefault('config_block_rss_client_show_channel_image', 0); } + /** + * Overriding the get_data function to insert a new RSS ID. + */ + public function get_data(): ?stdClass { + $data = parent::get_data(); + // Force the 'existing` method as a default. + $data->config_method = 'existing'; + // Sanitize the title to prevent XSS (Cross-Site Scripting) attacks by encoding special characters into HTML entities. + $data->config_title = htmlspecialchars($data->config_title, ENT_QUOTES, 'utf-8'); + // If the new RSS is not empty then add the ID to the config_rssid. + if ($data && $this->newrss) { + $data->config_rssid[] = $this->newrss->id; + } + return $data; + } + + /** + * Overriding the definition_after_data to empty the input. + */ + public function definition_after_data(): void { + parent::definition_after_data(); + $mform =& $this->_form; + // If form is not submitted then empty the feed URL. + if (!$this->is_submitted()) { + $mform->getElement('config_feedurl')->setValue(''); + } + } + + /** + * Overriding the validation to validate the RSS URL and store it to the database. + * + * If there are no errors, insert the new feed to the database and store the object in + * the private property so it can be saved to the RSS block config. + * + * @param array $data Data from the form. + * @param array $files Files form the form. + * @return array of errors from validation. + */ + public function validation($data, $files): array { + global $USER, $DB; + $errors = parent::validation($data, $files); + + if ($data['config_method'] === "new") { + // If the "New" method is selected and the feed URL is not empty, then proceed. + if ($data['config_feedurl']) { + if (!filter_var($data['config_feedurl'], FILTER_VALIDATE_URL)) { + $errors['config_feedurl'] = get_string('couldnotfindloadrssfeed', 'block_rss_client'); + return $errors; + } + try { + $rss = new moodle_simplepie(); + // Set timeout for longer than normal to try and grab the feed. + $rss->set_timeout(10); + $rss->set_feed_url($data['config_feedurl']); + $rss->set_autodiscovery_cache_duration(0); + $rss->set_autodiscovery_level(moodle_simplepie::LOCATOR_ALL); + $rss->init(); + if ($rss->error()) { + $errors['config_feedurl'] = get_string('couldnotfindloadrssfeed', 'block_rss_client'); + } else { + // Return URL without quoting. + $discoveredurl = new moodle_url($rss->subscribe_url()); + $theurl = $discoveredurl->out(false); + // Save the RSS to the database. + $this->newrss = new stdClass; + $this->newrss->userid = $USER->id; + $this->newrss->title = $rss->get_title(); + $this->newrss->description = $rss->get_description(); + $this->newrss->url = $theurl; + $newrssid = $DB->insert_record('block_rss_client', $this->newrss); + $this->newrss->id = $newrssid; + } + } catch (Exception $e) { + $errors['config_feedurl'] = get_string('couldnotfindloadrssfeed', 'block_rss_client'); + } + } else { + // If the "New" method is selected, but the feed URL is empty, then raise error. + $errors['config_feedurl'] = get_string('err_required', 'form'); + } + + } + + return $errors; + } + /** * Display the configuration form when block is being added to the page * diff --git a/blocks/rss_client/lang/en/block_rss_client.php b/blocks/rss_client/lang/en/block_rss_client.php index 06facda0af264..310ca7d5cce4d 100644 --- a/blocks/rss_client/lang/en/block_rss_client.php +++ b/blocks/rss_client/lang/en/block_rss_client.php @@ -27,17 +27,21 @@ $string['addnew'] = 'Add new'; $string['addnewfeed'] = 'Add a new feed'; $string['cannotmakemodification'] = 'You are not allowed to make modifications to this RSS feed at this time.'; +$string['choosefeedlabel'] = 'Select the feeds to display in this block'; $string['clientchannellink'] = 'Source site...'; $string['clientnumentries'] = 'The default number of entries to show per feed.'; -$string['clientshowchannellinklabel'] = 'Should a link to the original site (channel link) be displayed? (Note that if no feed link is supplied in the news feed then no link will be shown) :'; +$string['clientshowchannellinklabel'] = 'Show source link if available'; $string['clientshowimagelabel'] = 'Show channel image if available :'; $string['configblock'] = 'Configure this block'; +$string['configmethodexisting'] = 'Select existing RSS feed'; +$string['configmethodnew'] = 'Add new RSS feed'; $string['couldnotfindfeed'] = 'Could not find feed with id'; $string['couldnotfindloadrssfeed'] = 'Could not find or load the RSS feed.'; $string['customtitlelabel'] = 'Custom title (leave blank to use title supplied by feed):'; $string['deletefeedconfirm'] = 'Are you sure you want to delete this feed?'; $string['disabledrssfeeds'] = 'RSS feeds are disabled'; -$string['displaydescriptionlabel'] = 'Display each link\'s description?'; +$string['displaydescriptionlabel'] = 'Show descriptions for entries'; +$string['displaysettings'] = 'Display settings for RSS feed'; $string['editafeed'] = 'Edit a feed'; $string['editfeeds'] = 'Edit, subscribe or unsubscribe from RSS/Atom news feeds'; $string['editnewsfeeds'] = 'Edit news feeds'; @@ -54,11 +58,10 @@ $string['feedsconfigurenewinstance'] = 'Click here to configure this block to display RSS feeds.'; $string['feedsconfigurenewinstance2'] = 'Click the edit icon above to configure this block to display RSS feeds.'; $string['feedupdated'] = 'News feed updated'; -$string['feedurl'] = 'Feed URL'; +$string['feedurl'] = 'RSS link'; $string['findmorefeeds'] = 'Find more RSS feeds'; -$string['choosefeedlabel'] = 'Choose the feeds which you would like to make available in this block:'; $string['managefeeds'] = 'Manage all my feeds'; -$string['nofeeds'] = 'There are no RSS feeds defined for this site.'; +$string['nofeeds'] = 'There are no existing RSS feeds configured for this site. You can add one choosing \'Add new RSS feed\'.'; $string['numentries'] = 'Entries per feed'; $string['pickfeed'] = 'Pick a news feed'; $string['pluginname'] = 'Remote RSS feeds'; @@ -81,7 +84,7 @@ $string['rss_client:myaddinstance'] = 'Add a new Remote RSS feeds block to Dashboard'; $string['seeallfeeds'] = 'See all feeds'; $string['sharedfeed'] = 'Shared feed'; -$string['shownumentrieslabel'] = 'Max number entries to show per block.'; +$string['shownumentrieslabel'] = 'Entries to display'; $string['submitters'] = 'Who will be allowed to define new RSS feeds? Defined feeds are available for any page on your site.'; $string['submitters2'] = 'Submitters'; $string['timeout'] = 'Time in minutes before an RSS feed expires in cache. Note that this time defines the minimum time before expiry; the feed will be refreshed in cache on the next cron execution after expiry. Recommended values are 30 mins or greater.'; diff --git a/blocks/rss_client/templates/block.mustache b/blocks/rss_client/templates/block.mustache index 6cc2c71ea0770..8dca0217c8ca0 100644 --- a/blocks/rss_client/templates/block.mustache +++ b/blocks/rss_client/templates/block.mustache @@ -55,7 +55,8 @@ "permalink": "https://www.example.com/my-cat-story.html", "datepublished": "12 January 2016, 9:12 pm" } - ] + ], + "channellink": "https://www.example.com" }, { "title": "News from around my kitchen", @@ -81,7 +82,8 @@ "permalink": "https://www.example.com/oven-smoke.html", "datepublished": "13 January 2016, 8:25 pm" } - ] + ], + "channellink": "https://www.example.com" } ] } diff --git a/blocks/rss_client/templates/channel_image.mustache b/blocks/rss_client/templates/channel_image.mustache index f20166e53d7af..230f8d31f5dcb 100644 --- a/blocks/rss_client/templates/channel_image.mustache +++ b/blocks/rss_client/templates/channel_image.mustache @@ -42,7 +42,7 @@ {{/link}} - {{title}} + {{title}} {{#link}} diff --git a/blocks/rss_client/templates/feed.mustache b/blocks/rss_client/templates/feed.mustache index a69f3e828888b..7600bb9fa11a3 100644 --- a/blocks/rss_client/templates/feed.mustache +++ b/blocks/rss_client/templates/feed.mustache @@ -55,7 +55,8 @@ "permalink": "https://www.example.com/my-cat-story.html", "datepublished": "12 January 2016, 9:12 pm" } - ] + ], + "channellink": "https://www.example.com" } }} {{$image}} @@ -77,3 +78,9 @@ {{/items}} {{/items}} + +{{#channellink}} + +{{/channellink}} diff --git a/blocks/rss_client/templates/footer.mustache b/blocks/rss_client/templates/footer.mustache index dd5d0fe2b39d7..d3fb1c471fad5 100644 --- a/blocks/rss_client/templates/footer.mustache +++ b/blocks/rss_client/templates/footer.mustache @@ -30,13 +30,10 @@ Example context (json): { - "channellink": "https://www.example.com/feeds/rss" + "hasfailedfeeds": true, + "manageurl": "http://moodle.web/blocks/rss_client/managefeeds.php?courseid=1" } }} -{{#channellink}} - {{#str}} clientchannellink, block_rss_client {{/str}} - {{#hasfailedfeeds}}
    {{/hasfailedfeeds}} -{{/channellink}} {{#hasfailedfeeds}} - {{#str}} failedfeeds, block_rss_client {{/str}} -{{/hasfailedfeeds}} \ No newline at end of file + {{#str}} failedfeeds, block_rss_client {{/str}} +{{/hasfailedfeeds}} diff --git a/blocks/rss_client/tests/behat/block_rss_client_frontpage.feature b/blocks/rss_client/tests/behat/block_rss_client_frontpage.feature new file mode 100644 index 0000000000000..36eddaf2970ac --- /dev/null +++ b/blocks/rss_client/tests/behat/block_rss_client_frontpage.feature @@ -0,0 +1,48 @@ +@block @block_rss_client +Feature: Enable RSS client block menu on the frontpage + In order to enable the RSS client block on the frontpage + As an admin + I can add RSS client block to the frontpage + + Background: + Given I log in as "admin" + When I navigate to "Plugins > Blocks > Manage blocks" in site administration + Then I enable "rss_client" "block" plugin + And the following "blocks" exist: + | blockname | contextlevel | reference | pagetypepattern | defaultregion | + | rss_client | System | 1 | site-index | side-pre | + + @javascript + Scenario: Configuring the RSS block on the frontpage + Given I log in as "admin" + And I am on site homepage + And I turn editing mode on + And "Remote news feed" "block" should exist + And I configure the "Remote news feed" block + And I should see "There are no existing RSS feeds configured for this site. You can add one choosing 'Add new RSS feed'." + + # Test filling in an empty URL in the input. + And I click on "Add new RSS feed" "radio" + And I press "Save changes" + And I should see "You must supply a value here." + + # Test filling in with a non-valid URL in the input. + And I set the field "config_feedurl" to "https://example.com/notvalid.rss" + And I press "Save changes" + And I should see "Could not find or load the RSS feed." + + # Test filling in with the correct URL in the input. + And I set the field "config_feedurl" to "https://www.nasa.gov/rss/dyn/breaking_news.rss" + And I set the field "config_block_rss_client_show_channel_link" to "Yes" + And I press "Save changes" + And I should see "NASA" + And I should see "Source site..." + + # Test the existence of the available feeds. + When I configure the "NASA" block + Then I should see "NASA" in the "Select the feeds to display in this block" "select" + And I click on "Cancel" "button" in the "Configure NASA block" "dialogue" + + # Test the Manage all my feeds page. + And I click on "Manage all my feeds" "link" + And I should see "NASA" diff --git a/cache/classes/helper.php b/cache/classes/helper.php index cd46243af8fe5..a88372e77e16c 100644 --- a/cache/classes/helper.php +++ b/cache/classes/helper.php @@ -759,18 +759,27 @@ public static function clean_old_session_data($output = false) { debugging('Cache stores used for session definitions should ideally be searchable.', DEBUG_DEVELOPER); continue; } - // Get all of the keys. - $keys = $store->find_by_prefix(cache_session::KEY_PREFIX); - $todelete = array(); + // Get all of the last access keys. + $keys = $store->find_by_prefix(cache_session::LASTACCESS); + $todelete = []; foreach ($store->get_many($keys) as $key => $value) { - if (strpos($key, cache_session::KEY_PREFIX) !== 0 || !is_array($value) || !isset($value['lastaccess'])) { - continue; + $expiresvalue = 0; + if ($value instanceof cache_ttl_wrapper) { + $expiresvalue = $value->data; + } else if ($value instanceof cache_cached_object) { + $expiresvalue = $value->restore_object(); + } else { + $expiresvalue = $value; } - if ((int)$value['lastaccess'] < $purgetime || true) { - $todelete[] = $key; + $expires = (int) $expiresvalue; + + if ($expires > 0 && $expires < $purgetime) { + $prefix = substr($key, strlen(cache_session::LASTACCESS)); + $foundbyprefix = $store->find_by_prefix($prefix); + $todelete = array_merge($todelete, [$key], $foundbyprefix); } } - if (count($todelete)) { + if ($todelete) { $outcome = (int)$store->delete_many($todelete); if ($output) { $strdef = s($definition->get_id()); diff --git a/cache/stores/redis/lib.php b/cache/stores/redis/lib.php index 5c469056d8523..7e3145324da7e 100644 --- a/cache/stores/redis/lib.php +++ b/cache/stores/redis/lib.php @@ -384,7 +384,7 @@ public function get($key) { * @return array An array of the values of the given keys. */ public function get_many($keys) { - $values = $this->redis->hMGet($this->hash, $keys); + $values = $this->redis->hMGet($this->hash, $keys) ?: []; if ($this->compressor == self::COMPRESSOR_NONE) { return $values; diff --git a/calendar/lib.php b/calendar/lib.php index 7c48489f432b8..8f49b89e8ac46 100644 --- a/calendar/lib.php +++ b/calendar/lib.php @@ -2177,29 +2177,27 @@ function calendar_set_filters(array $courseeventsfrom, $ignorefilters = false, s } if (!empty($courseeventsfrom) && (calendar_show_event_type(CALENDAR_EVENT_GROUP, $user) || $ignorefilters)) { - - if (count($courseeventsfrom) == 1) { - $course = reset($courseeventsfrom); - if (has_any_capability($allgroupscaps, \context_course::instance($course->id))) { - $coursegroups = groups_get_all_groups($course->id, 0, 0, 'g.id'); - $group = array_keys($coursegroups); - } - } - if ($group === false) { - if (!empty($CFG->calendar_adminseesall) && has_any_capability($allgroupscaps, \context_system::instance())) { - $group = true; - } else if ($isvaliduser) { - $groupids = array(); - foreach ($courseeventsfrom as $courseid => $course) { - if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) { - // If this course has groups, show events from all of those related to the current user. + if (!empty($CFG->calendar_adminseesall) && has_any_capability($allgroupscaps, \context_system::instance())) { + $group = true; + } else if ($isvaliduser) { + $groupids = []; + foreach ($courseeventsfrom as $courseid => $course) { + if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) { + if (has_all_capabilities($allgroupscaps, \context_course::instance($courseid))) { + // User can access all groups in this course. + // Get all the groups in this course. + $coursegroups = groups_get_all_groups($course->id, 0, 0, 'g.id'); + $groupids = array_merge($groupids, array_keys($coursegroups)); + } else { + // User can only access their own groups. + // Get the groups the user is in. $coursegroups = groups_get_user_groups($course->id, $user->id); $groupids = array_merge($groupids, $coursegroups['0']); } } - if (!empty($groupids)) { - $group = $groupids; - } + } + if (!empty($groupids)) { + $group = $groupids; } } } diff --git a/calendar/renderer.php b/calendar/renderer.php index 2c356347dc671..7766e1b30dec9 100644 --- a/calendar/renderer.php +++ b/calendar/renderer.php @@ -234,10 +234,12 @@ public function course_filter_selector(moodle_url $returnurl, $label = null, $co if (isset($contextrecords[$course->id])) { context_helper::preload_from_record($contextrecords[$course->id]); } - $coursecontext = context_course::instance($course->id); + // Limit the displayed course name to prevent the dropdown from getting too wide. - $coursename = shorten_text($course->fullname, 50, true); - $courseoptions[$course->id] = format_string($coursename, true, ['context' => $coursecontext]); + $coursename = format_string($course->fullname, true, [ + 'context' => \core\context\course::instance($course->id), + ]); + $courseoptions[$course->id] = shorten_text($coursename, 50, true); } if ($courseid) { diff --git a/calendar/tests/behat/behat_calendar.php b/calendar/tests/behat/behat_calendar.php index 75c81ea333acc..291398e742f8a 100644 --- a/calendar/tests/behat/behat_calendar.php +++ b/calendar/tests/behat/behat_calendar.php @@ -48,10 +48,19 @@ public static function get_partial_named_selectors(): array { new behat_component_named_selector('mini calendar block', [".//*[@data-block='calendar_month']"]), new behat_component_named_selector('full calendar page', [".//*[@id='page-calendar-view']"]), new behat_component_named_selector('calendar day', [".//*[@data-region='day'][@data-day=%locator%]"]), + new behat_component_named_selector( + 'calendar day detail', + [".//*[@data-region='day'][@data-day=%locator%]//a[@data-action='view-day-link']"] + ), new behat_component_named_selector( 'responsive calendar day', [".//*[@data-region='day'][@data-day=%locator%]/div[contains(@class, 'hidden-desktop')]"] ), + new behat_component_named_selector( + 'responsive calendar day detail', + [".//*[@data-region='day'][@data-day=%locator%]" . + "/div[contains(@class, 'hidden-desktop')]//a[@data-action='view-day-link']"] + ), ]; } @@ -139,13 +148,26 @@ public function i_hover_over_day_of_this_month_in_full_calendar_page(int $day, s * * @param int $day The day of the current month. * @param string $responsive If not null, find the responsive version of the link. + * @param string $detail If not null, find the detail version of the link. */ - public function i_click_on_day_of_this_month_in_mini_calendar_block(int $day, string $responsive = ''): void { + public function i_click_on_day_of_this_month_in_mini_calendar_block( + int $day, + string $responsive = '', + string $detail = '', + ): void { + $selectortype = 'core_calendar >'; + if (!empty($responsive)) { + $selectortype .= ' responsive'; + } + $selectortype .= ' calendar day'; + if (!empty($detail)) { + $selectortype .= ' detail'; + } $this->execute( contextapi: "behat_general::i_click_on_in_the", params: [ $day, - empty($responsive) ? 'core_calendar > calendar day' : 'core_calendar > responsive calendar day', + $selectortype, '', 'core_calendar > mini calendar block', ], @@ -167,14 +189,16 @@ public function i_hover_over_today_in_mini_calendar_block(string $responsive = ' /** * Click on today in the mini-calendar. * - * @Given /^I click on today in the mini-calendar block( responsive view|)$/ + * @Given /^I click on today in the mini-calendar block( responsive view|)( to view the detail|)$/ * * @param string $responsive If not empty, use the responsive calendar link. + * @param string $detail If not empty, use the detail view calendar link. */ - public function i_click_on_today_in_mini_calendar_block(string $responsive = ''): void { + public function i_click_on_today_in_mini_calendar_block(string $responsive = '', string $detail = ''): void { $this->i_click_on_day_of_this_month_in_mini_calendar_block( day: date('j'), responsive: $responsive, + detail: $detail, ); } diff --git a/calendar/tests/lib_test.php b/calendar/tests/lib_test.php index b9ddfe94b58e4..6d8b59a4ad218 100644 --- a/calendar/tests/lib_test.php +++ b/calendar/tests/lib_test.php @@ -898,6 +898,182 @@ public function test_calendar_set_filters_logged_in_another_user(): void { $this->assertEquals($users[1]->id, $userid); } + /** + * This function tests calendar_set_filters for courses with separate group mode. + */ + public function test_calendar_set_filters_with_separate_group_mode(): void { + global $DB; + $this->resetAfterTest(); + $generator = $this->getDataGenerator(); + + // Create users. + $student1 = $generator->create_user(); + $student2 = $generator->create_user(); + $teacher1 = $generator->create_user(); + $teacher2 = $generator->create_user(); + + // Create courses. + $course1 = $generator->create_course([ + 'shortname' => 'C1', + 'groupmode' => 1, + 'groupmodeforce' => 1, + ]); + $course2 = $generator->create_course([ + 'shortname' => 'C2', + 'groupmode' => 1, + 'groupmodeforce' => 1, + ]); + $course1context = \context_course::instance($course1->id); + $course2context = \context_course::instance($course2->id); + + // Create groups. + $group1 = $generator->create_group([ + 'name' => 'G1-C1', + 'courseid' => $course1->id, + ]); + $group2 = $generator->create_group([ + 'name' => 'G1-C2', + 'courseid' => $course2->id, + ]); + $group3 = $generator->create_group([ + 'name' => 'G2-C2', + 'courseid' => $course2->id, + ]); + + // Modify the capabilities. + $editingteacherroleid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher']); + assign_capability( + 'moodle/site:accessallgroups', + CAP_PREVENT, + $editingteacherroleid, + $course1context->id, + true + ); + assign_capability( + 'moodle/site:accessallgroups', + CAP_PREVENT, + $editingteacherroleid, + $course2context->id, + true + ); + + // Enrol users. + $generator->enrol_user($student1->id, $course1->id, 'student'); + $generator->enrol_user($teacher1->id, $course1->id, 'editingteacher'); + $generator->enrol_user($student1->id, $course2->id, 'student'); + $generator->enrol_user($student2->id, $course2->id, 'student'); + $generator->enrol_user($teacher1->id, $course2->id, 'editingteacher'); + $generator->enrol_user($teacher2->id, $course2->id, 'editingteacher'); + + // Group memberships. + $generator->create_group_member([ + 'groupid' => $group1->id, + 'userid' => $student1->id, + ]); + $generator->create_group_member([ + 'groupid' => $group1->id, + 'userid' => $teacher1->id, + ]); + $generator->create_group_member([ + 'groupid' => $group2->id, + 'userid' => $student1->id, + ]); + $generator->create_group_member([ + 'groupid' => $group2->id, + 'userid' => $teacher1->id, + ]); + $generator->create_group_member([ + 'groupid' => $group3->id, + 'userid' => $student2->id, + ]); + $generator->create_group_member([ + 'groupid' => $group3->id, + 'userid' => $teacher2->id, + ]); + + // Test teacher1. + $this->setUser($teacher1); + $defaultcourses = calendar_get_default_courses( + null, + '*', + false, + $teacher1->id + ); + [$courseids, $groupids] = calendar_set_filters( + $defaultcourses, + false, + $teacher1 + ); + // Teacher1 can see SITE, C1, G1-C1, C2, G1-C2. + $this->assertCount(3, $courseids); // SITE, C1, C2. + $this->assertCount(2, $groupids); // G1-C1, G1-C2. + + $courseidskey = array_fill_keys($courseids, null); + $this->assertArrayHasKey(SITEID, $courseidskey); + $this->assertArrayHasKey($course1->id, $courseidskey); + $this->assertArrayHasKey($course2->id, $courseidskey); + + $groupidskey = array_fill_keys($groupids, null); + $this->assertArrayHasKey($group1->id, $groupidskey); + $this->assertArrayHasKey($group2->id, $groupidskey); + $this->assertArrayNotHasKey($group3->id, $groupidskey); + + // Test teacher2. + $this->setUser($teacher2); + $defaultcourses = calendar_get_default_courses( + null, + '*', + false, + $teacher2->id + ); + [$courseids, $groupids] = calendar_set_filters( + $defaultcourses, + false, + $teacher2 + ); + // Teacher2 can see SITE, C2, G2-C2. + $this->assertCount(2, $courseids); // SITE, C2. + $this->assertCount(1, $groupids); // G2-C2. + + $courseidskey = array_fill_keys($courseids, null); + $this->assertArrayHasKey(SITEID, $courseidskey); + $this->assertArrayHasKey($course2->id, $courseidskey); + + $groupidskey = array_fill_keys($groupids, null); + $this->assertArrayHasKey($group3->id, $groupidskey); + $this->assertArrayNotHasKey($group1->id, $groupidskey); + $this->assertArrayNotHasKey($group2->id, $groupidskey); + + // Modify the capabilities. + assign_capability( + 'moodle/site:accessallgroups', + CAP_ALLOW, + $editingteacherroleid, + $course2context->id, + true + ); + + $defaultcourses = calendar_get_default_courses( + null, + '*', + false, + $teacher2->id + ); + [$courseids, $groupids] = calendar_set_filters( + $defaultcourses, + false, + $teacher2 + ); + // Teacher2 can see SITE, C2, G1-C2, G2-C2. + $this->assertCount(2, $courseids); // SITE, C2. + $this->assertCount(2, $groupids); // G1-C2, G2-C2. + + $groupidskey = array_fill_keys($groupids, null); + $this->assertArrayHasKey($group2->id, $groupidskey); + $this->assertArrayHasKey($group3->id, $groupidskey); + $this->assertArrayNotHasKey($group1->id, $groupidskey); + } + /** * Test for calendar_view_event_allowed for course event types. */ diff --git a/cohort/amd/build/actions.min.js b/cohort/amd/build/actions.min.js index c5d49ea397455..8fb2def35fc48 100644 --- a/cohort/amd/build/actions.min.js +++ b/cohort/amd/build/actions.min.js @@ -1,10 +1,10 @@ -define("core_cohort/actions",["exports","core/event_dispatcher","core/notification","core/pending","core/prefetch","core/str","core_cohort/repository","core_reportbuilder/local/events","core_reportbuilder/local/selectors","core/local/inplace_editable/events"],(function(_exports,_event_dispatcher,_notification,_pending,_prefetch,_str,_repository,reportEvents,reportSelectors,_events2){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +define("core_cohort/actions",["exports","core/event_dispatcher","core/notification","core/pending","core/prefetch","core/str","core/toast","core_cohort/repository","core_reportbuilder/local/events","core_reportbuilder/local/selectors","core/local/inplace_editable/events"],(function(_exports,_event_dispatcher,_notification,_pending,_prefetch,_str,_toast,_repository,reportEvents,reportSelectors,_events2){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Cohorts actions. * * @module core_cohort/actions * @copyright 2024 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.registerEventListeners=_exports.init=void 0,_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),reportEvents=_interopRequireWildcard(reportEvents),reportSelectors=_interopRequireWildcard(reportSelectors);const SELECTORS_CHECKBOXES='[data-togglegroup="report-select-all"][data-toggle="slave"]:checked',SELECTORS_DELETEBUTTON='[data-action="cohort-delete-selected"]',SELECTORS_EDITNAME='[data-itemtype="cohortname"]';_exports.init=()=>{(0,_prefetch.prefetchStrings)("core_cohort",["delcohortsconfirm"]),(0,_prefetch.prefetchStrings)("core",["delete","deleteselected","selectitem"]),registerEventListeners()};const registerEventListeners=()=>{document.addEventListener(_events2.eventTypes.elementUpdated,(async event=>{if(event.target.closest(SELECTORS_EDITNAME)){const newName=await(0,_str.getString)("selectitem","core",event.target.dataset.value),cohortId=event.target.dataset.itemid,checkbox=document.querySelector('input[value="'.concat(cohortId,'"]')),label=document.querySelector('label[for="'.concat(checkbox.id,'"]'));newName&&label&&(label.innerHTML=newName)}})),document.addEventListener("click",(event=>{const cohortDeleteSelected=event.target.closest(SELECTORS_DELETEBUTTON);if(cohortDeleteSelected){event.preventDefault();const reportElement=document.querySelector(reportSelectors.regions.report),cohortDeleteChecked=reportElement.querySelectorAll(SELECTORS_CHECKBOXES);if(0===cohortDeleteChecked.length)return;_notification.default.saveCancelPromise((0,_str.getString)("deleteselected","core"),(0,_str.getString)("delcohortsconfirm","core_cohort"),(0,_str.getString)("delete","core"),{triggerElement:cohortDeleteSelected}).then((()=>{const pendingPromise=new _pending.default("core_cohort/cohorts:delete"),deleteCohortIds=[...cohortDeleteChecked].map((check=>check.value));return(0,_repository.deleteCohorts)(deleteCohortIds).then((()=>((0,_event_dispatcher.dispatchEvent)(reportEvents.tableReload,{preservePagination:!0},reportElement),pendingPromise.resolve()))).catch(_notification.default.exception)})).catch((()=>{}))}}))};_exports.registerEventListeners=registerEventListeners})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.registerEventListeners=_exports.init=void 0,_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),reportEvents=_interopRequireWildcard(reportEvents),reportSelectors=_interopRequireWildcard(reportSelectors);const SELECTORS_CHECKBOXES='[data-togglegroup="report-select-all"][data-toggle="slave"]:checked',SELECTORS_DELETE='[data-action="cohort-delete"]',SELECTORS_DELETEBUTTON='[data-action="cohort-delete-selected"]',SELECTORS_EDITNAME='[data-itemtype="cohortname"]';_exports.init=()=>{(0,_prefetch.prefetchStrings)("core_cohort",["delcohortsconfirm","delcohortssuccess","delconfirm","delsuccess"]),(0,_prefetch.prefetchStrings)("core",["delete","deleteselected","selectitem"]),registerEventListeners()};const registerEventListeners=()=>{document.addEventListener(_events2.eventTypes.elementUpdated,(async event=>{if(event.target.closest(SELECTORS_EDITNAME)){const cohortId=event.target.dataset.itemid,checkbox=document.querySelector('input[value="'.concat(cohortId,'"][type="checkbox"]')),label=document.querySelector('label[for="'.concat(checkbox.id,'"]'));label&&(label.innerHTML=await(0,_str.getString)("selectitem","core",event.target.dataset.value))}})),document.addEventListener("click",(event=>{const cohortDeleteSingle=event.target.closest(SELECTORS_DELETE);if(cohortDeleteSingle){event.preventDefault();const{cohortId:cohortId,cohortName:cohortName}=cohortDeleteSingle.dataset;_notification.default.saveCancelPromise((0,_str.getString)("deleteselected","core"),(0,_str.getString)("delconfirm","core_cohort",cohortName),(0,_str.getString)("delete","core"),{triggerElement:cohortDeleteSingle}).then((()=>{const pendingPromise=new _pending.default("core_cohort/cohort:delete"),reportElement=event.target.closest(reportSelectors.regions.report);return(0,_repository.deleteCohort)(cohortId).then((()=>(0,_toast.add)((0,_str.getString)("delsuccess","core_cohort")))).then((()=>((0,_event_dispatcher.dispatchEvent)(reportEvents.tableReload,{preservePagination:!0},reportElement),pendingPromise.resolve()))).catch(_notification.default.exception)})).catch((()=>{}))}const cohortDeleteMultiple=event.target.closest(SELECTORS_DELETEBUTTON);if(cohortDeleteMultiple){event.preventDefault();const reportElement=document.querySelector(reportSelectors.regions.report),cohortDeleteChecked=reportElement.querySelectorAll(SELECTORS_CHECKBOXES);if(0===cohortDeleteChecked.length)return;_notification.default.saveCancelPromise((0,_str.getString)("deleteselected","core"),(0,_str.getString)("delcohortsconfirm","core_cohort"),(0,_str.getString)("delete","core"),{triggerElement:cohortDeleteMultiple}).then((()=>{const pendingPromise=new _pending.default("core_cohort/cohorts:delete"),deleteCohortIds=[...cohortDeleteChecked].map((check=>check.value));return(0,_repository.deleteCohorts)(deleteCohortIds).then((()=>(0,_toast.add)((0,_str.getString)("delcohortssuccess","core_cohort")))).then((()=>((0,_event_dispatcher.dispatchEvent)(reportEvents.tableReload,{preservePagination:!0},reportElement),pendingPromise.resolve()))).catch(_notification.default.exception)})).catch((()=>{}))}}))};_exports.registerEventListeners=registerEventListeners})); //# sourceMappingURL=actions.min.js.map \ No newline at end of file diff --git a/cohort/amd/build/actions.min.js.map b/cohort/amd/build/actions.min.js.map index 7f777fd08ae6a..f26d0e3da5929 100644 --- a/cohort/amd/build/actions.min.js.map +++ b/cohort/amd/build/actions.min.js.map @@ -1 +1 @@ -{"version":3,"file":"actions.min.js","sources":["../src/actions.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 * Cohorts actions.\n *\n * @module core_cohort/actions\n * @copyright 2024 David Woloszyn \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {dispatchEvent} from 'core/event_dispatcher';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {prefetchStrings} from 'core/prefetch';\nimport {getString} from 'core/str';\nimport {deleteCohorts} from 'core_cohort/repository';\nimport * as reportEvents from 'core_reportbuilder/local/events';\nimport * as reportSelectors from 'core_reportbuilder/local/selectors';\nimport {eventTypes} from 'core/local/inplace_editable/events';\n\nconst SELECTORS = {\n CHECKBOXES: '[data-togglegroup=\"report-select-all\"][data-toggle=\"slave\"]:checked',\n DELETEBUTTON: '[data-action=\"cohort-delete-selected\"]',\n EDITNAME: '[data-itemtype=\"cohortname\"]',\n};\n\n/**\n * Initialise module.\n */\nexport const init = () => {\n\n prefetchStrings('core_cohort', [\n 'delcohortsconfirm',\n ]);\n\n prefetchStrings('core', [\n 'delete',\n 'deleteselected',\n 'selectitem',\n ]);\n\n registerEventListeners();\n};\n\n/**\n * Register event listeners.\n */\nexport const registerEventListeners = () => {\n\n // Edit cohort name inplace.\n document.addEventListener(eventTypes.elementUpdated, async(event) => {\n\n const editCohortName = event.target.closest(SELECTORS.EDITNAME);\n\n if (editCohortName) {\n const newName = await getString('selectitem', 'core', event.target.dataset.value);\n const cohortId = event.target.dataset.itemid;\n const checkbox = document.querySelector(`input[value=\"${cohortId}\"]`);\n const label = document.querySelector(`label[for=\"${checkbox.id}\"]`);\n\n if (newName && label) {\n label.innerHTML = newName;\n }\n }\n });\n\n // Delete multiple cohorts.\n document.addEventListener('click', event => {\n\n const cohortDeleteSelected = event.target.closest(SELECTORS.DELETEBUTTON);\n\n if (cohortDeleteSelected) {\n event.preventDefault();\n\n const reportElement = document.querySelector(reportSelectors.regions.report);\n const cohortDeleteChecked = reportElement.querySelectorAll(SELECTORS.CHECKBOXES);\n if (cohortDeleteChecked.length === 0) {\n return;\n }\n\n Notification.saveCancelPromise(\n getString('deleteselected', 'core'),\n getString('delcohortsconfirm', 'core_cohort'),\n getString('delete', 'core'),\n {triggerElement: cohortDeleteSelected}\n ).then(() => {\n const pendingPromise = new Pending('core_cohort/cohorts:delete');\n const deleteCohortIds = [...cohortDeleteChecked].map(check => check.value);\n\n // eslint-disable-next-line promise/no-nesting\n return deleteCohorts(deleteCohortIds)\n .then(() => {\n dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement);\n return pendingPromise.resolve();\n })\n .catch(Notification.exception);\n }).catch(() => {\n return;\n });\n }\n });\n};\n"],"names":["SELECTORS","registerEventListeners","document","addEventListener","eventTypes","elementUpdated","async","event","target","closest","newName","dataset","value","cohortId","itemid","checkbox","querySelector","label","id","innerHTML","cohortDeleteSelected","preventDefault","reportElement","reportSelectors","regions","report","cohortDeleteChecked","querySelectorAll","length","saveCancelPromise","triggerElement","then","pendingPromise","Pending","deleteCohortIds","map","check","reportEvents","tableReload","preservePagination","resolve","catch","Notification","exception"],"mappings":";;;;;;;kUAiCMA,qBACU,sEADVA,uBAEY,yCAFZA,mBAGQ,6CAMM,mCAEA,cAAe,CAC3B,oDAGY,OAAQ,CACpB,SACA,iBACA,eAGJC,gCAMSA,uBAAyB,KAGlCC,SAASC,iBAAiBC,oBAAWC,gBAAgBC,MAAAA,WAE1BC,MAAMC,OAAOC,QAAQT,oBAExB,OACVU,cAAgB,kBAAU,aAAc,OAAQH,MAAMC,OAAOG,QAAQC,OACrEC,SAAWN,MAAMC,OAAOG,QAAQG,OAChCC,SAAWb,SAASc,qCAA8BH,gBAClDI,MAAQf,SAASc,mCAA4BD,SAASG,UAExDR,SAAWO,QACXA,MAAME,UAAYT,aAM9BR,SAASC,iBAAiB,SAASI,cAEzBa,qBAAuBb,MAAMC,OAAOC,QAAQT,2BAE9CoB,qBAAsB,CACtBb,MAAMc,uBAEAC,cAAgBpB,SAASc,cAAcO,gBAAgBC,QAAQC,QAC/DC,oBAAsBJ,cAAcK,iBAAiB3B,yBACxB,IAA/B0B,oBAAoBE,oCAIXC,mBACT,kBAAU,iBAAkB,SAC5B,kBAAU,oBAAqB,gBAC/B,kBAAU,SAAU,QACpB,CAACC,eAAgBV,uBACnBW,MAAK,WACGC,eAAiB,IAAIC,iBAAQ,8BAC7BC,gBAAkB,IAAIR,qBAAqBS,KAAIC,OAASA,MAAMxB,eAG7D,6BAAcsB,iBAChBH,MAAK,yCACYM,aAAaC,YAAa,CAACC,oBAAoB,GAAOjB,eAC7DU,eAAeQ,aAEzBC,MAAMC,sBAAaC,cACzBF,OAAM"} \ No newline at end of file +{"version":3,"file":"actions.min.js","sources":["../src/actions.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 * Cohorts actions.\n *\n * @module core_cohort/actions\n * @copyright 2024 David Woloszyn \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {dispatchEvent} from 'core/event_dispatcher';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {prefetchStrings} from 'core/prefetch';\nimport {getString} from 'core/str';\nimport {add as addToast} from 'core/toast';\nimport {deleteCohort, deleteCohorts} from 'core_cohort/repository';\nimport * as reportEvents from 'core_reportbuilder/local/events';\nimport * as reportSelectors from 'core_reportbuilder/local/selectors';\nimport {eventTypes} from 'core/local/inplace_editable/events';\n\nconst SELECTORS = {\n CHECKBOXES: '[data-togglegroup=\"report-select-all\"][data-toggle=\"slave\"]:checked',\n DELETE: '[data-action=\"cohort-delete\"]',\n DELETEBUTTON: '[data-action=\"cohort-delete-selected\"]',\n EDITNAME: '[data-itemtype=\"cohortname\"]',\n};\n\n/**\n * Initialise module.\n */\nexport const init = () => {\n\n prefetchStrings('core_cohort', [\n 'delcohortsconfirm',\n 'delcohortssuccess',\n 'delconfirm',\n 'delsuccess',\n ]);\n\n prefetchStrings('core', [\n 'delete',\n 'deleteselected',\n 'selectitem',\n ]);\n\n registerEventListeners();\n};\n\n/**\n * Register event listeners.\n */\nexport const registerEventListeners = () => {\n\n // Edit cohort name inplace.\n document.addEventListener(eventTypes.elementUpdated, async(event) => {\n const editCohortName = event.target.closest(SELECTORS.EDITNAME);\n if (editCohortName) {\n const cohortId = event.target.dataset.itemid;\n const checkbox = document.querySelector(`input[value=\"${cohortId}\"][type=\"checkbox\"]`);\n const label = document.querySelector(`label[for=\"${checkbox.id}\"]`);\n if (label) {\n label.innerHTML = await getString('selectitem', 'core', event.target.dataset.value);\n }\n }\n });\n\n document.addEventListener('click', event => {\n\n // Delete single cohort.\n const cohortDeleteSingle = event.target.closest(SELECTORS.DELETE);\n if (cohortDeleteSingle) {\n event.preventDefault();\n\n const {cohortId, cohortName} = cohortDeleteSingle.dataset;\n\n Notification.saveCancelPromise(\n getString('deleteselected', 'core'),\n getString('delconfirm', 'core_cohort', cohortName),\n getString('delete', 'core'),\n {triggerElement: cohortDeleteSingle}\n ).then(() => {\n const pendingPromise = new Pending('core_cohort/cohort:delete');\n const reportElement = event.target.closest(reportSelectors.regions.report);\n\n // eslint-disable-next-line promise/no-nesting\n return deleteCohort(cohortId)\n .then(() => addToast(getString('delsuccess', 'core_cohort')))\n .then(() => {\n dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement);\n return pendingPromise.resolve();\n })\n .catch(Notification.exception);\n }).catch(() => {\n return;\n });\n }\n\n // Delete multiple cohorts.\n const cohortDeleteMultiple = event.target.closest(SELECTORS.DELETEBUTTON);\n if (cohortDeleteMultiple) {\n event.preventDefault();\n\n const reportElement = document.querySelector(reportSelectors.regions.report);\n const cohortDeleteChecked = reportElement.querySelectorAll(SELECTORS.CHECKBOXES);\n if (cohortDeleteChecked.length === 0) {\n return;\n }\n\n Notification.saveCancelPromise(\n getString('deleteselected', 'core'),\n getString('delcohortsconfirm', 'core_cohort'),\n getString('delete', 'core'),\n {triggerElement: cohortDeleteMultiple}\n ).then(() => {\n const pendingPromise = new Pending('core_cohort/cohorts:delete');\n const deleteCohortIds = [...cohortDeleteChecked].map(check => check.value);\n\n // eslint-disable-next-line promise/no-nesting\n return deleteCohorts(deleteCohortIds)\n .then(() => addToast(getString('delcohortssuccess', 'core_cohort')))\n .then(() => {\n dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement);\n return pendingPromise.resolve();\n })\n .catch(Notification.exception);\n }).catch(() => {\n return;\n });\n }\n });\n};\n"],"names":["SELECTORS","registerEventListeners","document","addEventListener","eventTypes","elementUpdated","async","event","target","closest","cohortId","dataset","itemid","checkbox","querySelector","label","id","innerHTML","value","cohortDeleteSingle","preventDefault","cohortName","saveCancelPromise","triggerElement","then","pendingPromise","Pending","reportElement","reportSelectors","regions","report","reportEvents","tableReload","preservePagination","resolve","catch","Notification","exception","cohortDeleteMultiple","cohortDeleteChecked","querySelectorAll","length","deleteCohortIds","map","check"],"mappings":";;;;;;;kUAkCMA,qBACU,sEADVA,iBAEM,gCAFNA,uBAGY,yCAHZA,mBAIQ,6CAMM,mCAEA,cAAe,CAC3B,oBACA,oBACA,aACA,6CAGY,OAAQ,CACpB,SACA,iBACA,eAGJC,gCAMSA,uBAAyB,KAGlCC,SAASC,iBAAiBC,oBAAWC,gBAAgBC,MAAAA,WAC1BC,MAAMC,OAAOC,QAAQT,oBACxB,OACVU,SAAWH,MAAMC,OAAOG,QAAQC,OAChCC,SAAWX,SAASY,qCAA8BJ,iCAClDK,MAAQb,SAASY,mCAA4BD,SAASG,UACxDD,QACAA,MAAME,gBAAkB,kBAAU,aAAc,OAAQV,MAAMC,OAAOG,QAAQO,YAKzFhB,SAASC,iBAAiB,SAASI,cAGzBY,mBAAqBZ,MAAMC,OAAOC,QAAQT,qBAC5CmB,mBAAoB,CACpBZ,MAAMa,uBAEAV,SAACA,SAADW,WAAWA,YAAcF,mBAAmBR,8BAErCW,mBACT,kBAAU,iBAAkB,SAC5B,kBAAU,aAAc,cAAeD,aACvC,kBAAU,SAAU,QACpB,CAACE,eAAgBJ,qBACnBK,MAAK,WACGC,eAAiB,IAAIC,iBAAQ,6BAC7BC,cAAgBpB,MAAMC,OAAOC,QAAQmB,gBAAgBC,QAAQC,eAG5D,4BAAapB,UACfc,MAAK,KAAM,eAAS,kBAAU,aAAc,kBAC5CA,MAAK,yCACYO,aAAaC,YAAa,CAACC,oBAAoB,GAAON,eAC7DF,eAAeS,aAEzBC,MAAMC,sBAAaC,cACzBF,OAAM,eAMPG,qBAAuB/B,MAAMC,OAAOC,QAAQT,2BAC9CsC,qBAAsB,CACtB/B,MAAMa,uBAEAO,cAAgBzB,SAASY,cAAcc,gBAAgBC,QAAQC,QAC/DS,oBAAsBZ,cAAca,iBAAiBxC,yBACxB,IAA/BuC,oBAAoBE,oCAIXnB,mBACT,kBAAU,iBAAkB,SAC5B,kBAAU,oBAAqB,gBAC/B,kBAAU,SAAU,QACpB,CAACC,eAAgBe,uBACnBd,MAAK,WACGC,eAAiB,IAAIC,iBAAQ,8BAC7BgB,gBAAkB,IAAIH,qBAAqBI,KAAIC,OAASA,MAAM1B,eAG7D,6BAAcwB,iBAChBlB,MAAK,KAAM,eAAS,kBAAU,oBAAqB,kBACnDA,MAAK,yCACYO,aAAaC,YAAa,CAACC,oBAAoB,GAAON,eAC7DF,eAAeS,aAEzBC,MAAMC,sBAAaC,cACzBF,OAAM"} \ No newline at end of file diff --git a/cohort/amd/build/repository.min.js b/cohort/amd/build/repository.min.js index 76f8244785fa8..e69e720323926 100644 --- a/cohort/amd/build/repository.min.js +++ b/cohort/amd/build/repository.min.js @@ -5,6 +5,6 @@ define("core_cohort/repository",["exports","core/ajax"],(function(_exports,_ajax * @module core_cohort/repository * @copyright 2024 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.deleteCohorts=_exports.deleteCohort=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.deleteCohort=cohort=>deleteCohorts([cohort]);const deleteCohorts=cohorts=>{const request={methodname:"core_cohort_delete_cohorts",args:{cohortids:cohorts}};return _ajax.default.call([request])[0]};_exports.deleteCohorts=deleteCohorts})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.deleteCohorts=_exports.deleteCohort=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.deleteCohort=cohortid=>deleteCohorts([cohortid]);const deleteCohorts=cohortids=>{const request={methodname:"core_cohort_delete_cohorts",args:{cohortids:cohortids}};return _ajax.default.call([request])[0]};_exports.deleteCohorts=deleteCohorts})); //# sourceMappingURL=repository.min.js.map \ No newline at end of file diff --git a/cohort/amd/build/repository.min.js.map b/cohort/amd/build/repository.min.js.map index 893bb2c2bdc11..ea344a898231b 100644 --- a/cohort/amd/build/repository.min.js.map +++ b/cohort/amd/build/repository.min.js.map @@ -1 +1 @@ -{"version":3,"file":"repository.min.js","sources":["../src/repository.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 * Module to handle cohort AJAX requests.\n *\n * @module core_cohort/repository\n * @copyright 2024 David Woloszyn \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\n\n/**\n * Delete single cohort.\n *\n * @param {Number} cohort Cohort ID\n * @return {Promise}\n */\nexport const deleteCohort = cohort => deleteCohorts([cohort]);\n\n/**\n * Delete multiple cohorts.\n *\n * @param {Number[]} cohorts Cohort IDs\n * @return {Promise}\n */\nexport const deleteCohorts = cohorts => {\n const request = {\n methodname: 'core_cohort_delete_cohorts',\n args: {cohortids: cohorts}\n };\n\n return Ajax.call([request])[0];\n};\n"],"names":["cohort","deleteCohorts","cohorts","request","methodname","args","cohortids","Ajax","call"],"mappings":";;;;;;;2LA+B4BA,QAAUC,cAAc,CAACD,eAQxCC,cAAgBC,gBACnBC,QAAU,CACZC,WAAY,6BACZC,KAAM,CAACC,UAAWJ,iBAGfK,cAAKC,KAAK,CAACL,UAAU"} \ No newline at end of file +{"version":3,"file":"repository.min.js","sources":["../src/repository.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 * Module to handle cohort AJAX requests.\n *\n * @module core_cohort/repository\n * @copyright 2024 David Woloszyn \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\n\n/**\n * Delete single cohort.\n *\n * @param {Number} cohortid\n * @return {Promise}\n */\nexport const deleteCohort = cohortid => deleteCohorts([cohortid]);\n\n/**\n * Delete multiple cohorts.\n *\n * @param {Number[]} cohortids\n * @return {Promise}\n */\nexport const deleteCohorts = cohortids => {\n const request = {\n methodname: 'core_cohort_delete_cohorts',\n args: {cohortids},\n };\n\n return Ajax.call([request])[0];\n};\n"],"names":["cohortid","deleteCohorts","cohortids","request","methodname","args","Ajax","call"],"mappings":";;;;;;;2LA+B4BA,UAAYC,cAAc,CAACD,iBAQ1CC,cAAgBC,kBACnBC,QAAU,CACZC,WAAY,6BACZC,KAAM,CAACH,UAAAA,mBAGJI,cAAKC,KAAK,CAACJ,UAAU"} \ No newline at end of file diff --git a/cohort/amd/src/actions.js b/cohort/amd/src/actions.js index e529714f79852..2e580e796b2a8 100644 --- a/cohort/amd/src/actions.js +++ b/cohort/amd/src/actions.js @@ -26,13 +26,15 @@ import Notification from 'core/notification'; import Pending from 'core/pending'; import {prefetchStrings} from 'core/prefetch'; import {getString} from 'core/str'; -import {deleteCohorts} from 'core_cohort/repository'; +import {add as addToast} from 'core/toast'; +import {deleteCohort, deleteCohorts} from 'core_cohort/repository'; import * as reportEvents from 'core_reportbuilder/local/events'; import * as reportSelectors from 'core_reportbuilder/local/selectors'; import {eventTypes} from 'core/local/inplace_editable/events'; const SELECTORS = { CHECKBOXES: '[data-togglegroup="report-select-all"][data-toggle="slave"]:checked', + DELETE: '[data-action="cohort-delete"]', DELETEBUTTON: '[data-action="cohort-delete-selected"]', EDITNAME: '[data-itemtype="cohortname"]', }; @@ -44,6 +46,9 @@ export const init = () => { prefetchStrings('core_cohort', [ 'delcohortsconfirm', + 'delcohortssuccess', + 'delconfirm', + 'delsuccess', ]); prefetchStrings('core', [ @@ -62,27 +67,51 @@ export const registerEventListeners = () => { // Edit cohort name inplace. document.addEventListener(eventTypes.elementUpdated, async(event) => { - const editCohortName = event.target.closest(SELECTORS.EDITNAME); - if (editCohortName) { - const newName = await getString('selectitem', 'core', event.target.dataset.value); const cohortId = event.target.dataset.itemid; - const checkbox = document.querySelector(`input[value="${cohortId}"]`); + const checkbox = document.querySelector(`input[value="${cohortId}"][type="checkbox"]`); const label = document.querySelector(`label[for="${checkbox.id}"]`); - - if (newName && label) { - label.innerHTML = newName; + if (label) { + label.innerHTML = await getString('selectitem', 'core', event.target.dataset.value); } } }); - // Delete multiple cohorts. document.addEventListener('click', event => { - const cohortDeleteSelected = event.target.closest(SELECTORS.DELETEBUTTON); + // Delete single cohort. + const cohortDeleteSingle = event.target.closest(SELECTORS.DELETE); + if (cohortDeleteSingle) { + event.preventDefault(); + + const {cohortId, cohortName} = cohortDeleteSingle.dataset; + + Notification.saveCancelPromise( + getString('deleteselected', 'core'), + getString('delconfirm', 'core_cohort', cohortName), + getString('delete', 'core'), + {triggerElement: cohortDeleteSingle} + ).then(() => { + const pendingPromise = new Pending('core_cohort/cohort:delete'); + const reportElement = event.target.closest(reportSelectors.regions.report); + + // eslint-disable-next-line promise/no-nesting + return deleteCohort(cohortId) + .then(() => addToast(getString('delsuccess', 'core_cohort'))) + .then(() => { + dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement); + return pendingPromise.resolve(); + }) + .catch(Notification.exception); + }).catch(() => { + return; + }); + } - if (cohortDeleteSelected) { + // Delete multiple cohorts. + const cohortDeleteMultiple = event.target.closest(SELECTORS.DELETEBUTTON); + if (cohortDeleteMultiple) { event.preventDefault(); const reportElement = document.querySelector(reportSelectors.regions.report); @@ -95,13 +124,14 @@ export const registerEventListeners = () => { getString('deleteselected', 'core'), getString('delcohortsconfirm', 'core_cohort'), getString('delete', 'core'), - {triggerElement: cohortDeleteSelected} + {triggerElement: cohortDeleteMultiple} ).then(() => { const pendingPromise = new Pending('core_cohort/cohorts:delete'); const deleteCohortIds = [...cohortDeleteChecked].map(check => check.value); // eslint-disable-next-line promise/no-nesting return deleteCohorts(deleteCohortIds) + .then(() => addToast(getString('delcohortssuccess', 'core_cohort'))) .then(() => { dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement); return pendingPromise.resolve(); diff --git a/cohort/amd/src/repository.js b/cohort/amd/src/repository.js index 8bd52578e630f..d570d9df5bde2 100644 --- a/cohort/amd/src/repository.js +++ b/cohort/amd/src/repository.js @@ -26,21 +26,21 @@ import Ajax from 'core/ajax'; /** * Delete single cohort. * - * @param {Number} cohort Cohort ID + * @param {Number} cohortid * @return {Promise} */ -export const deleteCohort = cohort => deleteCohorts([cohort]); +export const deleteCohort = cohortid => deleteCohorts([cohortid]); /** * Delete multiple cohorts. * - * @param {Number[]} cohorts Cohort IDs + * @param {Number[]} cohortids * @return {Promise} */ -export const deleteCohorts = cohorts => { +export const deleteCohorts = cohortids => { const request = { methodname: 'core_cohort_delete_cohorts', - args: {cohortids: cohorts} + args: {cohortids}, }; return Ajax.call([request])[0]; diff --git a/cohort/classes/reportbuilder/local/systemreports/cohorts.php b/cohort/classes/reportbuilder/local/systemreports/cohorts.php index 84e2d151b0844..c9394666aa411 100644 --- a/cohort/classes/reportbuilder/local/systemreports/cohorts.php +++ b/cohort/classes/reportbuilder/local/systemreports/cohorts.php @@ -188,12 +188,7 @@ protected function add_columns(): void { * unique identifier */ protected function add_filters(): void { - $filters = [ - 'cohort:name', - 'cohort:idnumber', - 'cohort:description', - ]; - $this->add_filters_from_entities($filters); + $this->add_filters_from_entity('cohort', ['name', 'idnumber', 'description', 'customfield*']); } /** @@ -245,9 +240,9 @@ protected function add_actions(): void { // Delete action. It will be only shown if user has 'moodle/cohort:manage' capabillity. $this->add_action((new action( - new moodle_url('/cohort/edit.php', ['id' => ':id', 'delete' => 1, 'returnurl' => $returnurl]), + new moodle_url('#'), new pix_icon('t/delete', '', 'core'), - ['class' => 'text-danger'], + ['class' => 'text-danger', 'data-action' => 'cohort-delete', 'data-cohort-id' => ':id', 'data-cohort-name' => ':name'], false, new lang_string('delete') ))->add_callback(function(stdClass $row): bool { diff --git a/cohort/edit.php b/cohort/edit.php index ff03dde09d793..a68969581e5d8 100644 --- a/cohort/edit.php +++ b/cohort/edit.php @@ -29,10 +29,8 @@ $id = optional_param('id', 0, PARAM_INT); $contextid = optional_param('contextid', 0, PARAM_INT); -$delete = optional_param('delete', 0, PARAM_BOOL); $show = optional_param('show', 0, PARAM_BOOL); $hide = optional_param('hide', 0, PARAM_BOOL); -$confirm = optional_param('confirm', 0, PARAM_BOOL); $returnurl = optional_param('returnurl', '', PARAM_LOCALURL); require_login(); @@ -86,25 +84,6 @@ $PAGE->set_heading($COURSE->fullname); } -if ($delete and $cohort->id) { - $PAGE->url->param('delete', 1); - if ($confirm and confirm_sesskey()) { - cohort_delete_cohort($cohort); - redirect($returnurl); - } - $strheading = get_string('delcohort', 'cohort'); - $PAGE->navbar->add($strheading); - $PAGE->set_title($strheading); - echo $OUTPUT->header(); - echo $OUTPUT->heading($strheading); - $yesurl = new moodle_url('/cohort/edit.php', array('id' => $cohort->id, 'delete' => 1, - 'confirm' => 1, 'sesskey' => sesskey(), 'returnurl' => $returnurl->out_as_local_url())); - $message = get_string('delconfirm', 'cohort', format_string($cohort->name)); - echo $OUTPUT->confirm($message, $yesurl, $returnurl); - echo $OUTPUT->footer(); - die; -} - if ($show && $cohort->id && confirm_sesskey()) { if (!$cohort->visible) { $record = (object)array('id' => $cohort->id, 'visible' => 1, 'contextid' => $cohort->contextid); diff --git a/cohort/tests/behat/add_cohort.feature b/cohort/tests/behat/add_cohort.feature index 5b1f6a6b18f68..9b80890097238 100644 --- a/cohort/tests/behat/add_cohort.feature +++ b/cohort/tests/behat/add_cohort.feature @@ -121,8 +121,17 @@ Feature: Add cohorts of users @javascript Scenario: Edit cohort name in-place When I navigate to "Users > Accounts > Cohorts" in site administration + Then the following should exist in the "reportbuilder-table" table: + | Name | Cohort ID | Description | + | Test cohort name | 333 | Test cohort description | And I set the field "Edit cohort name" to "Students cohort" - Then I should not see "Test cohort name" - And I should see "Students cohort" - And I navigate to "Users > Accounts > Cohorts" in site administration - And I should see "Students cohort" + And the following should not exist in the "reportbuilder-table" table: + | Name | Cohort ID | Description | + | Test cohort name | 333 | Test cohort description | + And the following should exist in the "reportbuilder-table" table: + | Name | Cohort ID | Description | + | Students cohort | 333 | Test cohort description | + And I reload the page + And the following should exist in the "reportbuilder-table" table: + | Name | Cohort ID | Description | + | Students cohort | 333 | Test cohort description | diff --git a/cohort/tests/behat/delete_cohorts.feature b/cohort/tests/behat/delete_cohorts.feature index 8ad66c67fe922..1b65754eebcfa 100644 --- a/cohort/tests/behat/delete_cohorts.feature +++ b/cohort/tests/behat/delete_cohorts.feature @@ -1,6 +1,6 @@ @core @core_cohort Feature: Delete cohorts - In order to delete multiple cohorts + In order to delete cohorts As an admin I need to select specific cohorts and perform that action @@ -12,17 +12,37 @@ Feature: Delete cohorts | Cohort 3 | cohort3 | System | | About cohort 3 | @javascript - Scenario: I can delete multiple cohorts using the checkboxes + Scenario: Delete single cohort Given I log in as "admin" And I navigate to "Users > Accounts > Cohorts" in site administration - And I should see "Cohort 1" - And I should see "Cohort 2" - And I should see "Cohort 3" - And I click on "Select 'Cohort 1'" "checkbox" + And the following should exist in the "Cohorts" table: + | Name | Cohort ID | + | Cohort 1 | cohort1 | + | Cohort 2 | cohort2 | + | Cohort 3 | cohort3 | + When I press "Delete" action in the "Cohort 1" report row + Then I should see "Do you really want to delete cohort 'Cohort 1'?" in the "Delete selected" "dialogue" + And I click on "Delete" "button" in the "Delete selected" "dialogue" + And I should see "Deleted cohort" + And I should not see "Cohort 1" in the "Cohorts" "table" + And I should see "Cohort 2" in the "Cohorts" "table" + And I should see "Cohort 3" in the "Cohorts" "table" + + @javascript + Scenario: Delete multiple cohorts + Given I log in as "admin" + And I navigate to "Users > Accounts > Cohorts" in site administration + And the following should exist in the "Cohorts" table: + | Name | Cohort ID | + | Cohort 1 | cohort1 | + | Cohort 2 | cohort2 | + | Cohort 3 | cohort3 | + When I click on "Select 'Cohort 1'" "checkbox" And I click on "Select 'Cohort 2'" "checkbox" - When I click on "Delete selected" "button" - Then I should see "Do you really want to delete the selected cohorts?" + And I click on "Delete selected" "button" + Then I should see "Do you really want to delete the selected cohorts?" in the "Delete selected" "dialogue" And I click on "Delete" "button" in the "Delete selected" "dialogue" - And I should not see "Cohort 1" - And I should not see "Cohort 2" - And I should see "Cohort 3" + And I should see "Deleted selected cohorts" + And I should not see "Cohort 1" in the "Cohorts" "table" + And I should not see "Cohort 2" in the "Cohorts" "table" + And I should see "Cohort 3" in the "Cohorts" "table" diff --git a/cohort/tests/behat/view_cohorts.feature b/cohort/tests/behat/view_cohorts.feature index e18bf968ca178..bdf42bc10945a 100644 --- a/cohort/tests/behat/view_cohorts.feature +++ b/cohort/tests/behat/view_cohorts.feature @@ -65,6 +65,15 @@ Feature: View cohort list @javascript Scenario: Cohorts list can be filtered + Given the following "custom field categories" exist: + | name | component | area | itemid | + | Newcat | core_cohort | cohort | 0 | + And the following "custom fields" exist: + | name | category | type | shortname | description | configdata | + | Field checkbox | Newcat | checkbox | checkbox | | | + And the following "cohorts" exist: + | name | idnumber | contextlevel | reference | customfield_checkbox | + | Cohort with CF | CH4 | Category | CAT1 | 1 | When I log in as "admin" And I navigate to "Users > Accounts > Cohorts" in site administration And I follow "All cohorts" @@ -73,11 +82,24 @@ Feature: View cohort list | Name operator | Contains | | Name value | category 1 | And I click on "Apply" "button" in the "[data-region='report-filters']" "css_element" - Then the following should exist in the "reportbuilder-table" table: + Then the following should exist in the "Cohorts" table: | Category | Name | | Cat 1 | Cohort in category 1 | - And the following should not exist in the "reportbuilder-table" table: + And the following should not exist in the "Cohorts" table: | Category | Name | | Cat 2 | Cohort in category 2 | | Cat 3 | Cohort in category 3 | | System | System cohort | + And I click on "Reset all" "button" in the "[data-region='report-filters']" "css_element" + And I set the following fields in the "Field checkbox" "core_reportbuilder > Filter" to these values: + | Field checkbox operator | Yes | + And I click on "Apply" "button" in the "[data-region='report-filters']" "css_element" + And the following should exist in the "Cohorts" table: + | Category | Name | + | Cat 1 | Cohort with CF | + And the following should not exist in the "Cohorts" table: + | Category | Name | + | Cat 1 | Cohort in category 1 | + | Cat 2 | Cohort in category 2 | + | Cat 3 | Cohort in category 3 | + | System | System cohort | diff --git a/comment/classes/reportbuilder/local/entities/comment.php b/comment/classes/reportbuilder/local/entities/comment.php index 659035e8b8ddc..cd34cb4c8dc29 100644 --- a/comment/classes/reportbuilder/local/entities/comment.php +++ b/comment/classes/reportbuilder/local/entities/comment.php @@ -193,10 +193,8 @@ protected function get_all_columns(): array { $this->get_entity_name() )) ->add_joins($this->get_joins()) - ->set_type(column::TYPE_INTEGER) ->add_fields("{$commentalias}.itemid") - ->set_is_sortable(true) - ->set_disabled_aggregation_all(); + ->set_is_sortable(true); // Time created. $columns[] = (new column( diff --git a/comment/comment.js b/comment/comment.js index 806ee9c64251b..13ab753fc10ae 100644 --- a/comment/comment.js +++ b/comment/comment.js @@ -100,9 +100,9 @@ M.core_comment = { var newcomment = Y.Node.create(result.html); container.appendChild(newcomment); var ids = result.ids; - var linkText = Y.one('#comment-link-text-' + cid); - if (linkText) { - linkText.set('innerHTML', M.util.get_string('commentscount', 'moodle', obj.count)); + var linkTextCount = Y.one('#comment-link-text-' + cid + ' .comment-link-count'); + if (linkTextCount) { + linkTextCount.set('innerHTML', obj.count); } for(var i in ids) { var attributes = { @@ -249,9 +249,9 @@ M.core_comment = { scope: scope, params: params, callback: async function(id, ret, args) { - var linkText = Y.one('#comment-link-text-' + scope.client_id); - if (ret.count && linkText) { - linkText.set('innerHTML', M.util.get_string('commentscount', 'moodle', ret.count)); + var linkTextCount = Y.one('#comment-link-text-' + scope.client_id + ' .comment-link-count'); + if (linkTextCount) { + linkTextCount.set('innerHTML', ret.count); } var container = Y.one('#comment-list-'+scope.client_id); var pagination = Y.one('#comment-pagination-'+scope.client_id); @@ -284,10 +284,10 @@ M.core_comment = { params = {'commentid': id}; function remove_dom(type, anim, cmt) { cmt.remove(); - var linkText = Y.one('#comment-link-text-' + cid), + var linkTextCount = Y.one('#comment-link-text-' + cid + ' .comment-link-count'), comments = Y.all('#comment-list-' + cid + ' li'); - if (linkText && comments) { - linkText.set('innerHTML', M.util.get_string('commentscount', 'moodle', comments.size())); + if (linkTextCount) { + linkTextCount.set('innerHTML', comments.size()); } } this.request({ diff --git a/comment/lib.php b/comment/lib.php index e555f93927c95..41c77e87147ed 100644 --- a/comment/lib.php +++ b/comment/lib.php @@ -263,7 +263,6 @@ public static function init(moodle_page $page = null) { $page->requires->strings_for_js(array( 'addcomment', 'comments', - 'commentscount', 'commentsrequirelogin', 'deletecommentbyon' ), @@ -454,7 +453,7 @@ public function output($return = true) { // comments open and closed $countstring = ''; if ($this->displaytotalcount) { - $countstring = '('.$this->count().')'; + $countstring = '(' . html_writer::span($this->count(), 'comment-link-count') . ')'; } $collapsedimage= 't/collapsed'; if (right_to_left()) { diff --git a/communication/classes/api.php b/communication/classes/api.php index b7b8e431b9bf7..a98592880fe62 100644 --- a/communication/classes/api.php +++ b/communication/classes/api.php @@ -265,13 +265,13 @@ public function form_definition_for_provider(\MoodleQuickForm $mform, string $pr $mform->insertElementBefore( $mform->createElement( 'text', - 'communicationroomname', + $provider . 'roomname', get_string('communicationroomname', 'communication'), 'maxlength="100" size="20"' ), 'addcommunicationoptionshere' ); - $mform->setType('communicationroomname', PARAM_TEXT); + $mform->setType($provider . 'roomname', PARAM_TEXT); $mform->insertElementBefore( $mform->createElement( @@ -376,6 +376,9 @@ public function set_avatar(?\stored_file $avatar): bool { * @return string */ public function get_room_name(): string { + if (!$this->communication) { + return ''; + } return $this->communication->get_room_name(); } @@ -387,7 +390,8 @@ public function get_room_name(): string { public function set_data(\stdClass $instance): void { if (!empty($instance->id) && $this->communication) { $instance->selectedcommunication = $this->communication->get_provider(); - $instance->communicationroomname = $this->communication->get_room_name(); + $roomnameidentifier = $this->get_provider() . 'roomname'; + $instance->$roomnameidentifier = $this->communication->get_room_name(); $this->communication->get_form_provider()->set_form_data($instance); } @@ -474,8 +478,6 @@ public function configure_room_and_membership_by_provider( // Now deactivate the previous provider. $this->update_room( active: processor::PROVIDER_INACTIVE, - communicationroomname: $communicationroomname, - avatar: $instanceimage, instance: $instance, queue: $queue, ); diff --git a/communication/classes/helper.php b/communication/classes/helper.php index 5a691669d3121..1d7bf47e4fe04 100644 --- a/communication/classes/helper.php +++ b/communication/classes/helper.php @@ -430,10 +430,14 @@ public static function update_course_communication_instance( if (empty($provider)) { $provider = $coursecommunication->get_provider(); } + $roomnameidenfier = $provider . 'roomname'; // Determine the communication room name if none was provided and add it to the course data. - if (empty($course->communicationroomname)) { - $course->communicationroomname = $course->fullname ?? get_course($course->id)->fullname; + if (empty($course->$roomnameidenfier)) { + $course->$roomnameidenfier = $coursecommunication->get_room_name(); + if (empty($course->$roomnameidenfier)) { + $course->$roomnameidenfier = $course->fullname ?? get_course($course->id)->fullname; + } } // List of enrolled users for course communication. @@ -465,7 +469,7 @@ public static function update_course_communication_instance( $communication->configure_room_and_membership_by_provider( provider: $provider, instance: $course, - communicationroomname: $course->communicationroomname, + communicationroomname: $course->$roomnameidenfier, users: $enrolledusers, instanceimage: $courseimage, ); @@ -486,7 +490,7 @@ public static function update_course_communication_instance( $communication->configure_room_and_membership_by_provider( provider: $provider, instance: $course, - communicationroomname: $course->communicationroomname, + communicationroomname: $course->$roomnameidenfier, users: $enrolledusers, instanceimage: $courseimage, queue: false, @@ -533,8 +537,9 @@ public static function update_group_communication_instances_for_course( context: $coursecontext, ); + $roomnameidenfier = $provider . 'roomname'; $communicationroomname = self::format_group_room_name( - baseroomname: $course->communicationroomname, + baseroomname: $course->$roomnameidenfier, groupname: $coursegroup->name, ); diff --git a/communication/provider/customlink/classes/communication_feature.php b/communication/provider/customlink/classes/communication_feature.php index e2b4c760a0518..51062176ff350 100644 --- a/communication/provider/customlink/classes/communication_feature.php +++ b/communication/provider/customlink/classes/communication_feature.php @@ -113,13 +113,17 @@ public function get_chat_room_url(): ?string { } public function save_form_data(\stdClass $instance): void { + if (empty($instance->customlinkurl)) { + return; + } + global $DB; $commid = $this->communication->get_id(); $cachekey = "link_url_{$commid}"; $newrecord = new \stdClass(); - $newrecord->url = $instance->customlinkurl ?? null; + $newrecord->url = $instance->customlinkurl; $existingrecord = $DB->get_record( self::CUSTOMLINK_TABLE, @@ -131,7 +135,7 @@ public function save_form_data(\stdClass $instance): void { // Create the record if it does not exist. $newrecord->commid = $commid; $DB->insert_record(self::CUSTOMLINK_TABLE, $newrecord); - } else if ($newrecord->url !== $existingrecord->url) { + } else if ($instance->customlinkurl !== $existingrecord->url) { // Update record if the URL has changed. $newrecord->id = $existingrecord->id; $DB->update_record(self::CUSTOMLINK_TABLE, $newrecord); diff --git a/communication/provider/customlink/tests/behat/custom_link.feature b/communication/provider/customlink/tests/behat/custom_link.feature index ab75d8258c74b..2668ed389c6b1 100644 --- a/communication/provider/customlink/tests/behat/custom_link.feature +++ b/communication/provider/customlink/tests/behat/custom_link.feature @@ -28,8 +28,8 @@ Feature: Communication custom link And I select "Custom link" from the "Provider" singleselect And I should see "Custom link URL" And I set the following fields to these values: - | communicationroomname | Test URL | - | customlinkurl | #wwwroot#/communication/provider/customlink/tests/behat/fixtures/custom_link_test_page.php | + | communication_customlinkroomname | Test URL | + | customlinkurl | #wwwroot#/communication/provider/customlink/tests/behat/fixtures/custom_link_test_page.php | And I press "Save changes" Then "Chat to course participants" "button" should be visible And I click on "Chat to course participants" "button" @@ -59,8 +59,8 @@ Feature: Communication custom link When I navigate to "Communication" in current page administration And I select "Custom link" from the "Provider" singleselect And I set the following fields to these values: - | communicationroomname | Test URL | - | customlinkurl | #wwwroot#/communication/provider/customlink/tests/behat/fixtures/custom_link_test_page.php | + | communication_customlinkroomname | Test URL | + | customlinkurl | #wwwroot#/communication/provider/customlink/tests/behat/fixtures/custom_link_test_page.php | And I press "Save changes" And "Chat to course participants" "button" should be visible And I run all adhoc tasks @@ -74,8 +74,8 @@ Feature: Communication custom link And I navigate to "Communication" in current page administration And I select "Custom link" from the "Provider" singleselect And I set the following fields to these values: - | communicationroomname | Test URL | - | customlinkurl | #wwwroot#/communication/provider/customlink/tests/behat/fixtures/custom_link_test_page.php | + | communication_customlinkroomname | Test URL | + | customlinkurl | #wwwroot#/communication/provider/customlink/tests/behat/fixtures/custom_link_test_page.php | And I press "Save changes" And "Chat to course participants" "button" should be visible And I run all adhoc tasks diff --git a/communication/provider/customlink/tests/communication_feature_test.php b/communication/provider/customlink/tests/communication_feature_test.php index d68141f7b400b..6c33830608979 100644 --- a/communication/provider/customlink/tests/communication_feature_test.php +++ b/communication/provider/customlink/tests/communication_feature_test.php @@ -88,6 +88,22 @@ public function test_save_form_data(): void { $communicationprocessor->get_form_provider()->save_form_data($formdatainstance); $fetchedurl = $communicationprocessor->get_room_provider()->get_chat_room_url(); $this->assertEquals($customlinkurl, $fetchedurl); + + // Test with empty customlinkurl. + $customlinkurlempty = ''; + $formdatainstance = (object) ['customlinkurl' => $customlinkurlempty]; + $communicationprocessor->get_form_provider()->save_form_data($formdatainstance); + $fetchedurl = $communicationprocessor->get_room_provider()->get_chat_room_url(); + // It should not update the url to an empty one. + $this->assertEquals($customlinkurl, $fetchedurl); + + // Test with null customlinkurl. + $customlinkurlempty = null; + $formdatainstance = (object) ['customlinkurl' => $customlinkurlempty]; + $communicationprocessor->get_form_provider()->save_form_data($formdatainstance); + $fetchedurl = $communicationprocessor->get_room_provider()->get_chat_room_url(); + // It should not update the url to a null one. + $this->assertEquals($customlinkurl, $fetchedurl); } /** diff --git a/communication/provider/matrix/tests/behat/matrix_form_fields.feature b/communication/provider/matrix/tests/behat/matrix_form_fields.feature index 78ea9fdbedc52..dd2145c1da67b 100644 --- a/communication/provider/matrix/tests/behat/matrix_form_fields.feature +++ b/communication/provider/matrix/tests/behat/matrix_form_fields.feature @@ -1,4 +1,4 @@ -@communication @communication_matrix @javascript +@communication @communication_matrix Feature: Communication matrix form field In order to create a new communication room in matrix As a teacher @@ -15,34 +15,20 @@ Feature: Communication matrix form field | user | course | role | | teacher1 | Test course | editingteacher | - Scenario: I can add room name for matrix room + @javascript + Scenario: I can add room name and topic for matrix room Given a Matrix mock server is configured - And I log in as "teacher1" - And I am on "Test course" course homepage + 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 field "id_selectedcommunication" to "Matrix" - And I wait to be redirected - And I should see "Room name" - And I set the field "id_communicationroomname" to "Sampleroomname" - And I press "Save changes" - And I navigate to "Communication" in current page administration - Then the field "id_communicationroomname" matches value "Sampleroomname" - - Scenario: I can add room topic for matrix room - Given a Matrix mock server is configured - And I log in as "teacher1" - And I am on "Test course" course homepage - When I navigate to "Communication" in current page administration - And I set the field "id_selectedcommunication" to "Matrix" + And I set the following fields to these values: + | selectedcommunication | communication_matrix | And I wait to be redirected + And I set the following fields to these values: + | communication_matrixroomname | Sampleroomname | + | matrixroomtopic | Sampleroomtopic | And I should see "Room name" And I should see "Room topic" - And I set the field "id_communicationroomname" to "Sampleroomname" - And I set the field "id_matrixroomtopic" to "Sampleroomtopic" And I press "Save changes" And I navigate to "Communication" in current page administration - Then the field "id_communicationroomname" matches value "Sampleroomname" - And I press "Cancel" - And I run all adhoc tasks - And I navigate to "Communication" in current page administration - And the field "id_matrixroomtopic" matches value "Sampleroomtopic" + Then the field "Room name" matches value "Sampleroomname" + And the field "Room topic" matches value "Sampleroomtopic" diff --git a/communication/tests/api_test.php b/communication/tests/api_test.php index bb1eb99d25e81..b9b30bb73b3af 100644 --- a/communication/tests/api_test.php +++ b/communication/tests/api_test.php @@ -69,8 +69,10 @@ public function test_set_data(): void { // Set the data. $communication->set_data($course); + $roomnameidenfier = $communication->get_provider() . 'roomname'; + // Test the set data. - $this->assertEquals($roomname, $course->communicationroomname); + $this->assertEquals($roomname, $course->$roomnameidenfier); $this->assertEquals($provider, $course->selectedcommunication); } @@ -476,6 +478,8 @@ public function test_configure_room_and_membership_by_provider(): void { // Now delete all the ad-hoc tasks. $DB->delete_records('task_adhoc'); + $course->customlinkurl = $course->customlinkurl ?? 'https://moodle.org'; + // Now change the provider to another one. $communication->configure_room_and_membership_by_provider( provider: 'communication_customlink', diff --git a/communication/tests/behat/communication_configuration.feature b/communication/tests/behat/communication_configuration.feature index 77aa1359ff084..7c001a090f653 100644 --- a/communication/tests/behat/communication_configuration.feature +++ b/communication/tests/behat/communication_configuration.feature @@ -38,5 +38,72 @@ Feature: Access the communication configuration page 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 Then I should see "Room name" And I should see "Room topic" + + @javascript + Scenario: Changing the communication provider in the form fetches the correct data + 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: + | selectedcommunication | communication_customlink | + And I wait to be redirected + And I should see "Room name" + And I should not see "Room topic" + And I should see "Custom link URL" + And I set the following fields to these values: + | communication_customlinkroomname | Custom link room | + | customlinkurl | https://moodle.org | + And I click on "Save changes" "button" + And I navigate to "Communication" in current page administration + And the field "Room name" matches value "Custom link room" + And the field "Custom link URL" matches value "https://moodle.org" + 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 the field "Room name" matches value "Matrix room" + And the field "Room topic" matches value "Matrix topic" + And I should not see "Custom link URL" + And I set the following fields to these values: + | selectedcommunication | communication_customlink | + And I wait to be redirected + And I should see "Room name" + And I should see "Custom link URL" + And the field "Room name" matches value "Custom link room" + And the field "Custom link URL" matches value "https://moodle.org" + And I should not see "Room topic" + And I set the following fields to these values: + | selectedcommunication | communication_matrix | + And I wait to be redirected + And I click on "Save changes" "button" + And I am on "Test course" course homepage with editing mode on + And I navigate to "Settings" in current page administration + And I set the following fields to these values: + | Group mode | Separate groups | + And I press "Save and display" + 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" + And I press "Cancel" + And I navigate to "Settings" in current page administration + And I set the following fields to these values: + | Group mode | Visible groups | + 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" diff --git a/completion/classes/form/form_trait.php b/completion/classes/form/form_trait.php index 1ba0e59b6b73f..2c4bccc5813bc 100644 --- a/completion/classes/form/form_trait.php +++ b/completion/classes/form/form_trait.php @@ -484,9 +484,20 @@ protected function definition_after_data_completion(?cm_info $cm = null): void { $mform->freeze($completionpassgradeel); // Has the completion pass grade completion criteria been set? If it has, then we shouldn't change - // the gradepass field. + // any of the modules "gradepass" type fields. if ($mform->exportValue($completionpassgradeel)) { - $mform->freeze('gradepass'); + + // Some modules define separate "gradepass" fields for each of their grade items. + $gradepassfieldels = array_merge(['gradepass'], array_map( + fn(string $gradeitem) => "{$gradeitem}gradepass", + component_gradeitems::get_itemname_mapping_for_component("mod_{$this->_modname}"), + )); + + foreach ($gradepassfieldels as $gradepassfieldel) { + if ($mform->elementExists($gradepassfieldel)) { + $mform->freeze($gradepassfieldel); + } + } } } $completiongradeitemnumberel = 'completiongradeitemnumber' . $suffix; diff --git a/completion/tests/behat/activity_completion_criteria.feature b/completion/tests/behat/activity_completion_criteria.feature index 1679166a4d593..24d085ff6e577 100644 --- a/completion/tests/behat/activity_completion_criteria.feature +++ b/completion/tests/behat/activity_completion_criteria.feature @@ -112,7 +112,7 @@ Feature: Allow to mark course as completed without cron for activity completion Given I am on the "Completion course" "grades > Single View > View" page logged in as "teacher1" And I click on "Users" "link" in the ".page-toggler" "css_element" And I turn editing mode on - And I click on "Student First" in the "user" search widget + And I click on "Student First" in the "Search users" search combo box And I set the field "Override for Test assignment name" to "1" When I set the following fields to these values: | Grade for Test assignment name | 10.00 | diff --git a/config-dist.php b/config-dist.php index ec2708cde8077..8e7de4445d9f6 100644 --- a/config-dist.php +++ b/config-dist.php @@ -773,6 +773,15 @@ // Defaults to 60 minutes. // // $CFG->enrolments_sync_interval = 3600 +// +// Stored progress polling interval +// +// Stored progress bars which can be polled for updates via AJAX can be controlled by the +// `progresspollinterval` config setting, to determine the interval (in seconds) at which the +// polling should be done and latest update retrieved. +// If no value is set, then it will default to 5 seconds. +// +// $CFG->progresspollinterval = 5; //========================================================================= // 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!! diff --git a/contentbank/tests/behat/delete_content.feature b/contentbank/tests/behat/delete_content.feature index 5a2a801c697ca..e95825d644932 100644 --- a/contentbank/tests/behat/delete_content.feature +++ b/contentbank/tests/behat/delete_content.feature @@ -12,17 +12,7 @@ Feature: Delete H5P file from the content bank And I follow "Manage private files..." And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Files" filemanager And I click on "Save changes" "button" - And I am on site homepage - And I turn editing mode on - And the following config values are set as admin: - | unaddableblocks | | theme_boost| - And I add the "Navigation" block if not present - And I configure the "Navigation" block - And I set the following fields to these values: - | Page contexts | Display throughout the entire site | - And I press "Save changes" - And I click on "Site pages" "list_item" in the "Navigation" "block" - And I click on "Content bank" "link" in the "Navigation" "block" + And I am on the "Content bank" page And I click on "Upload" "link" And I click on "Choose a file..." "button" And I click on "Private files" "link" in the ".fp-repo-area" "css_element" @@ -32,14 +22,16 @@ Feature: Delete H5P file from the content bank And I click on "Save changes" "button" Scenario: Admins can delete content from the content bank - Given I click on "More" "button" + Given I wait "2" seconds + And I click on "More" "button" And I should see "Delete" - And I click on "Delete" "link" in the ".cb-toolbar-container" "css_element" + And I click on "Delete" "link" And I should see "Are you sure you want to delete the content 'content2delete.h5p'" And I should not see "The content will only be deleted from the content bank" And I click on "Cancel" "button" in the "Delete content" "dialogue" Then I should see "content2delete.h5p" - And I click on "More" "button" + And I wait "2" seconds + And I click on "More" "button" And I click on "Delete" "link" in the ".cb-toolbar-container" "css_element" And I click on "Delete" "button" in the "Delete content" "dialogue" And I wait until the page is ready @@ -61,20 +53,21 @@ Feature: Delete H5P file from the content bank And I follow "Manage private files..." And I upload "h5p/tests/fixtures/find-the-words.h5p" file to "Files" filemanager And I click on "Save changes" "button" - When I click on "Site pages" "list_item" in the "Navigation" "block" - And I click on "Content bank" "link" in the "Navigation" "block" + When I am on the "Content bank" page And I should see "content2delete.h5p" And I follow "content2delete.h5p" + And I wait "2" seconds And I click on "More" "button" Then I should not see "Delete" - And I click on "Content bank" "link" + And I am on the "Content bank" page And I click on "Upload" "link" And I click on "Choose a file..." "button" And I click on "Private files" "link" in the ".fp-repo-area" "css_element" And I click on "find-the-words.h5p" "link" And I click on "Select this file" "button" And I click on "Save changes" "button" - And I click on "More" "button" + And I wait "2" seconds + And I click on "More" "button" And I should see "Delete" Scenario: The number of times a content is used is displayed before removing it @@ -86,10 +79,10 @@ Feature: Delete H5P file from the content bank And I click on "Link to the file" "radio" And I click on "Select this file" "button" And I click on "Save changes" "button" - When I click on "Site pages" "list_item" in the "Navigation" "block" - And I click on "Content bank" "link" in the "Navigation" "block" + And I am on the "Content bank" page And I follow "content2delete.h5p" - And I click on "More" "button" + And I wait "2" seconds + And I click on "More" "button" And I click on "Delete" "link" in the ".cb-toolbar-container" "css_element" Then I should see "Are you sure you want to delete the content 'content2delete.h5p'" And I should see "The content will only be deleted from the content bank" diff --git a/course/UPGRADING.md b/course/UPGRADING.md index 5ab375fcf87e9..c46b8870931e7 100644 --- a/course/UPGRADING.md +++ b/course/UPGRADING.md @@ -7,6 +7,9 @@ - - New optional sectionNum parameter has been added to activitychooser AMD module initializer. - New option sectionnum parameter has been added to get_course_content_items() external function. - New optional sectionnum parameter has been added to get_content_items_for_user_in_course() function. For more information see [MDL-81675](https://tracker.moodle.org/browse/MDL-81675) +- Webservices `core_course_get_courses_by_field` now accepts a new parameter `sectionid` to be able to retrieve the course that has the indicated section + + For more information see [MDL-81699](https://tracker.moodle.org/browse/MDL-81699) ### Deprecated diff --git a/course/amd/build/actions.min.js b/course/amd/build/actions.min.js index c25032540a337..b6a2862d863dc 100644 --- a/course/amd/build/actions.min.js +++ b/course/amd/build/actions.min.js @@ -6,6 +6,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.3 */ -define("core_course/actions",["jquery","core/ajax","core/templates","core/notification","core/str","core/url","core/yui","core/modal_copy_to_clipboard","core/modal_save_cancel","core/modal_events","core/key_codes","core/log","core_courseformat/courseeditor","core/event_dispatcher","core_course/events"],(function($,ajax,templates,notification,str,url,Y,ModalCopyToClipboard,ModalSaveCancel,ModalEvents,KeyCodes,log,editor,EventDispatcher,CourseEvents){const componentActions=["moveSection","moveCm","addSection","deleteSection","cmDelete","cmDuplicate","sectionHide","sectionShow","cmHide","cmShow","cmStealth","sectionHighlight","sectionUnhighlight","cmMoveRight","cmMoveLeft","cmNoGroups","cmVisibleGroups","cmSeparateGroups"],courseeditor=editor.getCurrentCourseEditor();let formatname;var CSS_EDITINPROGRESS="editinprogress",CSS_EDITINGMOVE="editing_move",SELECTOR={ACTIVITYLI:"li.activity",ACTIONAREA:".actions",ACTIVITYACTION:"a.cm-edit-action",MENU:".moodle-actionmenu[data-enhance=moodle-core-actionmenu]",TOGGLE:".toggle-display,.dropdown-toggle",SECTIONLI:"li.section",SECTIONACTIONMENU:".section_action_menu",SECTIONACTIONMENUTRIGGER:".section-actions",SECTIONITEM:'[data-for="section_title"]',ADDSECTIONS:".changenumsections [data-add-sections]",SECTIONBADGES:'[data-region="sectionbadges"]'};Y.use("moodle-course-coursebase",(function(){var courseformatselector=M.course.format.get_section_selector();courseformatselector&&(SELECTOR.SECTIONLI=courseformatselector)}));const dispatchEvent=function(eventName,detail,container,options){return container instanceof Element||void 0===container.get||(container=container.get(0)),EventDispatcher.dispatchEvent(eventName,detail,container,options)};var getModuleId=function(element){const item=element.get(0);if(item.dataset.id)return item.dataset.id;let id;return Y.use("moodle-course-util",(function(Y){id=Y.Moodle.core_course.util.cm.getId(Y.Node(item))})),id},addActivitySpinner=function(activity){activity.addClass(CSS_EDITINPROGRESS);var actionarea=activity.find(SELECTOR.ACTIONAREA).get(0);if(actionarea){var spinner=M.util.add_spinner(Y,Y.Node(actionarea));return spinner.show(),void 0!==activity.data("id")&&courseeditor.dispatch("cmLock",[activity.data("id")],!0),spinner}return null},addSectionSpinner=function(sectionelement){sectionelement.addClass(CSS_EDITINPROGRESS);var actionarea=sectionelement.find(SELECTOR.SECTIONACTIONMENU).get(0);if(actionarea){var spinner=M.util.add_spinner(Y,Y.Node(actionarea));return spinner.show(),void 0!==sectionelement.data("id")&&courseeditor.dispatch("sectionLock",[sectionelement.data("id")],!0),spinner}return null},addSectionLightbox=function(sectionelement){const item=sectionelement.get(0);var lightbox=M.util.add_lightbox(Y,Y.Node(item));return"section"==item.dataset.for&&item.dataset.id&&(courseeditor.dispatch("sectionLock",[item.dataset.id],!0),lightbox.setAttribute("data-state","section"),lightbox.setAttribute("data-state-id",item.dataset.id)),lightbox.show(),lightbox},removeSpinner=function(element,spinner,delay){window.setTimeout((function(){if(element.removeClass(CSS_EDITINPROGRESS),spinner&&spinner.hide(),void 0!==element.data("id")){const mutation="section"===element.data("for")?"sectionLock":"cmLock";courseeditor.dispatch(mutation,[element.data("id")],!1)}}),delay)},removeLightbox=function(lightbox,delay){lightbox&&window.setTimeout((function(){lightbox.hide(),lightbox.getAttribute("data-state")&&courseeditor.dispatch("".concat(lightbox.getAttribute("data-state"),"Lock"),[lightbox.getAttribute("data-state-id")],!1)}),delay)},initActionMenu=function(elementid){Y.use("moodle-course-coursebase",(function(){M.course.coursebase.invoke_function("setup_for_resource","#"+elementid)})),M.core.actionmenu&&M.core.actionmenu.newDOMNode&&M.core.actionmenu.newDOMNode(Y.one("#"+elementid))},editModule=function(moduleElement,cmid,target){var lightbox,action=target.attr("data-action"),spinner=addActivitySpinner(moduleElement),promises=ajax.call([{methodname:"core_course_edit_module",args:{id:cmid,action:action,sectionreturn:target.attr("data-sectionreturn")?target.attr("data-sectionreturn"):null}}],!0);"duplicate"===action&&(lightbox=addSectionLightbox(target.closest(SELECTOR.SECTIONLI))),$.when.apply($,promises).done((function(data){var mainElement,tabables,isInside,foundElement,elementToFocus=(mainElement=moduleElement,tabables=$("a:visible"),isInside=!1,foundElement=null,tabables.each((function(){if($.contains(mainElement[0],this))isInside=!0;else if(isInside)return foundElement=this,!1;return!0})),foundElement);moduleElement.replaceWith(data);let affectedids=[];$("
    "+data+"
    ").find(SELECTOR.ACTIVITYLI).each((function(index){initActionMenu($(this).attr("id")),0===index&&(!function(elementId,action){var mainelement=$("#"+elementId),selector="[data-action="+action+"]";"groupsseparate"!==action&&"groupsvisible"!==action&&"groupsnone"!==action||(selector="[data-action=groupsseparate],[data-action=groupsvisible],[data-action=groupsnone]"),mainelement.find(selector).is(":visible")?mainelement.find(selector).focus():mainelement.find(SELECTOR.MENU).find(SELECTOR.TOGGLE).focus()}($(this).attr("id"),action),elementToFocus=null),affectedids.push(getModuleId($(this)))})),elementToFocus&&elementToFocus.focus(),removeSpinner(moduleElement,spinner,400),removeLightbox(lightbox,400),moduleElement.trigger($.Event("coursemoduleedited",{ajaxreturn:data,action:action})),courseeditor.dispatch("legacyActivityAction",action,cmid,affectedids)})).fail((function(ex){removeSpinner(moduleElement,spinner),removeLightbox(lightbox);var e=$.Event("coursemoduleeditfailed",{exception:ex,action:action});moduleElement.trigger(e),e.isDefaultPrevented()||notification.exception(ex)}))},refreshModule=function(element,cmid,sectionreturn){void 0===sectionreturn&&(sectionreturn=courseeditor.sectionReturn);const activityElement=$(element);var spinner=addActivitySpinner(activityElement),promises=ajax.call([{methodname:"core_course_get_module",args:{id:cmid,sectionreturn:sectionreturn}}],!0);return new Promise(((resolve,reject)=>{$.when.apply($,promises).done((function(data){removeSpinner(activityElement,spinner,400),replaceActivityHtmlWith(data),resolve(data)})).fail((function(){removeSpinner(activityElement,spinner),reject()}))}))},confirmDeleteModule=function(mainelement,onconfirm){var modtypename=mainelement.attr("class").match(/modtype_([^\s]*)/)[1],modulename=function(element){var name;Y.use("moodle-course-util",(function(Y){name=Y.Moodle.core_course.util.cm.getName(Y.Node(element.get(0)))}));const state=courseeditor.state,cmid=getModuleId(element);var _state$cm$get;return!name&&state&&cmid&&(name=null===(_state$cm$get=state.cm.get(cmid))||void 0===_state$cm$get?void 0:_state$cm$get.name),name}(mainelement);str.get_string("pluginname",modtypename).done((function(pluginname){var plugindata={type:pluginname,name:modulename};str.get_strings([{key:"confirm",component:"core"},{key:null===modulename?"deletechecktype":"deletechecktypename",param:plugindata},{key:"yes"},{key:"no"}]).done((function(s){notification.confirm(s[0],s[1],s[2],s[3],onconfirm)}))}))},replaceActionItem=function(actionitem,image,stringname,stringcomponent,newaction){var stringRequests=[{key:stringname,component:stringcomponent}];return str.get_strings(stringRequests).then((function(strings){return actionitem.find("span.menu-action-text").html(strings[0]),templates.renderPix(image,"core")})).then((function(pixhtml){actionitem.find(".icon").replaceWith(pixhtml),actionitem.attr("data-action",newaction)})).catch(notification.exception)},defaultEditSectionHandler=function(sectionElement,actionItem,data,courseformat,sectionid){var action=actionItem.attr("data-action");if("hide"===action||"show"===action){if("hide"===action?(sectionElement.addClass("hidden"),setSectionBadge(sectionElement[0],"hiddenfromstudents",!0,!1),replaceActionItem(actionItem,"i/show","showfromothers","format_"+courseformat,"show")):(setSectionBadge(sectionElement[0],"hiddenfromstudents",!1,!1),sectionElement.removeClass("hidden"),replaceActionItem(actionItem,"i/hide","hidefromothers","format_"+courseformat,"hide")),void 0!==data.modules)for(var i in data.modules)replaceActivityHtmlWith(data.modules[i]);void 0!==data.section_availability&§ionElement.find(".section_availability").first().replaceWith(data.section_availability);void 0!==courseeditor.state.section.get(sectionid)&&courseeditor.dispatch("sectionState",[sectionid])}else if("setmarker"===action){var oldmarker=$(SELECTOR.SECTIONLI+".current"),oldActionItem=oldmarker.find(SELECTOR.SECTIONACTIONMENU+" a[data-action=removemarker]");oldmarker.removeClass("current"),replaceActionItem(oldActionItem,"i/marker","highlight","core","setmarker"),sectionElement.addClass("current"),replaceActionItem(actionItem,"i/marked","highlightoff","core","removemarker"),courseeditor.dispatch("legacySectionAction",action,sectionid),setSectionBadge(sectionElement[0],"iscurrent",!0,!0)}else"removemarker"===action&&(sectionElement.removeClass("current"),replaceActionItem(actionItem,"i/marker","highlight","core","setmarker"),courseeditor.dispatch("legacySectionAction",action,sectionid),setSectionBadge(sectionElement[0],"iscurrent",!1,!0))};var replaceActivityHtmlWith=function(activityHTML){$("
    "+activityHTML+"
    ").find(SELECTOR.ACTIVITYLI).each((function(){var id=$(this).attr("id");let focusedPath=function(id){const element=document.getElementById(id);if(element&&element.contains(document.activeElement))return element.querySelector(SELECTOR.ACTIONAREA).contains(document.activeElement)?"".concat(SELECTOR.ACTIONAREA,' [tabindex="0"]'):document.activeElement.id?"#".concat(document.activeElement.id):void 0}(id);if($(SELECTOR.ACTIVITYLI+"#"+id).replaceWith(activityHTML),initActionMenu(id),focusedPath){var _newItem$querySelecto;null===(_newItem$querySelecto=document.getElementById(id).querySelector(focusedPath))||void 0===_newItem$querySelecto||_newItem$querySelecto.focus()}}))},editSection=function(sectionElement,sectionid,target,courseformat){var action=target.attr("data-action"),sectionreturn=target.attr("data-sectionreturn")?target.attr("data-sectionreturn"):null;if(courseeditor.supportComponents&&componentActions.includes(action))return!1;var spinner=addSectionSpinner(sectionElement),promises=ajax.call([{methodname:"core_course_edit_section",args:{id:sectionid,action:action,sectionreturn:sectionreturn}}],!0),lightbox=addSectionLightbox(sectionElement);return $.when.apply($,promises).done((function(dataencoded){var data=$.parseJSON(dataencoded);removeSpinner(sectionElement,spinner),removeLightbox(lightbox),sectionElement.find(SELECTOR.SECTIONACTIONMENU).find(SELECTOR.TOGGLE).focus();var e=$.Event("coursesectionedited",{ajaxreturn:data,action:action});sectionElement.trigger(e),e.isDefaultPrevented()||defaultEditSectionHandler(sectionElement,target,data,courseformat,sectionid)})).fail((function(ex){removeSpinner(sectionElement,spinner),removeLightbox(lightbox);var e=$.Event("coursesectioneditfailed",{exception:ex,action:action});sectionElement.trigger(e),e.isDefaultPrevented()||notification.exception(ex)})),!0},setSectionBadge=function(sectionElement,badgetype,add,removeOther){const sectionbadges=sectionElement.querySelector(SELECTOR.SECTIONBADGES);if(!sectionbadges)return;const badge=sectionbadges.querySelector('[data-type="'+badgetype+'"]');badge&&(add?(removeOther&&document.querySelectorAll('[data-type="'+badgetype+'"]').forEach((b=>{b.classList.add("d-none")})),badge.classList.remove("d-none")):badge.classList.add("d-none"))};return Y.use("moodle-course-coursebase",(function(){M.course.coursebase.register_module({set_visibility_resource_ui:function(args){var mainelement=$(args.element.getDOMNode()),cmid=getModuleId(mainelement);if(cmid){var sectionreturn=mainelement.find("."+CSS_EDITINGMOVE).attr("data-sectionreturn");refreshModule(mainelement,cmid,sectionreturn)}},updateMovedCmState:params=>{const cm=courseeditor.state.cm.get(params.cmid);void 0!==cm&&courseeditor.dispatch("sectionState",[cm.sectionid]),courseeditor.dispatch("cmState",[params.cmid])},updateMovedSectionState:()=>{courseeditor.dispatch("courseState")}})})),courseeditor.addMutations({legacyActivityAction:function(statemanager,action,cmid,affectedids){const state=statemanager.state,cm=state.cm.get(cmid);if(void 0===cm)return;const section=state.section.get(cm.sectionid);if(void 0!==section){switch(courseeditor.dispatch("cmLock",[cm.id],!0),statemanager.setReadOnly(!1),cm.locked=!1,action){case"delete":section.cmlist=section.cmlist.reduce(((cmlist,current)=>(current!=cmid&&cmlist.push(current),cmlist)),[]),state.cm.delete(cmid);break;case"hide":case"show":case"duplicate":courseeditor.dispatch("cmState",affectedids)}statemanager.setReadOnly(!0)}},legacySectionAction:function(statemanager,action,sectionid){const state=statemanager.state,section=state.section.get(sectionid);if(void 0!==section){switch(statemanager.setReadOnly(!1),section.locked=!0,statemanager.setReadOnly(!0),statemanager.setReadOnly(!1),section.locked=!1,action){case"setmarker":state.section.forEach((current=>{current.id!=sectionid&&(current.current=!1)})),section.current=!0;break;case"removemarker":section.current=!1}statemanager.setReadOnly(!0)}}}),{initCoursePage:function(courseformat){if(formatname=courseformat,$("body").on("click keypress",SELECTOR.ACTIVITYLI+" "+SELECTOR.ACTIVITYACTION+"[data-action]",(function(e){if("keypress"!==e.type||13===e.keyCode){var actionItem=$(this),moduleElement=actionItem.closest(SELECTOR.ACTIVITYLI),action=actionItem.attr("data-action"),moduleId=getModuleId(moduleElement);switch(action){case"moveleft":case"moveright":case"delete":case"duplicate":case"hide":case"stealth":case"show":case"groupsseparate":case"groupsvisible":case"groupsnone":break;default:return}moduleId&&(e.preventDefault(),"delete"===action?confirmDeleteModule(moduleElement,(function(){editModule(moduleElement,moduleId,actionItem)})):editModule(moduleElement,moduleId,actionItem))}})),$("body").on("click keypress",SELECTOR.SECTIONACTIONMENUTRIGGER+"[data-sectionid] a[data-action]",(function(e){if("keypress"===e.type&&13!==e.keyCode)return;var actionItem=$(this),sectionElement=actionItem.closest(SELECTOR.SECTIONLI),sectionId=actionItem.closest(SELECTOR.SECTIONACTIONMENUTRIGGER).attr("data-sectionid");if("permalink"===actionItem.attr("data-action"))return e.preventDefault(),void ModalCopyToClipboard.create({text:actionItem.attr("href")},str.get_string("sectionlink","course"));let isExecuted=!0;var message,onconfirm;actionItem.attr("data-confirm")?(message=actionItem.attr("data-confirm"),onconfirm=function(){isExecuted=editSection(sectionElement,sectionId,actionItem,courseformat)},str.get_strings([{key:"confirm"},{key:"yes"},{key:"no"}]).done((function(s){notification.confirm(s[0],message,s[1],s[2],onconfirm)}))):isExecuted=editSection(sectionElement,sectionId,actionItem,courseformat),isExecuted&&e.preventDefault()})),$("body").on("updated","".concat(SELECTOR.SECTIONITEM," [data-inplaceeditable]"),(function(e){if(e.ajaxreturn&&e.ajaxreturn.itemid){void 0!==courseeditor.state.section.get(e.ajaxreturn.itemid)&&courseeditor.dispatch("sectionState",[e.ajaxreturn.itemid])}})),$("body").on("updated","".concat(SELECTOR.ACTIVITYLI," [data-inplaceeditable]"),(function(e){e.ajaxreturn&&e.ajaxreturn.itemid&&courseeditor.dispatch("cmState",[e.ajaxreturn.itemid])})),courseeditor.supportComponents&&componentActions.includes("addSection"))return;const trigger=$(SELECTOR.ADDSECTIONS),modalTitle=trigger.attr("data-add-sections"),newSections=trigger.attr("data-new-sections");str.get_string("numberweeks").then((function(strNumberSections){var modalBody=$('
    ');return modalBody.find("label").html(strNumberSections),modalBody.html()})).then((body=>ModalSaveCancel.create({body:body,title:modalTitle}))).then((function(modal){var numSections=$(modal.getBody()).find("#add_section_numsections"),addSections=function(){""+parseInt(numSections.val())===numSections.val()&&parseInt(numSections.val())>=1&&(document.location=trigger.attr("href")+"&numsections="+parseInt(numSections.val()))};return modal.setSaveButtonText(modalTitle),modal.getRoot().on(ModalEvents.shown,(function(){numSections.focus().select().on("keydown",(function(e){e.keyCode===KeyCodes.enter&&addSections()}))})),modal.getRoot().on(ModalEvents.save,(function(e){e.preventDefault(),addSections()})),trigger.on("click",(e=>{e.preventDefault(),modal.show()})),modal})).catch(notification.exception)},replaceSectionActionItem:function(sectionelement,selector,image,stringname,stringcomponent,newaction){log.debug("replaceSectionActionItem() is deprecated and will be removed.");var actionitem=sectionelement.find(SELECTOR.SECTIONACTIONMENU+" "+selector);replaceActionItem(actionitem,image,stringname,stringcomponent,newaction)},refreshModule:refreshModule,refreshSection:function(element,sectionid,sectionreturn){void 0===sectionreturn&&(sectionreturn=courseeditor.sectionReturn);const sectionElement=$(element),promises=ajax.call([{methodname:"core_course_edit_section",args:{id:sectionid,action:"refresh",sectionreturn:sectionreturn}}],!0);var spinner=addSectionSpinner(sectionElement);return new Promise(((resolve,reject)=>{$.when.apply($,promises).done((dataencoded=>{removeSpinner(sectionElement,spinner);const data=$.parseJSON(dataencoded),newSectionElement=$(data.content);sectionElement.replaceWith(newSectionElement),$("".concat(SELECTOR.SECTIONLI,"#").concat(sectionid," ").concat(SELECTOR.ACTIVITYLI)).each(((index,activity)=>{initActionMenu(activity.data("id"))}));dispatchEvent(CourseEvents.sectionRefreshed,{ajaxreturn:data,action:"refresh",newSectionElement:newSectionElement.get(0)},newSectionElement).defaultPrevented||defaultEditSectionHandler(newSectionElement,$(SELECTOR.SECTIONLI+"#"+sectionid),data,formatname,sectionid),resolve(data)})).fail((ex=>{dispatchEvent("coursesectionrefreshfailed",{exception:ex,action:"refresh"},sectionElement).defaultPrevented||notification.exception(ex),reject()}))}))}}})); +define("core_course/actions",["jquery","core/ajax","core/templates","core/notification","core/str","core/url","core/yui","core/modal_copy_to_clipboard","core/modal_save_cancel","core/modal_events","core/key_codes","core/log","core_courseformat/courseeditor","core/event_dispatcher","core_course/events"],(function($,ajax,templates,notification,str,url,Y,ModalCopyToClipboard,ModalSaveCancel,ModalEvents,KeyCodes,log,editor,EventDispatcher,CourseEvents){const componentActions=["moveSection","moveCm","addSection","deleteSection","cmDelete","cmDuplicate","sectionHide","sectionShow","cmHide","cmShow","cmStealth","sectionHighlight","sectionUnhighlight","cmMoveRight","cmMoveLeft","cmNoGroups","cmVisibleGroups","cmSeparateGroups"],courseeditor=editor.getCurrentCourseEditor();let formatname;var CSS_EDITINPROGRESS="editinprogress",CSS_EDITINGMOVE="editing_move",SELECTOR={ACTIVITYLI:"li.activity",ACTIONAREA:".actions",ACTIVITYACTION:"a.cm-edit-action",MENU:".moodle-actionmenu[data-enhance=moodle-core-actionmenu]",TOGGLE:".toggle-display,.dropdown-toggle",SECTIONLI:"li.section",SECTIONACTIONMENU:".section_action_menu",SECTIONACTIONMENUTRIGGER:".section-actions",SECTIONITEM:'[data-for="section_title"]',ADDSECTIONS:".changenumsections [data-add-sections]",SECTIONBADGES:'[data-region="sectionbadges"]'};Y.use("moodle-course-coursebase",(function(){var courseformatselector=M.course.format.get_section_selector();courseformatselector&&(SELECTOR.SECTIONLI=courseformatselector)}));const dispatchEvent=function(eventName,detail,container,options){return container instanceof Element||void 0===container.get||(container=container.get(0)),EventDispatcher.dispatchEvent(eventName,detail,container,options)};var getModuleId=function(element){const item=element.get(0);if(item.dataset.id)return item.dataset.id;let id;return Y.use("moodle-course-util",(function(Y){id=Y.Moodle.core_course.util.cm.getId(Y.Node(item))})),id},addActivitySpinner=function(activity){activity.addClass(CSS_EDITINPROGRESS);var actionarea=activity.find(SELECTOR.ACTIONAREA).get(0);if(actionarea){var spinner=M.util.add_spinner(Y,Y.Node(actionarea));return spinner.show(),void 0!==activity.data("id")&&courseeditor.dispatch("cmLock",[activity.data("id")],!0),spinner}return null},addSectionSpinner=function(sectionelement){sectionelement.addClass(CSS_EDITINPROGRESS);var actionarea=sectionelement.find(SELECTOR.SECTIONACTIONMENU).get(0);if(actionarea){var spinner=M.util.add_spinner(Y,Y.Node(actionarea));return spinner.show(),void 0!==sectionelement.data("id")&&courseeditor.dispatch("sectionLock",[sectionelement.data("id")],!0),spinner}return null},addSectionLightbox=function(sectionelement){const item=sectionelement.get(0);var lightbox=M.util.add_lightbox(Y,Y.Node(item));return"section"==item.dataset.for&&item.dataset.id&&(courseeditor.dispatch("sectionLock",[item.dataset.id],!0),lightbox.setAttribute("data-state","section"),lightbox.setAttribute("data-state-id",item.dataset.id)),lightbox.show(),lightbox},removeSpinner=function(element,spinner,delay){window.setTimeout((function(){if(element.removeClass(CSS_EDITINPROGRESS),spinner&&spinner.hide(),void 0!==element.data("id")){const mutation="section"===element.data("for")?"sectionLock":"cmLock";courseeditor.dispatch(mutation,[element.data("id")],!1)}}),delay)},removeLightbox=function(lightbox,delay){lightbox&&window.setTimeout((function(){lightbox.hide(),lightbox.getAttribute("data-state")&&courseeditor.dispatch("".concat(lightbox.getAttribute("data-state"),"Lock"),[lightbox.getAttribute("data-state-id")],!1)}),delay)},initActionMenu=function(elementid){Y.use("moodle-course-coursebase",(function(){M.course.coursebase.invoke_function("setup_for_resource","#"+elementid)})),M.core.actionmenu&&M.core.actionmenu.newDOMNode&&M.core.actionmenu.newDOMNode(Y.one("#"+elementid))},editModule=function(moduleElement,cmid,target){var lightbox,action=target.attr("data-action"),spinner=addActivitySpinner(moduleElement),promises=ajax.call([{methodname:"core_course_edit_module",args:{id:cmid,action:action,sectionreturn:target.attr("data-sectionreturn")?target.attr("data-sectionreturn"):null}}],!0);"duplicate"===action&&(lightbox=addSectionLightbox(target.closest(SELECTOR.SECTIONLI))),$.when.apply($,promises).done((function(data){var mainElement,tabables,isInside,foundElement,elementToFocus=(mainElement=moduleElement,tabables=$("a:visible"),isInside=!1,foundElement=null,tabables.each((function(){if($.contains(mainElement[0],this))isInside=!0;else if(isInside)return foundElement=this,!1;return!0})),foundElement);moduleElement.replaceWith(data);let affectedids=[];$("
    "+data+"
    ").find(SELECTOR.ACTIVITYLI).each((function(index){initActionMenu($(this).attr("id")),0===index&&(!function(elementId,action){var mainelement=$("#"+elementId),selector="[data-action="+action+"]";"groupsseparate"!==action&&"groupsvisible"!==action&&"groupsnone"!==action||(selector="[data-action=groupsseparate],[data-action=groupsvisible],[data-action=groupsnone]"),mainelement.find(selector).is(":visible")?mainelement.find(selector).focus():mainelement.find(SELECTOR.MENU).find(SELECTOR.TOGGLE).focus()}($(this).attr("id"),action),elementToFocus=null),affectedids.push(getModuleId($(this)))})),elementToFocus&&elementToFocus.focus(),removeSpinner(moduleElement,spinner,400),removeLightbox(lightbox,400),moduleElement.trigger($.Event("coursemoduleedited",{ajaxreturn:data,action:action})),courseeditor.dispatch("legacyActivityAction",action,cmid,affectedids)})).fail((function(ex){removeSpinner(moduleElement,spinner),removeLightbox(lightbox);var e=$.Event("coursemoduleeditfailed",{exception:ex,action:action});moduleElement.trigger(e),e.isDefaultPrevented()||notification.exception(ex)}))},refreshModule=function(element,cmid,sectionreturn){void 0===sectionreturn&&(sectionreturn=courseeditor.sectionReturn);const activityElement=$(element);var spinner=addActivitySpinner(activityElement),promises=ajax.call([{methodname:"core_course_get_module",args:{id:cmid,sectionreturn:sectionreturn}}],!0);return new Promise(((resolve,reject)=>{$.when.apply($,promises).done((function(data){removeSpinner(activityElement,spinner,400),replaceActivityHtmlWith(data),resolve(data)})).fail((function(){removeSpinner(activityElement,spinner),reject()}))}))},confirmDeleteModule=function(mainelement,onconfirm){var modtypename=mainelement.attr("class").match(/modtype_([^\s]*)/)[1],modulename=function(element){var name;Y.use("moodle-course-util",(function(Y){name=Y.Moodle.core_course.util.cm.getName(Y.Node(element.get(0)))}));const state=courseeditor.state,cmid=getModuleId(element);var _state$cm$get;return!name&&state&&cmid&&(name=null===(_state$cm$get=state.cm.get(cmid))||void 0===_state$cm$get?void 0:_state$cm$get.name),name}(mainelement);str.get_string("pluginname",modtypename).done((function(pluginname){var plugindata={type:pluginname,name:modulename};str.get_strings([{key:"confirm",component:"core"},{key:null===modulename?"deletechecktype":"deletechecktypename",param:plugindata},{key:"yes"},{key:"no"}]).done((function(s){notification.confirm(s[0],s[1],s[2],s[3],onconfirm)}))}))},replaceActionItem=function(actionitem,image,stringname,stringcomponent,newaction){var stringRequests=[{key:stringname,component:stringcomponent}];return str.get_strings(stringRequests).then((function(strings){return actionitem.find("span.menu-action-text").html(strings[0]),templates.renderPix(image,"core")})).then((function(pixhtml){actionitem.find(".icon").replaceWith(pixhtml),actionitem.attr("data-action",newaction)})).catch(notification.exception)},defaultEditSectionHandler=function(sectionElement,actionItem,data,courseformat,sectionid){var action=actionItem.attr("data-action");if("hide"===action||"show"===action){if("hide"===action?(sectionElement.addClass("hidden"),setSectionBadge(sectionElement[0],"hiddenfromstudents",!0,!1),replaceActionItem(actionItem,"i/show","showfromothers","format_"+courseformat,"show")):(setSectionBadge(sectionElement[0],"hiddenfromstudents",!1,!1),sectionElement.removeClass("hidden"),replaceActionItem(actionItem,"i/hide","hidefromothers","format_"+courseformat,"hide")),void 0!==data.modules)for(var i in data.modules)replaceActivityHtmlWith(data.modules[i]);void 0!==data.section_availability&§ionElement.find(".section_availability").first().replaceWith(data.section_availability);void 0!==courseeditor.state.section.get(sectionid)&&courseeditor.dispatch("sectionState",[sectionid])}else if("setmarker"===action){var oldmarker=$(SELECTOR.SECTIONLI+".current"),oldActionItem=oldmarker.find(SELECTOR.SECTIONACTIONMENU+" a[data-action=removemarker]");oldmarker.removeClass("current"),replaceActionItem(oldActionItem,"i/marker","highlight","core","setmarker"),sectionElement.addClass("current"),replaceActionItem(actionItem,"i/marked","highlightoff","core","removemarker"),courseeditor.dispatch("legacySectionAction",action,sectionid),setSectionBadge(sectionElement[0],"iscurrent",!0,!0)}else"removemarker"===action&&(sectionElement.removeClass("current"),replaceActionItem(actionItem,"i/marker","highlight","core","setmarker"),courseeditor.dispatch("legacySectionAction",action,sectionid),setSectionBadge(sectionElement[0],"iscurrent",!1,!0))};var replaceActivityHtmlWith=function(activityHTML){$("
    "+activityHTML+"
    ").find(SELECTOR.ACTIVITYLI).each((function(){var id=$(this).attr("id");let focusedPath=function(id){const element=document.getElementById(id);if(element&&element.contains(document.activeElement))return element.querySelector(SELECTOR.ACTIONAREA).contains(document.activeElement)?"".concat(SELECTOR.ACTIONAREA,' [tabindex="0"]'):document.activeElement.id?"#".concat(document.activeElement.id):void 0}(id);if($(SELECTOR.ACTIVITYLI+"#"+id).replaceWith(activityHTML),initActionMenu(id),focusedPath){var _newItem$querySelecto;null===(_newItem$querySelecto=document.getElementById(id).querySelector(focusedPath))||void 0===_newItem$querySelecto||_newItem$querySelecto.focus()}}))},editSection=function(sectionElement,sectionid,target,courseformat){var action=target.attr("data-action"),sectionreturn=target.attr("data-sectionreturn")?target.attr("data-sectionreturn"):null;if(courseeditor.supportComponents&&componentActions.includes(action))return!1;var spinner=addSectionSpinner(sectionElement),promises=ajax.call([{methodname:"core_course_edit_section",args:{id:sectionid,action:action,sectionreturn:sectionreturn}}],!0),lightbox=addSectionLightbox(sectionElement);return $.when.apply($,promises).done((function(dataencoded){var data=$.parseJSON(dataencoded);removeSpinner(sectionElement,spinner),removeLightbox(lightbox),sectionElement.find(SELECTOR.SECTIONACTIONMENU).find(SELECTOR.TOGGLE).focus();var e=$.Event("coursesectionedited",{ajaxreturn:data,action:action});sectionElement.trigger(e),e.isDefaultPrevented()||defaultEditSectionHandler(sectionElement,target,data,courseformat,sectionid)})).fail((function(ex){removeSpinner(sectionElement,spinner),removeLightbox(lightbox);var e=$.Event("coursesectioneditfailed",{exception:ex,action:action});sectionElement.trigger(e),e.isDefaultPrevented()||notification.exception(ex)})),!0},setSectionBadge=function(sectionElement,badgetype,add,removeOther){const sectionbadges=sectionElement.querySelector(SELECTOR.SECTIONBADGES);if(!sectionbadges)return;const badge=sectionbadges.querySelector('[data-type="'+badgetype+'"]');badge&&(add?(removeOther&&document.querySelectorAll('[data-type="'+badgetype+'"]').forEach((b=>{b.classList.add("d-none")})),badge.classList.remove("d-none")):badge.classList.add("d-none"))};return Y.use("moodle-course-coursebase",(function(){M.course.coursebase.register_module({set_visibility_resource_ui:function(args){var mainelement=$(args.element.getDOMNode()),cmid=getModuleId(mainelement);if(cmid){var sectionreturn=mainelement.find("."+CSS_EDITINGMOVE).attr("data-sectionreturn");refreshModule(mainelement,cmid,sectionreturn)}},updateMovedCmState:params=>{const cm=courseeditor.state.cm.get(params.cmid);void 0!==cm&&courseeditor.dispatch("sectionState",[cm.sectionid]),courseeditor.dispatch("cmState",[params.cmid])},updateMovedSectionState:()=>{courseeditor.dispatch("courseState")}})})),courseeditor.addMutations({legacyActivityAction:function(statemanager,action,cmid,affectedids){const state=statemanager.state,cm=state.cm.get(cmid);if(void 0===cm)return;const section=state.section.get(cm.sectionid);if(void 0!==section){switch(courseeditor.dispatch("cmLock",[cm.id],!0),statemanager.setReadOnly(!1),cm.locked=!1,action){case"delete":section.cmlist=section.cmlist.reduce(((cmlist,current)=>(current!=cmid&&cmlist.push(current),cmlist)),[]),state.cm.delete(cmid);break;case"hide":case"show":case"duplicate":courseeditor.dispatch("cmState",affectedids)}statemanager.setReadOnly(!0)}},legacySectionAction:function(statemanager,action,sectionid){const state=statemanager.state,section=state.section.get(sectionid);if(void 0!==section){switch(statemanager.setReadOnly(!1),section.locked=!0,statemanager.setReadOnly(!0),statemanager.setReadOnly(!1),section.locked=!1,action){case"setmarker":state.section.forEach((current=>{current.id!=sectionid&&(current.current=!1)})),section.current=!0;break;case"removemarker":section.current=!1}statemanager.setReadOnly(!0)}}}),{initCoursePage:function(courseformat){if(formatname=courseformat,$("body").on("click keypress",SELECTOR.ACTIVITYLI+" "+SELECTOR.ACTIVITYACTION+"[data-action]",(function(e){if("keypress"!==e.type||13===e.keyCode){var actionItem=$(this),moduleElement=actionItem.closest(SELECTOR.ACTIVITYLI),action=actionItem.attr("data-action"),moduleId=getModuleId(moduleElement);switch(action){case"moveleft":case"moveright":case"delete":case"duplicate":case"hide":case"stealth":case"show":case"groupsseparate":case"groupsvisible":case"groupsnone":break;default:return}moduleId&&(e.preventDefault(),"delete"===action?confirmDeleteModule(moduleElement,(function(){editModule(moduleElement,moduleId,actionItem)})):editModule(moduleElement,moduleId,actionItem))}})),$("body").on("click keypress",SELECTOR.SECTIONACTIONMENUTRIGGER+"[data-sectionid] a[data-action]",(function(e){if("keypress"===e.type&&13!==e.keyCode)return;var actionItem=$(this),sectionElement=actionItem.closest(SELECTOR.SECTIONLI),sectionId=actionItem.closest(SELECTOR.SECTIONACTIONMENUTRIGGER).attr("data-sectionid");if("permalink"===actionItem.attr("data-action"))return e.preventDefault(),void ModalCopyToClipboard.create({text:actionItem.attr("href")},str.get_string("sectionlink","course"));let isExecuted=!0;var message,onconfirm;actionItem.attr("data-confirm")?(message=actionItem.attr("data-confirm"),onconfirm=function(){isExecuted=editSection(sectionElement,sectionId,actionItem,courseformat)},str.get_strings([{key:"confirm"},{key:"yes"},{key:"no"}]).done((function(s){notification.confirm(s[0],message,s[1],s[2],onconfirm)}))):isExecuted=editSection(sectionElement,sectionId,actionItem,courseformat),isExecuted&&e.preventDefault()})),$("body").on("updated","".concat(SELECTOR.SECTIONITEM," [data-inplaceeditable]"),(function(e){if(e.ajaxreturn&&e.ajaxreturn.itemid){void 0!==courseeditor.state.section.get(e.ajaxreturn.itemid)&&courseeditor.dispatch("sectionState",[e.ajaxreturn.itemid])}})),$("body").on("updated","".concat(SELECTOR.ACTIVITYLI,' [data-itemtype="activityname"][data-inplaceeditable]'),(function(e){e.ajaxreturn&&e.ajaxreturn.itemid&&courseeditor.dispatch("cmState",[e.ajaxreturn.itemid])})),courseeditor.supportComponents&&componentActions.includes("addSection"))return;const trigger=$(SELECTOR.ADDSECTIONS),modalTitle=trigger.attr("data-add-sections"),newSections=trigger.attr("data-new-sections");str.get_string("numberweeks").then((function(strNumberSections){var modalBody=$('
    ');return modalBody.find("label").html(strNumberSections),modalBody.html()})).then((body=>ModalSaveCancel.create({body:body,title:modalTitle}))).then((function(modal){var numSections=$(modal.getBody()).find("#add_section_numsections"),addSections=function(){""+parseInt(numSections.val())===numSections.val()&&parseInt(numSections.val())>=1&&(document.location=trigger.attr("href")+"&numsections="+parseInt(numSections.val()))};return modal.setSaveButtonText(modalTitle),modal.getRoot().on(ModalEvents.shown,(function(){numSections.focus().select().on("keydown",(function(e){e.keyCode===KeyCodes.enter&&addSections()}))})),modal.getRoot().on(ModalEvents.save,(function(e){e.preventDefault(),addSections()})),trigger.on("click",(e=>{e.preventDefault(),modal.show()})),modal})).catch(notification.exception)},replaceSectionActionItem:function(sectionelement,selector,image,stringname,stringcomponent,newaction){log.debug("replaceSectionActionItem() is deprecated and will be removed.");var actionitem=sectionelement.find(SELECTOR.SECTIONACTIONMENU+" "+selector);replaceActionItem(actionitem,image,stringname,stringcomponent,newaction)},refreshModule:refreshModule,refreshSection:function(element,sectionid,sectionreturn){void 0===sectionreturn&&(sectionreturn=courseeditor.sectionReturn);const sectionElement=$(element),promises=ajax.call([{methodname:"core_course_edit_section",args:{id:sectionid,action:"refresh",sectionreturn:sectionreturn}}],!0);var spinner=addSectionSpinner(sectionElement);return new Promise(((resolve,reject)=>{$.when.apply($,promises).done((dataencoded=>{removeSpinner(sectionElement,spinner);const data=$.parseJSON(dataencoded),newSectionElement=$(data.content);sectionElement.replaceWith(newSectionElement),$("".concat(SELECTOR.SECTIONLI,"#").concat(sectionid," ").concat(SELECTOR.ACTIVITYLI)).each(((index,activity)=>{initActionMenu(activity.data("id"))}));dispatchEvent(CourseEvents.sectionRefreshed,{ajaxreturn:data,action:"refresh",newSectionElement:newSectionElement.get(0)},newSectionElement).defaultPrevented||defaultEditSectionHandler(newSectionElement,$(SELECTOR.SECTIONLI+"#"+sectionid),data,formatname,sectionid),resolve(data)})).fail((ex=>{dispatchEvent("coursesectionrefreshfailed",{exception:ex,action:"refresh"},sectionElement).defaultPrevented||notification.exception(ex),reject()}))}))}}})); //# sourceMappingURL=actions.min.js.map \ No newline at end of file diff --git a/course/amd/build/actions.min.js.map b/course/amd/build/actions.min.js.map index a7cf51b9e705d..9a7d8152d76ff 100644 --- a/course/amd/build/actions.min.js.map +++ b/course/amd/build/actions.min.js.map @@ -1 +1 @@ -{"version":3,"file":"actions.min.js","sources":["../src/actions.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 * Various actions on modules and sections in the editing mode - hiding, duplicating, deleting, etc.\n *\n * @module core_course/actions\n * @copyright 2016 Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.3\n */\ndefine(\n [\n 'jquery',\n 'core/ajax',\n 'core/templates',\n 'core/notification',\n 'core/str',\n 'core/url',\n 'core/yui',\n 'core/modal_copy_to_clipboard',\n 'core/modal_save_cancel',\n 'core/modal_events',\n 'core/key_codes',\n 'core/log',\n 'core_courseformat/courseeditor',\n 'core/event_dispatcher',\n 'core_course/events'\n ],\n function(\n $,\n ajax,\n templates,\n notification,\n str,\n url,\n Y,\n ModalCopyToClipboard,\n ModalSaveCancel,\n ModalEvents,\n KeyCodes,\n log,\n editor,\n EventDispatcher,\n CourseEvents\n ) {\n\n // Eventually, core_courseformat/local/content/actions will handle all actions for\n // component compatible formats and the default actions.js won't be necessary anymore.\n // Meanwhile, we filter the migrated actions.\n const componentActions = [\n 'moveSection', 'moveCm', 'addSection', 'deleteSection', 'cmDelete', 'cmDuplicate', 'sectionHide', 'sectionShow',\n 'cmHide', 'cmShow', 'cmStealth', 'sectionHighlight', 'sectionUnhighlight', 'cmMoveRight', 'cmMoveLeft',\n 'cmNoGroups', 'cmVisibleGroups', 'cmSeparateGroups',\n ];\n\n // The course reactive instance.\n const courseeditor = editor.getCurrentCourseEditor();\n\n // The current course format name (loaded on init).\n let formatname;\n\n var CSS = {\n EDITINPROGRESS: 'editinprogress',\n SECTIONDRAGGABLE: 'sectiondraggable',\n EDITINGMOVE: 'editing_move'\n };\n var SELECTOR = {\n ACTIVITYLI: 'li.activity',\n ACTIONAREA: '.actions',\n ACTIVITYACTION: 'a.cm-edit-action',\n MENU: '.moodle-actionmenu[data-enhance=moodle-core-actionmenu]',\n TOGGLE: '.toggle-display,.dropdown-toggle',\n SECTIONLI: 'li.section',\n SECTIONACTIONMENU: '.section_action_menu',\n SECTIONACTIONMENUTRIGGER: '.section-actions',\n SECTIONITEM: '[data-for=\"section_title\"]',\n ADDSECTIONS: '.changenumsections [data-add-sections]',\n SECTIONBADGES: '[data-region=\"sectionbadges\"]',\n };\n\n Y.use('moodle-course-coursebase', function() {\n var courseformatselector = M.course.format.get_section_selector();\n if (courseformatselector) {\n SELECTOR.SECTIONLI = courseformatselector;\n }\n });\n\n /**\n * Dispatch event wrapper.\n *\n * Old jQuery events will be replaced by native events gradually.\n *\n * @method dispatchEvent\n * @param {String} eventName The name of the event\n * @param {Object} detail Any additional details to pass into the eveent\n * @param {Node|HTMLElement} container The point at which to dispatch the event\n * @param {Object} options\n * @param {Boolean} options.bubbles Whether to bubble up the DOM\n * @param {Boolean} options.cancelable Whether preventDefault() can be called\n * @param {Boolean} options.composed Whether the event can bubble across the ShadowDOM boundary\n * @returns {CustomEvent}\n */\n const dispatchEvent = function(eventName, detail, container, options) {\n // Most actions still uses jQuery node instead of regular HTMLElement.\n if (!(container instanceof Element) && container.get !== undefined) {\n container = container.get(0);\n }\n return EventDispatcher.dispatchEvent(eventName, detail, container, options);\n };\n\n /**\n * Wrapper for Y.Moodle.core_course.util.cm.getId\n *\n * @param {JQuery} element\n * @returns {Integer}\n */\n var getModuleId = function(element) {\n // Check if we have a data-id first.\n const item = element.get(0);\n if (item.dataset.id) {\n return item.dataset.id;\n }\n // Use YUI way if data-id is not present.\n let id;\n Y.use('moodle-course-util', function(Y) {\n id = Y.Moodle.core_course.util.cm.getId(Y.Node(item));\n });\n return id;\n };\n\n /**\n * Wrapper for Y.Moodle.core_course.util.cm.getName\n *\n * @param {JQuery} element\n * @returns {String}\n */\n var getModuleName = function(element) {\n var name;\n Y.use('moodle-course-util', function(Y) {\n name = Y.Moodle.core_course.util.cm.getName(Y.Node(element.get(0)));\n });\n // Check if we have the name in the course state.\n const state = courseeditor.state;\n const cmid = getModuleId(element);\n if (!name && state && cmid) {\n name = state.cm.get(cmid)?.name;\n }\n return name;\n };\n\n /**\n * Wrapper for M.util.add_spinner for an activity\n *\n * @param {JQuery} activity\n * @returns {Node}\n */\n var addActivitySpinner = function(activity) {\n activity.addClass(CSS.EDITINPROGRESS);\n var actionarea = activity.find(SELECTOR.ACTIONAREA).get(0);\n if (actionarea) {\n var spinner = M.util.add_spinner(Y, Y.Node(actionarea));\n spinner.show();\n // Lock the activity state element.\n if (activity.data('id') !== undefined) {\n courseeditor.dispatch('cmLock', [activity.data('id')], true);\n }\n return spinner;\n }\n return null;\n };\n\n /**\n * Wrapper for M.util.add_spinner for a section\n *\n * @param {JQuery} sectionelement\n * @returns {Node}\n */\n var addSectionSpinner = function(sectionelement) {\n sectionelement.addClass(CSS.EDITINPROGRESS);\n var actionarea = sectionelement.find(SELECTOR.SECTIONACTIONMENU).get(0);\n if (actionarea) {\n var spinner = M.util.add_spinner(Y, Y.Node(actionarea));\n spinner.show();\n // Lock the section state element.\n if (sectionelement.data('id') !== undefined) {\n courseeditor.dispatch('sectionLock', [sectionelement.data('id')], true);\n }\n return spinner;\n }\n return null;\n };\n\n /**\n * Wrapper for M.util.add_lightbox\n *\n * @param {JQuery} sectionelement\n * @returns {Node}\n */\n var addSectionLightbox = function(sectionelement) {\n const item = sectionelement.get(0);\n var lightbox = M.util.add_lightbox(Y, Y.Node(item));\n if (item.dataset.for == 'section' && item.dataset.id) {\n courseeditor.dispatch('sectionLock', [item.dataset.id], true);\n lightbox.setAttribute('data-state', 'section');\n lightbox.setAttribute('data-state-id', item.dataset.id);\n }\n lightbox.show();\n return lightbox;\n };\n\n /**\n * Removes the spinner element\n *\n * @param {JQuery} element\n * @param {Node} spinner\n * @param {Number} delay\n */\n var removeSpinner = function(element, spinner, delay) {\n window.setTimeout(function() {\n element.removeClass(CSS.EDITINPROGRESS);\n if (spinner) {\n spinner.hide();\n }\n // Unlock the state element.\n if (element.data('id') !== undefined) {\n const mutation = (element.data('for') === 'section') ? 'sectionLock' : 'cmLock';\n courseeditor.dispatch(mutation, [element.data('id')], false);\n }\n }, delay);\n };\n\n /**\n * Removes the lightbox element\n *\n * @param {Node} lightbox lighbox YUI element returned by addSectionLightbox\n * @param {Number} delay\n */\n var removeLightbox = function(lightbox, delay) {\n if (lightbox) {\n window.setTimeout(function() {\n lightbox.hide();\n // Unlock state if necessary.\n if (lightbox.getAttribute('data-state')) {\n courseeditor.dispatch(\n `${lightbox.getAttribute('data-state')}Lock`,\n [lightbox.getAttribute('data-state-id')],\n false\n );\n }\n }, delay);\n }\n };\n\n /**\n * Initialise action menu for the element (section or module)\n *\n * @param {String} elementid CSS id attribute of the element\n */\n var initActionMenu = function(elementid) {\n // Initialise action menu in the new activity.\n Y.use('moodle-course-coursebase', function() {\n M.course.coursebase.invoke_function('setup_for_resource', '#' + elementid);\n });\n if (M.core.actionmenu && M.core.actionmenu.newDOMNode) {\n M.core.actionmenu.newDOMNode(Y.one('#' + elementid));\n }\n };\n\n /**\n * Returns focus to the element that was clicked or \"Edit\" link if element is no longer visible.\n *\n * @param {String} elementId CSS id attribute of the element\n * @param {String} action data-action property of the element that was clicked\n */\n var focusActionItem = function(elementId, action) {\n var mainelement = $('#' + elementId);\n var selector = '[data-action=' + action + ']';\n if (action === 'groupsseparate' || action === 'groupsvisible' || action === 'groupsnone') {\n // New element will have different data-action.\n selector = '[data-action=groupsseparate],[data-action=groupsvisible],[data-action=groupsnone]';\n }\n if (mainelement.find(selector).is(':visible')) {\n mainelement.find(selector).focus();\n } else {\n // Element not visible, focus the \"Edit\" link.\n mainelement.find(SELECTOR.MENU).find(SELECTOR.TOGGLE).focus();\n }\n };\n\n /**\n * Find next after the element\n *\n * @param {JQuery} mainElement element that is about to be deleted\n * @returns {JQuery}\n */\n var findNextFocusable = function(mainElement) {\n var tabables = $(\"a:visible\");\n var isInside = false;\n var foundElement = null;\n tabables.each(function() {\n if ($.contains(mainElement[0], this)) {\n isInside = true;\n } else if (isInside) {\n foundElement = this;\n return false; // Returning false in .each() is equivalent to \"break;\" inside the loop in php.\n }\n return true;\n });\n return foundElement;\n };\n\n /**\n * Performs an action on a module (moving, deleting, duplicating, hiding, etc.)\n *\n * @param {JQuery} moduleElement activity element we perform action on\n * @param {Number} cmid\n * @param {JQuery} target the element (menu item) that was clicked\n */\n var editModule = function(moduleElement, cmid, target) {\n var action = target.attr('data-action');\n var spinner = addActivitySpinner(moduleElement);\n var promises = ajax.call([{\n methodname: 'core_course_edit_module',\n args: {id: cmid,\n action: action,\n sectionreturn: target.attr('data-sectionreturn') ? target.attr('data-sectionreturn') : null\n }\n }], true);\n\n var lightbox;\n if (action === 'duplicate') {\n lightbox = addSectionLightbox(target.closest(SELECTOR.SECTIONLI));\n }\n $.when.apply($, promises)\n .done(function(data) {\n var elementToFocus = findNextFocusable(moduleElement);\n moduleElement.replaceWith(data);\n let affectedids = [];\n // Initialise action menu for activity(ies) added as a result of this.\n $('
    ' + data + '
    ').find(SELECTOR.ACTIVITYLI).each(function(index) {\n initActionMenu($(this).attr('id'));\n if (index === 0) {\n focusActionItem($(this).attr('id'), action);\n elementToFocus = null;\n }\n // Save any activity id in cmids.\n affectedids.push(getModuleId($(this)));\n });\n // In case of activity deletion focus the next focusable element.\n if (elementToFocus) {\n elementToFocus.focus();\n }\n // Remove spinner and lightbox with a delay.\n removeSpinner(moduleElement, spinner, 400);\n removeLightbox(lightbox, 400);\n // Trigger event that can be observed by course formats.\n moduleElement.trigger($.Event('coursemoduleedited', {ajaxreturn: data, action: action}));\n\n // Modify cm state.\n courseeditor.dispatch('legacyActivityAction', action, cmid, affectedids);\n\n }).fail(function(ex) {\n // Remove spinner and lightbox.\n removeSpinner(moduleElement, spinner);\n removeLightbox(lightbox);\n // Trigger event that can be observed by course formats.\n var e = $.Event('coursemoduleeditfailed', {exception: ex, action: action});\n moduleElement.trigger(e);\n if (!e.isDefaultPrevented()) {\n notification.exception(ex);\n }\n });\n };\n\n /**\n * Requests html for the module via WS core_course_get_module and updates the module on the course page\n *\n * Used after d&d of the module to another section\n *\n * @param {JQuery|Element} element\n * @param {Number} cmid\n * @param {Number} sectionreturn\n * @return {Promise} the refresh promise\n */\n var refreshModule = function(element, cmid, sectionreturn) {\n\n if (sectionreturn === undefined) {\n sectionreturn = courseeditor.sectionReturn;\n }\n\n const activityElement = $(element);\n var spinner = addActivitySpinner(activityElement);\n var promises = ajax.call([{\n methodname: 'core_course_get_module',\n args: {id: cmid, sectionreturn: sectionreturn}\n }], true);\n\n return new Promise((resolve, reject) => {\n $.when.apply($, promises)\n .done(function(data) {\n removeSpinner(activityElement, spinner, 400);\n replaceActivityHtmlWith(data);\n resolve(data);\n }).fail(function() {\n removeSpinner(activityElement, spinner);\n reject();\n });\n });\n };\n\n /**\n * Requests html for the section via WS core_course_edit_section and updates the section on the course page\n *\n * @param {JQuery|Element} element\n * @param {Number} sectionid\n * @param {Number} sectionreturn\n * @return {Promise} the refresh promise\n */\n var refreshSection = function(element, sectionid, sectionreturn) {\n\n if (sectionreturn === undefined) {\n sectionreturn = courseeditor.sectionReturn;\n }\n\n const sectionElement = $(element);\n const action = 'refresh';\n const promises = ajax.call([{\n methodname: 'core_course_edit_section',\n args: {id: sectionid, action, sectionreturn},\n }], true);\n\n var spinner = addSectionSpinner(sectionElement);\n return new Promise((resolve, reject) => {\n $.when.apply($, promises)\n .done(dataencoded => {\n\n removeSpinner(sectionElement, spinner);\n const data = $.parseJSON(dataencoded);\n\n const newSectionElement = $(data.content);\n sectionElement.replaceWith(newSectionElement);\n\n // Init modules menus.\n $(`${SELECTOR.SECTIONLI}#${sectionid} ${SELECTOR.ACTIVITYLI}`).each(\n (index, activity) => {\n initActionMenu(activity.data('id'));\n }\n );\n\n // Trigger event that can be observed by course formats.\n const event = dispatchEvent(\n CourseEvents.sectionRefreshed,\n {\n ajaxreturn: data,\n action: action,\n newSectionElement: newSectionElement.get(0),\n },\n newSectionElement\n );\n\n if (!event.defaultPrevented) {\n defaultEditSectionHandler(\n newSectionElement, $(SELECTOR.SECTIONLI + '#' + sectionid),\n data,\n formatname,\n sectionid\n );\n }\n resolve(data);\n }).fail(ex => {\n // Trigger event that can be observed by course formats.\n const event = dispatchEvent(\n 'coursesectionrefreshfailed',\n {exception: ex, action: action},\n sectionElement\n );\n if (!event.defaultPrevented) {\n notification.exception(ex);\n }\n reject();\n });\n });\n };\n\n /**\n * Displays the delete confirmation to delete a module\n *\n * @param {JQuery} mainelement activity element we perform action on\n * @param {function} onconfirm function to execute on confirm\n */\n var confirmDeleteModule = function(mainelement, onconfirm) {\n var modtypename = mainelement.attr('class').match(/modtype_([^\\s]*)/)[1];\n var modulename = getModuleName(mainelement);\n\n str.get_string('pluginname', modtypename).done(function(pluginname) {\n var plugindata = {\n type: pluginname,\n name: modulename\n };\n str.get_strings([\n {key: 'confirm', component: 'core'},\n {key: modulename === null ? 'deletechecktype' : 'deletechecktypename', param: plugindata},\n {key: 'yes'},\n {key: 'no'}\n ]).done(function(s) {\n notification.confirm(s[0], s[1], s[2], s[3], onconfirm);\n }\n );\n });\n };\n\n /**\n * Displays the delete confirmation to delete a section\n *\n * @param {String} message confirmation message\n * @param {function} onconfirm function to execute on confirm\n */\n var confirmEditSection = function(message, onconfirm) {\n str.get_strings([\n {key: 'confirm'}, // TODO link text\n {key: 'yes'},\n {key: 'no'}\n ]).done(function(s) {\n notification.confirm(s[0], message, s[1], s[2], onconfirm);\n }\n );\n };\n\n /**\n * Replaces an action menu item with another one (for example Show->Hide, Set marker->Remove marker)\n *\n * @param {JQuery} actionitem\n * @param {String} image new image name (\"i/show\", \"i/hide\", etc.)\n * @param {String} stringname new string for the action menu item\n * @param {String} stringcomponent\n * @param {String} newaction new value for data-action attribute of the link\n * @return {Promise} promise which is resolved when the replacement has completed\n */\n var replaceActionItem = function(actionitem, image, stringname,\n stringcomponent, newaction) {\n\n var stringRequests = [{key: stringname, component: stringcomponent}];\n // Do not provide an icon with duplicate, different text to the menu item.\n\n return str.get_strings(stringRequests).then(function(strings) {\n actionitem.find('span.menu-action-text').html(strings[0]);\n\n return templates.renderPix(image, 'core');\n }).then(function(pixhtml) {\n actionitem.find('.icon').replaceWith(pixhtml);\n actionitem.attr('data-action', newaction);\n return;\n }).catch(notification.exception);\n };\n\n /**\n * Default post-processing for section AJAX edit actions.\n *\n * This can be overridden in course formats by listening to event coursesectionedited:\n *\n * $('body').on('coursesectionedited', 'li.section', function(e) {\n * var action = e.action,\n * sectionElement = $(e.target),\n * data = e.ajaxreturn;\n * // ... Do some processing here.\n * e.preventDefault(); // Prevent default handler.\n * });\n *\n * @param {JQuery} sectionElement\n * @param {JQuery} actionItem\n * @param {Object} data\n * @param {String} courseformat\n * @param {Number} sectionid\n */\n var defaultEditSectionHandler = function(sectionElement, actionItem, data, courseformat, sectionid) {\n var action = actionItem.attr('data-action');\n if (action === 'hide' || action === 'show') {\n if (action === 'hide') {\n sectionElement.addClass('hidden');\n setSectionBadge(sectionElement[0], 'hiddenfromstudents', true, false);\n replaceActionItem(actionItem, 'i/show',\n 'showfromothers', 'format_' + courseformat, 'show');\n } else {\n setSectionBadge(sectionElement[0], 'hiddenfromstudents', false, false);\n sectionElement.removeClass('hidden');\n replaceActionItem(actionItem, 'i/hide',\n 'hidefromothers', 'format_' + courseformat, 'hide');\n }\n // Replace the modules with new html (that indicates that they are now hidden or not hidden).\n if (data.modules !== undefined) {\n for (var i in data.modules) {\n replaceActivityHtmlWith(data.modules[i]);\n }\n }\n // Replace the section availability information.\n if (data.section_availability !== undefined) {\n sectionElement.find('.section_availability').first().replaceWith(data.section_availability);\n }\n // Modify course state.\n const section = courseeditor.state.section.get(sectionid);\n if (section !== undefined) {\n courseeditor.dispatch('sectionState', [sectionid]);\n }\n } else if (action === 'setmarker') {\n var oldmarker = $(SELECTOR.SECTIONLI + '.current'),\n oldActionItem = oldmarker.find(SELECTOR.SECTIONACTIONMENU + ' ' + 'a[data-action=removemarker]');\n oldmarker.removeClass('current');\n replaceActionItem(oldActionItem, 'i/marker',\n 'highlight', 'core', 'setmarker');\n sectionElement.addClass('current');\n replaceActionItem(actionItem, 'i/marked',\n 'highlightoff', 'core', 'removemarker');\n courseeditor.dispatch('legacySectionAction', action, sectionid);\n setSectionBadge(sectionElement[0], 'iscurrent', true, true);\n } else if (action === 'removemarker') {\n sectionElement.removeClass('current');\n replaceActionItem(actionItem, 'i/marker',\n 'highlight', 'core', 'setmarker');\n courseeditor.dispatch('legacySectionAction', action, sectionid);\n setSectionBadge(sectionElement[0], 'iscurrent', false, true);\n }\n };\n\n /**\n * Get the focused element path in an activity if any.\n *\n * This method is used to restore focus when the activity HTML is refreshed.\n * Only the main course editor elements can be refocused as they are always present\n * even if the activity content changes.\n *\n * @param {String} id the element id the activity element\n * @return {String|undefined} the inner path of the focused element or undefined\n */\n const getActivityFocusedElement = function(id) {\n const element = document.getElementById(id);\n if (!element || !element.contains(document.activeElement)) {\n return undefined;\n }\n // Check if the actions menu toggler is focused.\n if (element.querySelector(SELECTOR.ACTIONAREA).contains(document.activeElement)) {\n return `${SELECTOR.ACTIONAREA} [tabindex=\"0\"]`;\n }\n // Return the current element id if any.\n if (document.activeElement.id) {\n return `#${document.activeElement.id}`;\n }\n return undefined;\n };\n\n /**\n * Replaces the course module with the new html (used to update module after it was edited or its visibility was changed).\n *\n * @param {String} activityHTML\n */\n var replaceActivityHtmlWith = function(activityHTML) {\n $('
    ' + activityHTML + '
    ').find(SELECTOR.ACTIVITYLI).each(function() {\n // Extract id from the new activity html.\n var id = $(this).attr('id');\n // Check if the current focused element is inside the activity.\n let focusedPath = getActivityFocusedElement(id);\n // Find the existing element with the same id and replace its contents with new html.\n $(SELECTOR.ACTIVITYLI + '#' + id).replaceWith(activityHTML);\n // Initialise action menu.\n initActionMenu(id);\n // Re-focus the previous elements.\n if (focusedPath) {\n const newItem = document.getElementById(id);\n newItem.querySelector(focusedPath)?.focus();\n }\n\n });\n };\n\n /**\n * Performs an action on a module (moving, deleting, duplicating, hiding, etc.)\n *\n * @param {JQuery} sectionElement section element we perform action on\n * @param {Nunmber} sectionid\n * @param {JQuery} target the element (menu item) that was clicked\n * @param {String} courseformat\n * @return {boolean} true the action call is sent to the server or false if it is ignored.\n */\n var editSection = function(sectionElement, sectionid, target, courseformat) {\n var action = target.attr('data-action'),\n sectionreturn = target.attr('data-sectionreturn') ? target.attr('data-sectionreturn') : null;\n\n // Filter direct component handled actions.\n if (courseeditor.supportComponents && componentActions.includes(action)) {\n return false;\n }\n\n var spinner = addSectionSpinner(sectionElement);\n var promises = ajax.call([{\n methodname: 'core_course_edit_section',\n args: {id: sectionid, action: action, sectionreturn: sectionreturn}\n }], true);\n\n var lightbox = addSectionLightbox(sectionElement);\n $.when.apply($, promises)\n .done(function(dataencoded) {\n var data = $.parseJSON(dataencoded);\n removeSpinner(sectionElement, spinner);\n removeLightbox(lightbox);\n sectionElement.find(SELECTOR.SECTIONACTIONMENU).find(SELECTOR.TOGGLE).focus();\n // Trigger event that can be observed by course formats.\n var e = $.Event('coursesectionedited', {ajaxreturn: data, action: action});\n sectionElement.trigger(e);\n if (!e.isDefaultPrevented()) {\n defaultEditSectionHandler(sectionElement, target, data, courseformat, sectionid);\n }\n }).fail(function(ex) {\n // Remove spinner and lightbox.\n removeSpinner(sectionElement, spinner);\n removeLightbox(lightbox);\n // Trigger event that can be observed by course formats.\n var e = $.Event('coursesectioneditfailed', {exception: ex, action: action});\n sectionElement.trigger(e);\n if (!e.isDefaultPrevented()) {\n notification.exception(ex);\n }\n });\n return true;\n };\n\n /**\n * Sets the section badge in the section header.\n *\n * @param {JQuery} sectionElement section element we perform action on\n * @param {String} badgetype the type of badge this is for\n * @param {bool} add true to add, false to remove\n * @param {boolean} removeOther in case of adding a badge, whether to remove all other.\n */\n var setSectionBadge = function(sectionElement, badgetype, add, removeOther) {\n const sectionbadges = sectionElement.querySelector(SELECTOR.SECTIONBADGES);\n if (!sectionbadges) {\n return;\n }\n const badge = sectionbadges.querySelector('[data-type=\"' + badgetype + '\"]');\n if (!badge) {\n return;\n }\n if (add) {\n if (removeOther) {\n document.querySelectorAll('[data-type=\"' + badgetype + '\"]').forEach((b) => {\n b.classList.add('d-none');\n });\n }\n badge.classList.remove('d-none');\n } else {\n badge.classList.add('d-none');\n }\n };\n\n // Register a function to be executed after D&D of an activity.\n Y.use('moodle-course-coursebase', function() {\n M.course.coursebase.register_module({\n // Ignore camelcase eslint rule for the next line because it is an expected name of the callback.\n // eslint-disable-next-line camelcase\n set_visibility_resource_ui: function(args) {\n var mainelement = $(args.element.getDOMNode());\n var cmid = getModuleId(mainelement);\n if (cmid) {\n var sectionreturn = mainelement.find('.' + CSS.EDITINGMOVE).attr('data-sectionreturn');\n refreshModule(mainelement, cmid, sectionreturn);\n }\n },\n /**\n * Update the course state when some cm is moved via YUI.\n * @param {*} params\n */\n updateMovedCmState: (params) => {\n const state = courseeditor.state;\n\n // Update old section.\n const cm = state.cm.get(params.cmid);\n if (cm !== undefined) {\n courseeditor.dispatch('sectionState', [cm.sectionid]);\n }\n // Update cm state.\n courseeditor.dispatch('cmState', [params.cmid]);\n },\n /**\n * Update the course state when some section is moved via YUI.\n */\n updateMovedSectionState: () => {\n courseeditor.dispatch('courseState');\n },\n });\n });\n\n // From Moodle 4.0 all edit actions are being re-implemented as state mutation.\n // This means all method from this \"actions\" module will be deprecated when all the course\n // interface is migrated to reactive components.\n // Most legacy actions did not provide enough information to regenarate the course so they\n // use the mutations courseState, sectionState and cmState to get the updated state from\n // the server. However, some activity actions where we can prevent an extra webservice\n // call by implementing an adhoc mutation.\n courseeditor.addMutations({\n /**\n * Compatibility function to update Moodle 4.0 course state using legacy actions.\n *\n * This method only updates some actions which does not require to use cmState mutation\n * to get updated data form the server.\n *\n * @param {Object} statemanager the current state in read write mode\n * @param {String} action the performed action\n * @param {Number} cmid the affected course module id\n * @param {Array} affectedids all affected cm ids (for duplicate action)\n */\n legacyActivityAction: function(statemanager, action, cmid, affectedids) {\n\n const state = statemanager.state;\n const cm = state.cm.get(cmid);\n if (cm === undefined) {\n return;\n }\n const section = state.section.get(cm.sectionid);\n if (section === undefined) {\n return;\n }\n\n // Send the element is locked.\n courseeditor.dispatch('cmLock', [cm.id], true);\n\n // Now we do the real mutation.\n statemanager.setReadOnly(false);\n\n // This unlocked will take effect when the read only is restored.\n cm.locked = false;\n\n switch (action) {\n case 'delete':\n // Remove from section.\n section.cmlist = section.cmlist.reduce(\n (cmlist, current) => {\n if (current != cmid) {\n cmlist.push(current);\n }\n return cmlist;\n },\n []\n );\n // Delete form list.\n state.cm.delete(cmid);\n break;\n\n case 'hide':\n case 'show':\n case 'duplicate':\n courseeditor.dispatch('cmState', affectedids);\n break;\n }\n statemanager.setReadOnly(true);\n },\n legacySectionAction: function(statemanager, action, sectionid) {\n\n const state = statemanager.state;\n const section = state.section.get(sectionid);\n if (section === undefined) {\n return;\n }\n\n // Send the element is locked. Reactive events are only triggered when the state\n // read only mode is restored. We want to notify the interface the element is\n // locked so we need to do a quick lock operation before performing the rest\n // of the mutation.\n statemanager.setReadOnly(false);\n section.locked = true;\n statemanager.setReadOnly(true);\n\n // Now we do the real mutation.\n statemanager.setReadOnly(false);\n\n // This locked will take effect when the read only is restored.\n section.locked = false;\n\n switch (action) {\n case 'setmarker':\n // Remove previous marker.\n state.section.forEach((current) => {\n if (current.id != sectionid) {\n current.current = false;\n }\n });\n section.current = true;\n break;\n\n case 'removemarker':\n section.current = false;\n break;\n }\n statemanager.setReadOnly(true);\n },\n });\n\n return /** @alias module:core_course/actions */ {\n\n /**\n * Initialises course page\n *\n * @method init\n * @param {String} courseformat name of the current course format (for fetching strings)\n */\n initCoursePage: function(courseformat) {\n\n formatname = courseformat;\n\n // Add a handler for course module actions.\n $('body').on('click keypress', SELECTOR.ACTIVITYLI + ' ' +\n SELECTOR.ACTIVITYACTION + '[data-action]', function(e) {\n if (e.type === 'keypress' && e.keyCode !== 13) {\n return;\n }\n var actionItem = $(this),\n moduleElement = actionItem.closest(SELECTOR.ACTIVITYLI),\n action = actionItem.attr('data-action'),\n moduleId = getModuleId(moduleElement);\n switch (action) {\n case 'moveleft':\n case 'moveright':\n case 'delete':\n case 'duplicate':\n case 'hide':\n case 'stealth':\n case 'show':\n case 'groupsseparate':\n case 'groupsvisible':\n case 'groupsnone':\n break;\n default:\n // Nothing to do here!\n return;\n }\n if (!moduleId) {\n return;\n }\n e.preventDefault();\n if (action === 'delete') {\n // Deleting requires confirmation.\n confirmDeleteModule(moduleElement, function() {\n editModule(moduleElement, moduleId, actionItem);\n });\n } else {\n editModule(moduleElement, moduleId, actionItem);\n }\n });\n\n // Add a handler for section action menu.\n $('body').on('click keypress',\n SELECTOR.SECTIONACTIONMENUTRIGGER + '[data-sectionid] ' +\n 'a[data-action]', function(e) {\n if (e.type === 'keypress' && e.keyCode !== 13) {\n return;\n }\n var actionItem = $(this),\n sectionElement = actionItem.closest(SELECTOR.SECTIONLI),\n sectionId = actionItem.closest(SELECTOR.SECTIONACTIONMENUTRIGGER).attr('data-sectionid');\n\n if (actionItem.attr('data-action') === 'permalink') {\n e.preventDefault();\n ModalCopyToClipboard.create({\n text: actionItem.attr('href'),\n }, str.get_string('sectionlink', 'course')\n );\n return;\n }\n\n let isExecuted = true;\n if (actionItem.attr('data-confirm')) {\n // Action requires confirmation.\n confirmEditSection(actionItem.attr('data-confirm'), function() {\n isExecuted = editSection(sectionElement, sectionId, actionItem, courseformat);\n });\n } else {\n isExecuted = editSection(sectionElement, sectionId, actionItem, courseformat);\n }\n // Prevent any other module from capturing the action if it is already in execution.\n if (isExecuted) {\n e.preventDefault();\n }\n });\n\n // The section and activity names are edited using inplace editable.\n // The \"update\" jQuery event must be captured in order to update the course state.\n $('body').on('updated', `${SELECTOR.SECTIONITEM} [data-inplaceeditable]`, function(e) {\n if (e.ajaxreturn && e.ajaxreturn.itemid) {\n const state = courseeditor.state;\n const section = state.section.get(e.ajaxreturn.itemid);\n if (section !== undefined) {\n courseeditor.dispatch('sectionState', [e.ajaxreturn.itemid]);\n }\n }\n });\n $('body').on('updated', `${SELECTOR.ACTIVITYLI} [data-inplaceeditable]`, function(e) {\n if (e.ajaxreturn && e.ajaxreturn.itemid) {\n courseeditor.dispatch('cmState', [e.ajaxreturn.itemid]);\n }\n });\n\n // Component-based formats don't use modals to create sections.\n if (courseeditor.supportComponents && componentActions.includes('addSection')) {\n return;\n }\n\n // Add a handler for \"Add sections\" link to ask for a number of sections to add.\n const trigger = $(SELECTOR.ADDSECTIONS);\n const modalTitle = trigger.attr('data-add-sections');\n const newSections = trigger.attr('data-new-sections');\n str.get_string('numberweeks')\n .then(function(strNumberSections) {\n var modalBody = $('
    ' +\n '
    ');\n modalBody.find('label').html(strNumberSections);\n\n return modalBody.html();\n })\n .then((body) => ModalSaveCancel.create({\n body,\n title: modalTitle,\n }))\n .then(function(modal) {\n var numSections = $(modal.getBody()).find('#add_section_numsections'),\n addSections = function() {\n // Check if value of the \"Number of sections\" is a valid positive integer and redirect\n // to adding a section script.\n if ('' + parseInt(numSections.val()) === numSections.val() && parseInt(numSections.val()) >= 1) {\n document.location = trigger.attr('href') + '&numsections=' + parseInt(numSections.val());\n }\n };\n modal.setSaveButtonText(modalTitle);\n modal.getRoot().on(ModalEvents.shown, function() {\n // When modal is shown focus and select the input and add a listener to keypress of \"Enter\".\n numSections.focus().select().on('keydown', function(e) {\n if (e.keyCode === KeyCodes.enter) {\n addSections();\n }\n });\n });\n modal.getRoot().on(ModalEvents.save, function(e) {\n // When modal \"Add\" button is pressed.\n e.preventDefault();\n addSections();\n });\n\n trigger.on('click', (e) => {\n e.preventDefault();\n modal.show();\n });\n\n return modal;\n })\n .catch(notification.exception);\n },\n\n /**\n * Replaces a section action menu item with another one (for example Show->Hide, Set marker->Remove marker)\n *\n * This method can be used by course formats in their listener to the coursesectionedited event\n *\n * @deprecated since Moodle 3.9\n * @param {JQuery} sectionelement\n * @param {String} selector CSS selector inside the section element, for example \"a[data-action=show]\"\n * @param {String} image new image name (\"i/show\", \"i/hide\", etc.)\n * @param {String} stringname new string for the action menu item\n * @param {String} stringcomponent\n * @param {String} newaction new value for data-action attribute of the link\n */\n replaceSectionActionItem: function(sectionelement, selector, image, stringname,\n stringcomponent, newaction) {\n log.debug('replaceSectionActionItem() is deprecated and will be removed.');\n var actionitem = sectionelement.find(SELECTOR.SECTIONACTIONMENU + ' ' + selector);\n replaceActionItem(actionitem, image, stringname, stringcomponent, newaction);\n },\n // Method to refresh a module.\n refreshModule,\n refreshSection,\n };\n });\n"],"names":["define","$","ajax","templates","notification","str","url","Y","ModalCopyToClipboard","ModalSaveCancel","ModalEvents","KeyCodes","log","editor","EventDispatcher","CourseEvents","componentActions","courseeditor","getCurrentCourseEditor","formatname","CSS","SELECTOR","ACTIVITYLI","ACTIONAREA","ACTIVITYACTION","MENU","TOGGLE","SECTIONLI","SECTIONACTIONMENU","SECTIONACTIONMENUTRIGGER","SECTIONITEM","ADDSECTIONS","SECTIONBADGES","use","courseformatselector","M","course","format","get_section_selector","dispatchEvent","eventName","detail","container","options","Element","undefined","get","getModuleId","element","item","dataset","id","Moodle","core_course","util","cm","getId","Node","addActivitySpinner","activity","addClass","actionarea","find","spinner","add_spinner","show","data","dispatch","addSectionSpinner","sectionelement","addSectionLightbox","lightbox","add_lightbox","for","setAttribute","removeSpinner","delay","window","setTimeout","removeClass","hide","mutation","removeLightbox","getAttribute","initActionMenu","elementid","coursebase","invoke_function","core","actionmenu","newDOMNode","one","editModule","moduleElement","cmid","target","action","attr","promises","call","methodname","args","sectionreturn","closest","when","apply","done","mainElement","tabables","isInside","foundElement","elementToFocus","each","contains","this","replaceWith","affectedids","index","elementId","mainelement","selector","is","focus","focusActionItem","push","trigger","Event","ajaxreturn","fail","ex","e","exception","isDefaultPrevented","refreshModule","sectionReturn","activityElement","Promise","resolve","reject","replaceActivityHtmlWith","confirmDeleteModule","onconfirm","modtypename","match","modulename","name","getName","state","_state$cm$get","getModuleName","get_string","pluginname","plugindata","type","get_strings","key","component","param","s","confirm","replaceActionItem","actionitem","image","stringname","stringcomponent","newaction","stringRequests","then","strings","html","renderPix","pixhtml","catch","defaultEditSectionHandler","sectionElement","actionItem","courseformat","sectionid","setSectionBadge","modules","i","section_availability","first","section","oldmarker","oldActionItem","activityHTML","focusedPath","document","getElementById","activeElement","querySelector","getActivityFocusedElement","editSection","supportComponents","includes","dataencoded","parseJSON","badgetype","add","removeOther","sectionbadges","badge","querySelectorAll","forEach","b","classList","remove","register_module","set_visibility_resource_ui","getDOMNode","updateMovedCmState","params","updateMovedSectionState","addMutations","legacyActivityAction","statemanager","setReadOnly","locked","cmlist","reduce","current","delete","legacySectionAction","initCoursePage","on","keyCode","moduleId","preventDefault","sectionId","create","text","isExecuted","message","itemid","modalTitle","newSections","strNumberSections","modalBody","body","title","modal","numSections","getBody","addSections","parseInt","val","location","setSaveButtonText","getRoot","shown","select","enter","save","replaceSectionActionItem","debug","refreshSection","newSectionElement","content","sectionRefreshed","defaultPrevented"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"actions.min.js","sources":["../src/actions.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 * Various actions on modules and sections in the editing mode - hiding, duplicating, deleting, etc.\n *\n * @module core_course/actions\n * @copyright 2016 Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.3\n */\ndefine(\n [\n 'jquery',\n 'core/ajax',\n 'core/templates',\n 'core/notification',\n 'core/str',\n 'core/url',\n 'core/yui',\n 'core/modal_copy_to_clipboard',\n 'core/modal_save_cancel',\n 'core/modal_events',\n 'core/key_codes',\n 'core/log',\n 'core_courseformat/courseeditor',\n 'core/event_dispatcher',\n 'core_course/events'\n ],\n function(\n $,\n ajax,\n templates,\n notification,\n str,\n url,\n Y,\n ModalCopyToClipboard,\n ModalSaveCancel,\n ModalEvents,\n KeyCodes,\n log,\n editor,\n EventDispatcher,\n CourseEvents\n ) {\n\n // Eventually, core_courseformat/local/content/actions will handle all actions for\n // component compatible formats and the default actions.js won't be necessary anymore.\n // Meanwhile, we filter the migrated actions.\n const componentActions = [\n 'moveSection', 'moveCm', 'addSection', 'deleteSection', 'cmDelete', 'cmDuplicate', 'sectionHide', 'sectionShow',\n 'cmHide', 'cmShow', 'cmStealth', 'sectionHighlight', 'sectionUnhighlight', 'cmMoveRight', 'cmMoveLeft',\n 'cmNoGroups', 'cmVisibleGroups', 'cmSeparateGroups',\n ];\n\n // The course reactive instance.\n const courseeditor = editor.getCurrentCourseEditor();\n\n // The current course format name (loaded on init).\n let formatname;\n\n var CSS = {\n EDITINPROGRESS: 'editinprogress',\n SECTIONDRAGGABLE: 'sectiondraggable',\n EDITINGMOVE: 'editing_move'\n };\n var SELECTOR = {\n ACTIVITYLI: 'li.activity',\n ACTIONAREA: '.actions',\n ACTIVITYACTION: 'a.cm-edit-action',\n MENU: '.moodle-actionmenu[data-enhance=moodle-core-actionmenu]',\n TOGGLE: '.toggle-display,.dropdown-toggle',\n SECTIONLI: 'li.section',\n SECTIONACTIONMENU: '.section_action_menu',\n SECTIONACTIONMENUTRIGGER: '.section-actions',\n SECTIONITEM: '[data-for=\"section_title\"]',\n ADDSECTIONS: '.changenumsections [data-add-sections]',\n SECTIONBADGES: '[data-region=\"sectionbadges\"]',\n };\n\n Y.use('moodle-course-coursebase', function() {\n var courseformatselector = M.course.format.get_section_selector();\n if (courseformatselector) {\n SELECTOR.SECTIONLI = courseformatselector;\n }\n });\n\n /**\n * Dispatch event wrapper.\n *\n * Old jQuery events will be replaced by native events gradually.\n *\n * @method dispatchEvent\n * @param {String} eventName The name of the event\n * @param {Object} detail Any additional details to pass into the eveent\n * @param {Node|HTMLElement} container The point at which to dispatch the event\n * @param {Object} options\n * @param {Boolean} options.bubbles Whether to bubble up the DOM\n * @param {Boolean} options.cancelable Whether preventDefault() can be called\n * @param {Boolean} options.composed Whether the event can bubble across the ShadowDOM boundary\n * @returns {CustomEvent}\n */\n const dispatchEvent = function(eventName, detail, container, options) {\n // Most actions still uses jQuery node instead of regular HTMLElement.\n if (!(container instanceof Element) && container.get !== undefined) {\n container = container.get(0);\n }\n return EventDispatcher.dispatchEvent(eventName, detail, container, options);\n };\n\n /**\n * Wrapper for Y.Moodle.core_course.util.cm.getId\n *\n * @param {JQuery} element\n * @returns {Integer}\n */\n var getModuleId = function(element) {\n // Check if we have a data-id first.\n const item = element.get(0);\n if (item.dataset.id) {\n return item.dataset.id;\n }\n // Use YUI way if data-id is not present.\n let id;\n Y.use('moodle-course-util', function(Y) {\n id = Y.Moodle.core_course.util.cm.getId(Y.Node(item));\n });\n return id;\n };\n\n /**\n * Wrapper for Y.Moodle.core_course.util.cm.getName\n *\n * @param {JQuery} element\n * @returns {String}\n */\n var getModuleName = function(element) {\n var name;\n Y.use('moodle-course-util', function(Y) {\n name = Y.Moodle.core_course.util.cm.getName(Y.Node(element.get(0)));\n });\n // Check if we have the name in the course state.\n const state = courseeditor.state;\n const cmid = getModuleId(element);\n if (!name && state && cmid) {\n name = state.cm.get(cmid)?.name;\n }\n return name;\n };\n\n /**\n * Wrapper for M.util.add_spinner for an activity\n *\n * @param {JQuery} activity\n * @returns {Node}\n */\n var addActivitySpinner = function(activity) {\n activity.addClass(CSS.EDITINPROGRESS);\n var actionarea = activity.find(SELECTOR.ACTIONAREA).get(0);\n if (actionarea) {\n var spinner = M.util.add_spinner(Y, Y.Node(actionarea));\n spinner.show();\n // Lock the activity state element.\n if (activity.data('id') !== undefined) {\n courseeditor.dispatch('cmLock', [activity.data('id')], true);\n }\n return spinner;\n }\n return null;\n };\n\n /**\n * Wrapper for M.util.add_spinner for a section\n *\n * @param {JQuery} sectionelement\n * @returns {Node}\n */\n var addSectionSpinner = function(sectionelement) {\n sectionelement.addClass(CSS.EDITINPROGRESS);\n var actionarea = sectionelement.find(SELECTOR.SECTIONACTIONMENU).get(0);\n if (actionarea) {\n var spinner = M.util.add_spinner(Y, Y.Node(actionarea));\n spinner.show();\n // Lock the section state element.\n if (sectionelement.data('id') !== undefined) {\n courseeditor.dispatch('sectionLock', [sectionelement.data('id')], true);\n }\n return spinner;\n }\n return null;\n };\n\n /**\n * Wrapper for M.util.add_lightbox\n *\n * @param {JQuery} sectionelement\n * @returns {Node}\n */\n var addSectionLightbox = function(sectionelement) {\n const item = sectionelement.get(0);\n var lightbox = M.util.add_lightbox(Y, Y.Node(item));\n if (item.dataset.for == 'section' && item.dataset.id) {\n courseeditor.dispatch('sectionLock', [item.dataset.id], true);\n lightbox.setAttribute('data-state', 'section');\n lightbox.setAttribute('data-state-id', item.dataset.id);\n }\n lightbox.show();\n return lightbox;\n };\n\n /**\n * Removes the spinner element\n *\n * @param {JQuery} element\n * @param {Node} spinner\n * @param {Number} delay\n */\n var removeSpinner = function(element, spinner, delay) {\n window.setTimeout(function() {\n element.removeClass(CSS.EDITINPROGRESS);\n if (spinner) {\n spinner.hide();\n }\n // Unlock the state element.\n if (element.data('id') !== undefined) {\n const mutation = (element.data('for') === 'section') ? 'sectionLock' : 'cmLock';\n courseeditor.dispatch(mutation, [element.data('id')], false);\n }\n }, delay);\n };\n\n /**\n * Removes the lightbox element\n *\n * @param {Node} lightbox lighbox YUI element returned by addSectionLightbox\n * @param {Number} delay\n */\n var removeLightbox = function(lightbox, delay) {\n if (lightbox) {\n window.setTimeout(function() {\n lightbox.hide();\n // Unlock state if necessary.\n if (lightbox.getAttribute('data-state')) {\n courseeditor.dispatch(\n `${lightbox.getAttribute('data-state')}Lock`,\n [lightbox.getAttribute('data-state-id')],\n false\n );\n }\n }, delay);\n }\n };\n\n /**\n * Initialise action menu for the element (section or module)\n *\n * @param {String} elementid CSS id attribute of the element\n */\n var initActionMenu = function(elementid) {\n // Initialise action menu in the new activity.\n Y.use('moodle-course-coursebase', function() {\n M.course.coursebase.invoke_function('setup_for_resource', '#' + elementid);\n });\n if (M.core.actionmenu && M.core.actionmenu.newDOMNode) {\n M.core.actionmenu.newDOMNode(Y.one('#' + elementid));\n }\n };\n\n /**\n * Returns focus to the element that was clicked or \"Edit\" link if element is no longer visible.\n *\n * @param {String} elementId CSS id attribute of the element\n * @param {String} action data-action property of the element that was clicked\n */\n var focusActionItem = function(elementId, action) {\n var mainelement = $('#' + elementId);\n var selector = '[data-action=' + action + ']';\n if (action === 'groupsseparate' || action === 'groupsvisible' || action === 'groupsnone') {\n // New element will have different data-action.\n selector = '[data-action=groupsseparate],[data-action=groupsvisible],[data-action=groupsnone]';\n }\n if (mainelement.find(selector).is(':visible')) {\n mainelement.find(selector).focus();\n } else {\n // Element not visible, focus the \"Edit\" link.\n mainelement.find(SELECTOR.MENU).find(SELECTOR.TOGGLE).focus();\n }\n };\n\n /**\n * Find next
    after the element\n *\n * @param {JQuery} mainElement element that is about to be deleted\n * @returns {JQuery}\n */\n var findNextFocusable = function(mainElement) {\n var tabables = $(\"a:visible\");\n var isInside = false;\n var foundElement = null;\n tabables.each(function() {\n if ($.contains(mainElement[0], this)) {\n isInside = true;\n } else if (isInside) {\n foundElement = this;\n return false; // Returning false in .each() is equivalent to \"break;\" inside the loop in php.\n }\n return true;\n });\n return foundElement;\n };\n\n /**\n * Performs an action on a module (moving, deleting, duplicating, hiding, etc.)\n *\n * @param {JQuery} moduleElement activity element we perform action on\n * @param {Number} cmid\n * @param {JQuery} target the element (menu item) that was clicked\n */\n var editModule = function(moduleElement, cmid, target) {\n var action = target.attr('data-action');\n var spinner = addActivitySpinner(moduleElement);\n var promises = ajax.call([{\n methodname: 'core_course_edit_module',\n args: {id: cmid,\n action: action,\n sectionreturn: target.attr('data-sectionreturn') ? target.attr('data-sectionreturn') : null\n }\n }], true);\n\n var lightbox;\n if (action === 'duplicate') {\n lightbox = addSectionLightbox(target.closest(SELECTOR.SECTIONLI));\n }\n $.when.apply($, promises)\n .done(function(data) {\n var elementToFocus = findNextFocusable(moduleElement);\n moduleElement.replaceWith(data);\n let affectedids = [];\n // Initialise action menu for activity(ies) added as a result of this.\n $('
    ' + data + '
    ').find(SELECTOR.ACTIVITYLI).each(function(index) {\n initActionMenu($(this).attr('id'));\n if (index === 0) {\n focusActionItem($(this).attr('id'), action);\n elementToFocus = null;\n }\n // Save any activity id in cmids.\n affectedids.push(getModuleId($(this)));\n });\n // In case of activity deletion focus the next focusable element.\n if (elementToFocus) {\n elementToFocus.focus();\n }\n // Remove spinner and lightbox with a delay.\n removeSpinner(moduleElement, spinner, 400);\n removeLightbox(lightbox, 400);\n // Trigger event that can be observed by course formats.\n moduleElement.trigger($.Event('coursemoduleedited', {ajaxreturn: data, action: action}));\n\n // Modify cm state.\n courseeditor.dispatch('legacyActivityAction', action, cmid, affectedids);\n\n }).fail(function(ex) {\n // Remove spinner and lightbox.\n removeSpinner(moduleElement, spinner);\n removeLightbox(lightbox);\n // Trigger event that can be observed by course formats.\n var e = $.Event('coursemoduleeditfailed', {exception: ex, action: action});\n moduleElement.trigger(e);\n if (!e.isDefaultPrevented()) {\n notification.exception(ex);\n }\n });\n };\n\n /**\n * Requests html for the module via WS core_course_get_module and updates the module on the course page\n *\n * Used after d&d of the module to another section\n *\n * @param {JQuery|Element} element\n * @param {Number} cmid\n * @param {Number} sectionreturn\n * @return {Promise} the refresh promise\n */\n var refreshModule = function(element, cmid, sectionreturn) {\n\n if (sectionreturn === undefined) {\n sectionreturn = courseeditor.sectionReturn;\n }\n\n const activityElement = $(element);\n var spinner = addActivitySpinner(activityElement);\n var promises = ajax.call([{\n methodname: 'core_course_get_module',\n args: {id: cmid, sectionreturn: sectionreturn}\n }], true);\n\n return new Promise((resolve, reject) => {\n $.when.apply($, promises)\n .done(function(data) {\n removeSpinner(activityElement, spinner, 400);\n replaceActivityHtmlWith(data);\n resolve(data);\n }).fail(function() {\n removeSpinner(activityElement, spinner);\n reject();\n });\n });\n };\n\n /**\n * Requests html for the section via WS core_course_edit_section and updates the section on the course page\n *\n * @param {JQuery|Element} element\n * @param {Number} sectionid\n * @param {Number} sectionreturn\n * @return {Promise} the refresh promise\n */\n var refreshSection = function(element, sectionid, sectionreturn) {\n\n if (sectionreturn === undefined) {\n sectionreturn = courseeditor.sectionReturn;\n }\n\n const sectionElement = $(element);\n const action = 'refresh';\n const promises = ajax.call([{\n methodname: 'core_course_edit_section',\n args: {id: sectionid, action, sectionreturn},\n }], true);\n\n var spinner = addSectionSpinner(sectionElement);\n return new Promise((resolve, reject) => {\n $.when.apply($, promises)\n .done(dataencoded => {\n\n removeSpinner(sectionElement, spinner);\n const data = $.parseJSON(dataencoded);\n\n const newSectionElement = $(data.content);\n sectionElement.replaceWith(newSectionElement);\n\n // Init modules menus.\n $(`${SELECTOR.SECTIONLI}#${sectionid} ${SELECTOR.ACTIVITYLI}`).each(\n (index, activity) => {\n initActionMenu(activity.data('id'));\n }\n );\n\n // Trigger event that can be observed by course formats.\n const event = dispatchEvent(\n CourseEvents.sectionRefreshed,\n {\n ajaxreturn: data,\n action: action,\n newSectionElement: newSectionElement.get(0),\n },\n newSectionElement\n );\n\n if (!event.defaultPrevented) {\n defaultEditSectionHandler(\n newSectionElement, $(SELECTOR.SECTIONLI + '#' + sectionid),\n data,\n formatname,\n sectionid\n );\n }\n resolve(data);\n }).fail(ex => {\n // Trigger event that can be observed by course formats.\n const event = dispatchEvent(\n 'coursesectionrefreshfailed',\n {exception: ex, action: action},\n sectionElement\n );\n if (!event.defaultPrevented) {\n notification.exception(ex);\n }\n reject();\n });\n });\n };\n\n /**\n * Displays the delete confirmation to delete a module\n *\n * @param {JQuery} mainelement activity element we perform action on\n * @param {function} onconfirm function to execute on confirm\n */\n var confirmDeleteModule = function(mainelement, onconfirm) {\n var modtypename = mainelement.attr('class').match(/modtype_([^\\s]*)/)[1];\n var modulename = getModuleName(mainelement);\n\n str.get_string('pluginname', modtypename).done(function(pluginname) {\n var plugindata = {\n type: pluginname,\n name: modulename\n };\n str.get_strings([\n {key: 'confirm', component: 'core'},\n {key: modulename === null ? 'deletechecktype' : 'deletechecktypename', param: plugindata},\n {key: 'yes'},\n {key: 'no'}\n ]).done(function(s) {\n notification.confirm(s[0], s[1], s[2], s[3], onconfirm);\n }\n );\n });\n };\n\n /**\n * Displays the delete confirmation to delete a section\n *\n * @param {String} message confirmation message\n * @param {function} onconfirm function to execute on confirm\n */\n var confirmEditSection = function(message, onconfirm) {\n str.get_strings([\n {key: 'confirm'}, // TODO link text\n {key: 'yes'},\n {key: 'no'}\n ]).done(function(s) {\n notification.confirm(s[0], message, s[1], s[2], onconfirm);\n }\n );\n };\n\n /**\n * Replaces an action menu item with another one (for example Show->Hide, Set marker->Remove marker)\n *\n * @param {JQuery} actionitem\n * @param {String} image new image name (\"i/show\", \"i/hide\", etc.)\n * @param {String} stringname new string for the action menu item\n * @param {String} stringcomponent\n * @param {String} newaction new value for data-action attribute of the link\n * @return {Promise} promise which is resolved when the replacement has completed\n */\n var replaceActionItem = function(actionitem, image, stringname,\n stringcomponent, newaction) {\n\n var stringRequests = [{key: stringname, component: stringcomponent}];\n // Do not provide an icon with duplicate, different text to the menu item.\n\n return str.get_strings(stringRequests).then(function(strings) {\n actionitem.find('span.menu-action-text').html(strings[0]);\n\n return templates.renderPix(image, 'core');\n }).then(function(pixhtml) {\n actionitem.find('.icon').replaceWith(pixhtml);\n actionitem.attr('data-action', newaction);\n return;\n }).catch(notification.exception);\n };\n\n /**\n * Default post-processing for section AJAX edit actions.\n *\n * This can be overridden in course formats by listening to event coursesectionedited:\n *\n * $('body').on('coursesectionedited', 'li.section', function(e) {\n * var action = e.action,\n * sectionElement = $(e.target),\n * data = e.ajaxreturn;\n * // ... Do some processing here.\n * e.preventDefault(); // Prevent default handler.\n * });\n *\n * @param {JQuery} sectionElement\n * @param {JQuery} actionItem\n * @param {Object} data\n * @param {String} courseformat\n * @param {Number} sectionid\n */\n var defaultEditSectionHandler = function(sectionElement, actionItem, data, courseformat, sectionid) {\n var action = actionItem.attr('data-action');\n if (action === 'hide' || action === 'show') {\n if (action === 'hide') {\n sectionElement.addClass('hidden');\n setSectionBadge(sectionElement[0], 'hiddenfromstudents', true, false);\n replaceActionItem(actionItem, 'i/show',\n 'showfromothers', 'format_' + courseformat, 'show');\n } else {\n setSectionBadge(sectionElement[0], 'hiddenfromstudents', false, false);\n sectionElement.removeClass('hidden');\n replaceActionItem(actionItem, 'i/hide',\n 'hidefromothers', 'format_' + courseformat, 'hide');\n }\n // Replace the modules with new html (that indicates that they are now hidden or not hidden).\n if (data.modules !== undefined) {\n for (var i in data.modules) {\n replaceActivityHtmlWith(data.modules[i]);\n }\n }\n // Replace the section availability information.\n if (data.section_availability !== undefined) {\n sectionElement.find('.section_availability').first().replaceWith(data.section_availability);\n }\n // Modify course state.\n const section = courseeditor.state.section.get(sectionid);\n if (section !== undefined) {\n courseeditor.dispatch('sectionState', [sectionid]);\n }\n } else if (action === 'setmarker') {\n var oldmarker = $(SELECTOR.SECTIONLI + '.current'),\n oldActionItem = oldmarker.find(SELECTOR.SECTIONACTIONMENU + ' ' + 'a[data-action=removemarker]');\n oldmarker.removeClass('current');\n replaceActionItem(oldActionItem, 'i/marker',\n 'highlight', 'core', 'setmarker');\n sectionElement.addClass('current');\n replaceActionItem(actionItem, 'i/marked',\n 'highlightoff', 'core', 'removemarker');\n courseeditor.dispatch('legacySectionAction', action, sectionid);\n setSectionBadge(sectionElement[0], 'iscurrent', true, true);\n } else if (action === 'removemarker') {\n sectionElement.removeClass('current');\n replaceActionItem(actionItem, 'i/marker',\n 'highlight', 'core', 'setmarker');\n courseeditor.dispatch('legacySectionAction', action, sectionid);\n setSectionBadge(sectionElement[0], 'iscurrent', false, true);\n }\n };\n\n /**\n * Get the focused element path in an activity if any.\n *\n * This method is used to restore focus when the activity HTML is refreshed.\n * Only the main course editor elements can be refocused as they are always present\n * even if the activity content changes.\n *\n * @param {String} id the element id the activity element\n * @return {String|undefined} the inner path of the focused element or undefined\n */\n const getActivityFocusedElement = function(id) {\n const element = document.getElementById(id);\n if (!element || !element.contains(document.activeElement)) {\n return undefined;\n }\n // Check if the actions menu toggler is focused.\n if (element.querySelector(SELECTOR.ACTIONAREA).contains(document.activeElement)) {\n return `${SELECTOR.ACTIONAREA} [tabindex=\"0\"]`;\n }\n // Return the current element id if any.\n if (document.activeElement.id) {\n return `#${document.activeElement.id}`;\n }\n return undefined;\n };\n\n /**\n * Replaces the course module with the new html (used to update module after it was edited or its visibility was changed).\n *\n * @param {String} activityHTML\n */\n var replaceActivityHtmlWith = function(activityHTML) {\n $('
    ' + activityHTML + '
    ').find(SELECTOR.ACTIVITYLI).each(function() {\n // Extract id from the new activity html.\n var id = $(this).attr('id');\n // Check if the current focused element is inside the activity.\n let focusedPath = getActivityFocusedElement(id);\n // Find the existing element with the same id and replace its contents with new html.\n $(SELECTOR.ACTIVITYLI + '#' + id).replaceWith(activityHTML);\n // Initialise action menu.\n initActionMenu(id);\n // Re-focus the previous elements.\n if (focusedPath) {\n const newItem = document.getElementById(id);\n newItem.querySelector(focusedPath)?.focus();\n }\n\n });\n };\n\n /**\n * Performs an action on a module (moving, deleting, duplicating, hiding, etc.)\n *\n * @param {JQuery} sectionElement section element we perform action on\n * @param {Nunmber} sectionid\n * @param {JQuery} target the element (menu item) that was clicked\n * @param {String} courseformat\n * @return {boolean} true the action call is sent to the server or false if it is ignored.\n */\n var editSection = function(sectionElement, sectionid, target, courseformat) {\n var action = target.attr('data-action'),\n sectionreturn = target.attr('data-sectionreturn') ? target.attr('data-sectionreturn') : null;\n\n // Filter direct component handled actions.\n if (courseeditor.supportComponents && componentActions.includes(action)) {\n return false;\n }\n\n var spinner = addSectionSpinner(sectionElement);\n var promises = ajax.call([{\n methodname: 'core_course_edit_section',\n args: {id: sectionid, action: action, sectionreturn: sectionreturn}\n }], true);\n\n var lightbox = addSectionLightbox(sectionElement);\n $.when.apply($, promises)\n .done(function(dataencoded) {\n var data = $.parseJSON(dataencoded);\n removeSpinner(sectionElement, spinner);\n removeLightbox(lightbox);\n sectionElement.find(SELECTOR.SECTIONACTIONMENU).find(SELECTOR.TOGGLE).focus();\n // Trigger event that can be observed by course formats.\n var e = $.Event('coursesectionedited', {ajaxreturn: data, action: action});\n sectionElement.trigger(e);\n if (!e.isDefaultPrevented()) {\n defaultEditSectionHandler(sectionElement, target, data, courseformat, sectionid);\n }\n }).fail(function(ex) {\n // Remove spinner and lightbox.\n removeSpinner(sectionElement, spinner);\n removeLightbox(lightbox);\n // Trigger event that can be observed by course formats.\n var e = $.Event('coursesectioneditfailed', {exception: ex, action: action});\n sectionElement.trigger(e);\n if (!e.isDefaultPrevented()) {\n notification.exception(ex);\n }\n });\n return true;\n };\n\n /**\n * Sets the section badge in the section header.\n *\n * @param {JQuery} sectionElement section element we perform action on\n * @param {String} badgetype the type of badge this is for\n * @param {bool} add true to add, false to remove\n * @param {boolean} removeOther in case of adding a badge, whether to remove all other.\n */\n var setSectionBadge = function(sectionElement, badgetype, add, removeOther) {\n const sectionbadges = sectionElement.querySelector(SELECTOR.SECTIONBADGES);\n if (!sectionbadges) {\n return;\n }\n const badge = sectionbadges.querySelector('[data-type=\"' + badgetype + '\"]');\n if (!badge) {\n return;\n }\n if (add) {\n if (removeOther) {\n document.querySelectorAll('[data-type=\"' + badgetype + '\"]').forEach((b) => {\n b.classList.add('d-none');\n });\n }\n badge.classList.remove('d-none');\n } else {\n badge.classList.add('d-none');\n }\n };\n\n // Register a function to be executed after D&D of an activity.\n Y.use('moodle-course-coursebase', function() {\n M.course.coursebase.register_module({\n // Ignore camelcase eslint rule for the next line because it is an expected name of the callback.\n // eslint-disable-next-line camelcase\n set_visibility_resource_ui: function(args) {\n var mainelement = $(args.element.getDOMNode());\n var cmid = getModuleId(mainelement);\n if (cmid) {\n var sectionreturn = mainelement.find('.' + CSS.EDITINGMOVE).attr('data-sectionreturn');\n refreshModule(mainelement, cmid, sectionreturn);\n }\n },\n /**\n * Update the course state when some cm is moved via YUI.\n * @param {*} params\n */\n updateMovedCmState: (params) => {\n const state = courseeditor.state;\n\n // Update old section.\n const cm = state.cm.get(params.cmid);\n if (cm !== undefined) {\n courseeditor.dispatch('sectionState', [cm.sectionid]);\n }\n // Update cm state.\n courseeditor.dispatch('cmState', [params.cmid]);\n },\n /**\n * Update the course state when some section is moved via YUI.\n */\n updateMovedSectionState: () => {\n courseeditor.dispatch('courseState');\n },\n });\n });\n\n // From Moodle 4.0 all edit actions are being re-implemented as state mutation.\n // This means all method from this \"actions\" module will be deprecated when all the course\n // interface is migrated to reactive components.\n // Most legacy actions did not provide enough information to regenarate the course so they\n // use the mutations courseState, sectionState and cmState to get the updated state from\n // the server. However, some activity actions where we can prevent an extra webservice\n // call by implementing an adhoc mutation.\n courseeditor.addMutations({\n /**\n * Compatibility function to update Moodle 4.0 course state using legacy actions.\n *\n * This method only updates some actions which does not require to use cmState mutation\n * to get updated data form the server.\n *\n * @param {Object} statemanager the current state in read write mode\n * @param {String} action the performed action\n * @param {Number} cmid the affected course module id\n * @param {Array} affectedids all affected cm ids (for duplicate action)\n */\n legacyActivityAction: function(statemanager, action, cmid, affectedids) {\n\n const state = statemanager.state;\n const cm = state.cm.get(cmid);\n if (cm === undefined) {\n return;\n }\n const section = state.section.get(cm.sectionid);\n if (section === undefined) {\n return;\n }\n\n // Send the element is locked.\n courseeditor.dispatch('cmLock', [cm.id], true);\n\n // Now we do the real mutation.\n statemanager.setReadOnly(false);\n\n // This unlocked will take effect when the read only is restored.\n cm.locked = false;\n\n switch (action) {\n case 'delete':\n // Remove from section.\n section.cmlist = section.cmlist.reduce(\n (cmlist, current) => {\n if (current != cmid) {\n cmlist.push(current);\n }\n return cmlist;\n },\n []\n );\n // Delete form list.\n state.cm.delete(cmid);\n break;\n\n case 'hide':\n case 'show':\n case 'duplicate':\n courseeditor.dispatch('cmState', affectedids);\n break;\n }\n statemanager.setReadOnly(true);\n },\n legacySectionAction: function(statemanager, action, sectionid) {\n\n const state = statemanager.state;\n const section = state.section.get(sectionid);\n if (section === undefined) {\n return;\n }\n\n // Send the element is locked. Reactive events are only triggered when the state\n // read only mode is restored. We want to notify the interface the element is\n // locked so we need to do a quick lock operation before performing the rest\n // of the mutation.\n statemanager.setReadOnly(false);\n section.locked = true;\n statemanager.setReadOnly(true);\n\n // Now we do the real mutation.\n statemanager.setReadOnly(false);\n\n // This locked will take effect when the read only is restored.\n section.locked = false;\n\n switch (action) {\n case 'setmarker':\n // Remove previous marker.\n state.section.forEach((current) => {\n if (current.id != sectionid) {\n current.current = false;\n }\n });\n section.current = true;\n break;\n\n case 'removemarker':\n section.current = false;\n break;\n }\n statemanager.setReadOnly(true);\n },\n });\n\n return /** @alias module:core_course/actions */ {\n\n /**\n * Initialises course page\n *\n * @method init\n * @param {String} courseformat name of the current course format (for fetching strings)\n */\n initCoursePage: function(courseformat) {\n\n formatname = courseformat;\n\n // Add a handler for course module actions.\n $('body').on('click keypress', SELECTOR.ACTIVITYLI + ' ' +\n SELECTOR.ACTIVITYACTION + '[data-action]', function(e) {\n if (e.type === 'keypress' && e.keyCode !== 13) {\n return;\n }\n var actionItem = $(this),\n moduleElement = actionItem.closest(SELECTOR.ACTIVITYLI),\n action = actionItem.attr('data-action'),\n moduleId = getModuleId(moduleElement);\n switch (action) {\n case 'moveleft':\n case 'moveright':\n case 'delete':\n case 'duplicate':\n case 'hide':\n case 'stealth':\n case 'show':\n case 'groupsseparate':\n case 'groupsvisible':\n case 'groupsnone':\n break;\n default:\n // Nothing to do here!\n return;\n }\n if (!moduleId) {\n return;\n }\n e.preventDefault();\n if (action === 'delete') {\n // Deleting requires confirmation.\n confirmDeleteModule(moduleElement, function() {\n editModule(moduleElement, moduleId, actionItem);\n });\n } else {\n editModule(moduleElement, moduleId, actionItem);\n }\n });\n\n // Add a handler for section action menu.\n $('body').on('click keypress',\n SELECTOR.SECTIONACTIONMENUTRIGGER + '[data-sectionid] ' +\n 'a[data-action]', function(e) {\n if (e.type === 'keypress' && e.keyCode !== 13) {\n return;\n }\n var actionItem = $(this),\n sectionElement = actionItem.closest(SELECTOR.SECTIONLI),\n sectionId = actionItem.closest(SELECTOR.SECTIONACTIONMENUTRIGGER).attr('data-sectionid');\n\n if (actionItem.attr('data-action') === 'permalink') {\n e.preventDefault();\n ModalCopyToClipboard.create({\n text: actionItem.attr('href'),\n }, str.get_string('sectionlink', 'course')\n );\n return;\n }\n\n let isExecuted = true;\n if (actionItem.attr('data-confirm')) {\n // Action requires confirmation.\n confirmEditSection(actionItem.attr('data-confirm'), function() {\n isExecuted = editSection(sectionElement, sectionId, actionItem, courseformat);\n });\n } else {\n isExecuted = editSection(sectionElement, sectionId, actionItem, courseformat);\n }\n // Prevent any other module from capturing the action if it is already in execution.\n if (isExecuted) {\n e.preventDefault();\n }\n });\n\n // The section and activity names are edited using inplace editable.\n // The \"update\" jQuery event must be captured in order to update the course state.\n $('body').on('updated', `${SELECTOR.SECTIONITEM} [data-inplaceeditable]`, function(e) {\n if (e.ajaxreturn && e.ajaxreturn.itemid) {\n const state = courseeditor.state;\n const section = state.section.get(e.ajaxreturn.itemid);\n if (section !== undefined) {\n courseeditor.dispatch('sectionState', [e.ajaxreturn.itemid]);\n }\n }\n });\n $('body').on('updated', `${SELECTOR.ACTIVITYLI} [data-itemtype=\"activityname\"][data-inplaceeditable]`, function(e) {\n if (e.ajaxreturn && e.ajaxreturn.itemid) {\n courseeditor.dispatch('cmState', [e.ajaxreturn.itemid]);\n }\n });\n\n // Component-based formats don't use modals to create sections.\n if (courseeditor.supportComponents && componentActions.includes('addSection')) {\n return;\n }\n\n // Add a handler for \"Add sections\" link to ask for a number of sections to add.\n const trigger = $(SELECTOR.ADDSECTIONS);\n const modalTitle = trigger.attr('data-add-sections');\n const newSections = trigger.attr('data-new-sections');\n str.get_string('numberweeks')\n .then(function(strNumberSections) {\n var modalBody = $('
    ' +\n '
    ');\n modalBody.find('label').html(strNumberSections);\n\n return modalBody.html();\n })\n .then((body) => ModalSaveCancel.create({\n body,\n title: modalTitle,\n }))\n .then(function(modal) {\n var numSections = $(modal.getBody()).find('#add_section_numsections'),\n addSections = function() {\n // Check if value of the \"Number of sections\" is a valid positive integer and redirect\n // to adding a section script.\n if ('' + parseInt(numSections.val()) === numSections.val() && parseInt(numSections.val()) >= 1) {\n document.location = trigger.attr('href') + '&numsections=' + parseInt(numSections.val());\n }\n };\n modal.setSaveButtonText(modalTitle);\n modal.getRoot().on(ModalEvents.shown, function() {\n // When modal is shown focus and select the input and add a listener to keypress of \"Enter\".\n numSections.focus().select().on('keydown', function(e) {\n if (e.keyCode === KeyCodes.enter) {\n addSections();\n }\n });\n });\n modal.getRoot().on(ModalEvents.save, function(e) {\n // When modal \"Add\" button is pressed.\n e.preventDefault();\n addSections();\n });\n\n trigger.on('click', (e) => {\n e.preventDefault();\n modal.show();\n });\n\n return modal;\n })\n .catch(notification.exception);\n },\n\n /**\n * Replaces a section action menu item with another one (for example Show->Hide, Set marker->Remove marker)\n *\n * This method can be used by course formats in their listener to the coursesectionedited event\n *\n * @deprecated since Moodle 3.9\n * @param {JQuery} sectionelement\n * @param {String} selector CSS selector inside the section element, for example \"a[data-action=show]\"\n * @param {String} image new image name (\"i/show\", \"i/hide\", etc.)\n * @param {String} stringname new string for the action menu item\n * @param {String} stringcomponent\n * @param {String} newaction new value for data-action attribute of the link\n */\n replaceSectionActionItem: function(sectionelement, selector, image, stringname,\n stringcomponent, newaction) {\n log.debug('replaceSectionActionItem() is deprecated and will be removed.');\n var actionitem = sectionelement.find(SELECTOR.SECTIONACTIONMENU + ' ' + selector);\n replaceActionItem(actionitem, image, stringname, stringcomponent, newaction);\n },\n // Method to refresh a module.\n refreshModule,\n refreshSection,\n };\n });\n"],"names":["define","$","ajax","templates","notification","str","url","Y","ModalCopyToClipboard","ModalSaveCancel","ModalEvents","KeyCodes","log","editor","EventDispatcher","CourseEvents","componentActions","courseeditor","getCurrentCourseEditor","formatname","CSS","SELECTOR","ACTIVITYLI","ACTIONAREA","ACTIVITYACTION","MENU","TOGGLE","SECTIONLI","SECTIONACTIONMENU","SECTIONACTIONMENUTRIGGER","SECTIONITEM","ADDSECTIONS","SECTIONBADGES","use","courseformatselector","M","course","format","get_section_selector","dispatchEvent","eventName","detail","container","options","Element","undefined","get","getModuleId","element","item","dataset","id","Moodle","core_course","util","cm","getId","Node","addActivitySpinner","activity","addClass","actionarea","find","spinner","add_spinner","show","data","dispatch","addSectionSpinner","sectionelement","addSectionLightbox","lightbox","add_lightbox","for","setAttribute","removeSpinner","delay","window","setTimeout","removeClass","hide","mutation","removeLightbox","getAttribute","initActionMenu","elementid","coursebase","invoke_function","core","actionmenu","newDOMNode","one","editModule","moduleElement","cmid","target","action","attr","promises","call","methodname","args","sectionreturn","closest","when","apply","done","mainElement","tabables","isInside","foundElement","elementToFocus","each","contains","this","replaceWith","affectedids","index","elementId","mainelement","selector","is","focus","focusActionItem","push","trigger","Event","ajaxreturn","fail","ex","e","exception","isDefaultPrevented","refreshModule","sectionReturn","activityElement","Promise","resolve","reject","replaceActivityHtmlWith","confirmDeleteModule","onconfirm","modtypename","match","modulename","name","getName","state","_state$cm$get","getModuleName","get_string","pluginname","plugindata","type","get_strings","key","component","param","s","confirm","replaceActionItem","actionitem","image","stringname","stringcomponent","newaction","stringRequests","then","strings","html","renderPix","pixhtml","catch","defaultEditSectionHandler","sectionElement","actionItem","courseformat","sectionid","setSectionBadge","modules","i","section_availability","first","section","oldmarker","oldActionItem","activityHTML","focusedPath","document","getElementById","activeElement","querySelector","getActivityFocusedElement","editSection","supportComponents","includes","dataencoded","parseJSON","badgetype","add","removeOther","sectionbadges","badge","querySelectorAll","forEach","b","classList","remove","register_module","set_visibility_resource_ui","getDOMNode","updateMovedCmState","params","updateMovedSectionState","addMutations","legacyActivityAction","statemanager","setReadOnly","locked","cmlist","reduce","current","delete","legacySectionAction","initCoursePage","on","keyCode","moduleId","preventDefault","sectionId","create","text","isExecuted","message","itemid","modalTitle","newSections","strNumberSections","modalBody","body","title","modal","numSections","getBody","addSections","parseInt","val","location","setSaveButtonText","getRoot","shown","select","enter","save","replaceSectionActionItem","debug","refreshSection","newSectionElement","content","sectionRefreshed","defaultPrevented"],"mappings":"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"} \ No newline at end of file diff --git a/course/amd/build/activitychooser.min.js b/course/amd/build/activitychooser.min.js index ee36e31e62d45..c341ef882179b 100644 --- a/course/amd/build/activitychooser.min.js +++ b/course/amd/build/activitychooser.min.js @@ -5,6 +5,6 @@ define("core_course/activitychooser",["exports","core_course/local/activitychoos * @module core_course/activitychooser * @copyright 2020 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,ChooserDialogue=_interopRequireWildcard(ChooserDialogue),Repository=_interopRequireWildcard(Repository),_selectors=_interopRequireDefault(_selectors),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),Templates=_interopRequireWildcard(Templates),_modal=_interopRequireDefault(_modal),_pending=_interopRequireDefault(_pending);let initialized=!1;_exports.init=(courseId,chooserConfig,sectionNum)=>{const pendingPromise=new _pending.default;registerListenerEvents(courseId,chooserConfig,sectionNum),pendingPromise.resolve()};const registerListenerEvents=(courseId,chooserConfig,sectionNum)=>{if(initialized)return;const events=["click",_custom_interaction_events.default.events.activate,_custom_interaction_events.default.events.keyboardActivate],fetchModuleData=(()=>{let innerPromise=null;return()=>(innerPromise||(innerPromise=new Promise((resolve=>{resolve(Repository.activityModules(courseId,sectionNum))}))),innerPromise)})(),fetchFooterData=(()=>{let footerInnerPromise=null;return sectionnum=>(footerInnerPromise||(footerInnerPromise=new Promise((resolve=>{resolve(Repository.fetchFooterData(courseId,sectionnum))}))),footerInnerPromise)})();_custom_interaction_events.default.define(document,events),events.forEach((event=>{document.addEventListener(event,(async e=>{if(e.target.closest(_selectors.default.elements.sectionmodchooser)){let caller,sectionnum;const sectionDiv=e.target.closest(_selectors.default.elements.section),button=e.target.closest(_selectors.default.elements.sectionmodchooser);let bodyPromiseResolver;null!==sectionDiv&§ionDiv.hasAttribute("data-number")?(caller=sectionDiv,sectionnum=sectionDiv.getAttribute("data-number")):(caller=button,caller.hasAttribute("data-sectionid")&&(window.console.warn("The data-sectionid attribute has been deprecated. Please update your code to use data-sectionnum instead."),caller.setAttribute("data-sectionnum",caller.dataset.sectionid)),sectionnum=caller.dataset.sectionnum);const bodyPromise=new Promise((resolve=>{bodyPromiseResolver=resolve})),footerData=await fetchFooterData(sectionnum),sectionModal=buildModal(bodyPromise,footerData),data=await fetchModuleData().catch((async e=>{const errorTemplateData={errormessage:e.message};bodyPromiseResolver(await Templates.render("core_course/local/activitychooser/error",errorTemplateData))}));if(!data)return;const builtModuleData=sectionMapper(data,sectionnum,caller.dataset.sectionreturnnum,caller.dataset.beforemod);ChooserDialogue.displayChooser(sectionModal,builtModuleData,partiallyAppliedFavouriteManager(data,sectionnum),footerData),bodyPromiseResolver(await Templates.render("core_course/activitychooser",templateDataBuilder(builtModuleData,chooserConfig)))}}))})),initialized=!0},sectionMapper=(webServiceData,num,sectionreturnnum,beforemod)=>{const newData=JSON.parse(JSON.stringify(webServiceData));return newData.content_items.forEach((module=>{module.link+="§ion="+num+"&beforemod="+(null!=beforemod?beforemod:0),sectionreturnnum&&(module.link+="&sr="+sectionreturnnum)})),newData.content_items},templateDataBuilder=(data,chooserConfig)=>{let activities=[],resources=[],showAll=!0,showActivities=!1,showResources=!1;const tabMode=parseInt(chooserConfig.tabmode),favourites=data.filter((mod=>!0===mod.favourite)),recommended=data.filter((mod=>!0===mod.recommended));(tabMode=>-1!==[0,3,2,5].indexOf(tabMode))(tabMode)&&(activities=data.filter((mod=>0===mod.archetype)),resources=data.filter((mod=>1===mod.archetype)),showActivities=!0,showResources=!0,2!==tabMode&&5!==tabMode||(showAll=!1));const recommendedBeginning=-1!==[3,4,5].indexOf(tabMode),favouritesFirst=!!favourites.length,recommendedFirst=!1===favouritesFirst&&!0===recommendedBeginning&&!!recommended.length;return{default:data,showAll:showAll,activities:activities,showActivities:showActivities,activitiesFirst:!1===showAll&&!1===favouritesFirst&&!1===recommendedFirst,resources:resources,showResources:showResources,favourites:favourites,recommended:recommended,recommendedFirst:recommendedFirst,recommendedBeginning:recommendedBeginning,favouritesFirst:favouritesFirst,fallback:!0===showAll&&!1===favouritesFirst&&!1===recommendedFirst}},buildModal=(body,footer)=>_modal.default.create({body:body,title:(0,_str.getString)("addresourceoractivity"),footer:footer.customfootertemplate,large:!0,scrollable:!1,templateContext:{classes:"modchooser"},show:!0}),partiallyAppliedFavouriteManager=(moduleData,sectionnum)=>async(internal,favourite,modalBody)=>{const favouriteArea=modalBody.querySelector(_selectors.default.render.favourites),favouriteButtons=modalBody.querySelectorAll('[data-internal="'.concat(internal,'"] ').concat(_selectors.default.actions.optionActions.manageFavourite)),favouriteTabNav=modalBody.querySelector(_selectors.default.regions.favouriteTabNav),result=moduleData.content_items.find((_ref=>{let{name:name}=_ref;return name===internal})),newFaves={};if(result)if(favourite){result.favourite=!0,newFaves.content_items=moduleData.content_items.filter((mod=>!0===mod.favourite));const builtFaves=sectionMapper(newFaves,sectionnum),{html:html,js:js}=await Templates.renderForPromise("core_course/local/activitychooser/favourites",{favourites:builtFaves});await Templates.replaceNodeContents(favouriteArea,html,js),Array.from(favouriteButtons).forEach((element=>{element.classList.remove("text-muted"),element.classList.add("text-primary"),element.dataset.favourited="true",element.setAttribute("aria-pressed",!0),element.firstElementChild.classList.remove("fa-star-o"),element.firstElementChild.classList.add("fa-star")})),favouriteTabNav.classList.remove("d-none")}else{result.favourite=!1;const nodeToRemove=favouriteArea.querySelector('[data-internal="'.concat(internal,'"]'));nodeToRemove.parentNode.removeChild(nodeToRemove),Array.from(favouriteButtons).forEach((element=>{element.classList.add("text-muted"),element.classList.remove("text-primary"),element.dataset.favourited="false",element.setAttribute("aria-pressed",!1),element.firstElementChild.classList.remove("fa-star"),element.firstElementChild.classList.add("fa-star-o")}));0===moduleData.content_items.filter((mod=>!0===mod.favourite)).length&&((favouriteTabNav,modalBody)=>{if(favouriteTabNav.tabIndex=-1,favouriteTabNav.classList.add("d-none"),favouriteTabNav.classList.contains("active")){favouriteTabNav.classList.remove("active"),favouriteTabNav.setAttribute("aria-selected","false"),modalBody.querySelector(_selectors.default.regions.favouriteTab).classList.remove("active");const defaultTabNav=modalBody.querySelector(_selectors.default.regions.defaultTabNav),activitiesTabNav=modalBody.querySelector(_selectors.default.regions.activityTabNav);!1===defaultTabNav.classList.contains("d-none")?(defaultTabNav.classList.add("active"),defaultTabNav.setAttribute("aria-selected","true"),defaultTabNav.tabIndex=0,defaultTabNav.focus(),modalBody.querySelector(_selectors.default.regions.defaultTab).classList.add("active")):(activitiesTabNav.classList.add("active"),activitiesTabNav.setAttribute("aria-selected","true"),activitiesTabNav.tabIndex=0,activitiesTabNav.focus(),modalBody.querySelector(_selectors.default.regions.activityTab).classList.add("active"))}})(favouriteTabNav,modalBody)}}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,ChooserDialogue=_interopRequireWildcard(ChooserDialogue),Repository=_interopRequireWildcard(Repository),_selectors=_interopRequireDefault(_selectors),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),Templates=_interopRequireWildcard(Templates),_modal=_interopRequireDefault(_modal),_pending=_interopRequireDefault(_pending);let initialized=!1;_exports.init=(courseId,chooserConfig)=>{const pendingPromise=new _pending.default;registerListenerEvents(courseId,chooserConfig),pendingPromise.resolve()};const registerListenerEvents=(courseId,chooserConfig)=>{if(initialized)return;const events=["click",_custom_interaction_events.default.events.activate,_custom_interaction_events.default.events.keyboardActivate],fetchModuleData=(()=>{let innerPromises=new Map;return sectionNum=>(innerPromises.has(sectionNum)||innerPromises.set(sectionNum,new Promise((resolve=>{resolve(Repository.activityModules(courseId,sectionNum))}))),innerPromises.get(sectionNum))})(),fetchFooterData=(()=>{let footerInnerPromise=null;return sectionNum=>(footerInnerPromise||(footerInnerPromise=new Promise((resolve=>{resolve(Repository.fetchFooterData(courseId,sectionNum))}))),footerInnerPromise)})();_custom_interaction_events.default.define(document,events),events.forEach((event=>{document.addEventListener(event,(async e=>{if(e.target.closest(_selectors.default.elements.sectionmodchooser)){let caller,sectionnum;const sectionDiv=e.target.closest(_selectors.default.elements.section),button=e.target.closest(_selectors.default.elements.sectionmodchooser);let bodyPromiseResolver;null!==sectionDiv&§ionDiv.hasAttribute("data-number")?(caller=sectionDiv,sectionnum=sectionDiv.getAttribute("data-number")):(caller=button,caller.hasAttribute("data-sectionid")&&(window.console.warn("The data-sectionid attribute has been deprecated. Please update your code to use data-sectionnum instead."),caller.setAttribute("data-sectionnum",caller.dataset.sectionid)),sectionnum=caller.dataset.sectionnum);const bodyPromise=new Promise((resolve=>{bodyPromiseResolver=resolve})),footerData=await fetchFooterData(sectionnum),sectionModal=buildModal(bodyPromise,footerData),data=await fetchModuleData(sectionnum).catch((async e=>{const errorTemplateData={errormessage:e.message};bodyPromiseResolver(await Templates.render("core_course/local/activitychooser/error",errorTemplateData))}));if(!data)return;const builtModuleData=sectionMapper(data,sectionnum,caller.dataset.sectionreturnnum,caller.dataset.beforemod);ChooserDialogue.displayChooser(sectionModal,builtModuleData,partiallyAppliedFavouriteManager(data,sectionnum),footerData),bodyPromiseResolver(await Templates.render("core_course/activitychooser",templateDataBuilder(builtModuleData,chooserConfig)))}}))})),initialized=!0},sectionMapper=(webServiceData,num,sectionreturnnum,beforemod)=>{const newData=JSON.parse(JSON.stringify(webServiceData));return newData.content_items.forEach((module=>{module.link+="§ion="+num+"&beforemod="+(null!=beforemod?beforemod:0),sectionreturnnum&&(module.link+="&sr="+sectionreturnnum)})),newData.content_items},templateDataBuilder=(data,chooserConfig)=>{let activities=[],resources=[],showAll=!0,showActivities=!1,showResources=!1;const tabMode=parseInt(chooserConfig.tabmode),favourites=data.filter((mod=>!0===mod.favourite)),recommended=data.filter((mod=>!0===mod.recommended));(tabMode=>-1!==[0,3,2,5].indexOf(tabMode))(tabMode)&&(activities=data.filter((mod=>0===mod.archetype)),resources=data.filter((mod=>1===mod.archetype)),showActivities=!0,showResources=!0,2!==tabMode&&5!==tabMode||(showAll=!1));const recommendedBeginning=-1!==[3,4,5].indexOf(tabMode),favouritesFirst=!!favourites.length,recommendedFirst=!1===favouritesFirst&&!0===recommendedBeginning&&!!recommended.length;return{default:data,showAll:showAll,activities:activities,showActivities:showActivities,activitiesFirst:!1===showAll&&!1===favouritesFirst&&!1===recommendedFirst,resources:resources,showResources:showResources,favourites:favourites,recommended:recommended,recommendedFirst:recommendedFirst,recommendedBeginning:recommendedBeginning,favouritesFirst:favouritesFirst,fallback:!0===showAll&&!1===favouritesFirst&&!1===recommendedFirst}},buildModal=(body,footer)=>_modal.default.create({body:body,title:(0,_str.getString)("addresourceoractivity"),footer:footer.customfootertemplate,large:!0,scrollable:!1,templateContext:{classes:"modchooser"},show:!0}),partiallyAppliedFavouriteManager=(moduleData,sectionnum)=>async(internal,favourite,modalBody)=>{const favouriteArea=modalBody.querySelector(_selectors.default.render.favourites),favouriteButtons=modalBody.querySelectorAll('[data-internal="'.concat(internal,'"] ').concat(_selectors.default.actions.optionActions.manageFavourite)),favouriteTabNav=modalBody.querySelector(_selectors.default.regions.favouriteTabNav),result=moduleData.content_items.find((_ref=>{let{name:name}=_ref;return name===internal})),newFaves={};if(result)if(favourite){result.favourite=!0,newFaves.content_items=moduleData.content_items.filter((mod=>!0===mod.favourite));const builtFaves=sectionMapper(newFaves,sectionnum),{html:html,js:js}=await Templates.renderForPromise("core_course/local/activitychooser/favourites",{favourites:builtFaves});await Templates.replaceNodeContents(favouriteArea,html,js),Array.from(favouriteButtons).forEach((element=>{element.classList.remove("text-muted"),element.classList.add("text-primary"),element.dataset.favourited="true",element.setAttribute("aria-pressed",!0),element.firstElementChild.classList.remove("fa-star-o"),element.firstElementChild.classList.add("fa-star")})),favouriteTabNav.classList.remove("d-none")}else{result.favourite=!1;const nodeToRemove=favouriteArea.querySelector('[data-internal="'.concat(internal,'"]'));nodeToRemove.parentNode.removeChild(nodeToRemove),Array.from(favouriteButtons).forEach((element=>{element.classList.add("text-muted"),element.classList.remove("text-primary"),element.dataset.favourited="false",element.setAttribute("aria-pressed",!1),element.firstElementChild.classList.remove("fa-star"),element.firstElementChild.classList.add("fa-star-o")}));0===moduleData.content_items.filter((mod=>!0===mod.favourite)).length&&((favouriteTabNav,modalBody)=>{if(favouriteTabNav.tabIndex=-1,favouriteTabNav.classList.add("d-none"),favouriteTabNav.classList.contains("active")){favouriteTabNav.classList.remove("active"),favouriteTabNav.setAttribute("aria-selected","false"),modalBody.querySelector(_selectors.default.regions.favouriteTab).classList.remove("active");const defaultTabNav=modalBody.querySelector(_selectors.default.regions.defaultTabNav),activitiesTabNav=modalBody.querySelector(_selectors.default.regions.activityTabNav);!1===defaultTabNav.classList.contains("d-none")?(defaultTabNav.classList.add("active"),defaultTabNav.setAttribute("aria-selected","true"),defaultTabNav.tabIndex=0,defaultTabNav.focus(),modalBody.querySelector(_selectors.default.regions.defaultTab).classList.add("active")):(activitiesTabNav.classList.add("active"),activitiesTabNav.setAttribute("aria-selected","true"),activitiesTabNav.tabIndex=0,activitiesTabNav.focus(),modalBody.querySelector(_selectors.default.regions.activityTab).classList.add("active"))}})(favouriteTabNav,modalBody)}}})); //# sourceMappingURL=activitychooser.min.js.map \ No newline at end of file diff --git a/course/amd/build/activitychooser.min.js.map b/course/amd/build/activitychooser.min.js.map index 64a897c0c80b7..8bafb5e046376 100644 --- a/course/amd/build/activitychooser.min.js.map +++ b/course/amd/build/activitychooser.min.js.map @@ -1 +1 @@ -{"version":3,"file":"activitychooser.min.js","sources":["../src/activitychooser.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 * A type of dialogue used as for choosing modules in a course.\n *\n * @module core_course/activitychooser\n * @copyright 2020 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as ChooserDialogue from 'core_course/local/activitychooser/dialogue';\nimport * as Repository from 'core_course/local/activitychooser/repository';\nimport selectors from 'core_course/local/activitychooser/selectors';\nimport CustomEvents from 'core/custom_interaction_events';\nimport * as Templates from 'core/templates';\nimport {getString} from 'core/str';\nimport Modal from 'core/modal';\nimport Pending from 'core/pending';\n\n// Set up some JS module wide constants that can be added to in the future.\n\n// Tab config options.\nconst ALLACTIVITIESRESOURCES = 0;\nconst ACTIVITIESRESOURCES = 2;\nconst ALLACTIVITIESRESOURCESREC = 3;\nconst ONLYALLREC = 4;\nconst ACTIVITIESRESOURCESREC = 5;\n\n\n// Module types.\nconst ACTIVITY = 0;\nconst RESOURCE = 1;\n\nlet initialized = false;\n\n/**\n * Set up the activity chooser.\n *\n * @method init\n * @param {Number} courseId Course ID to use later on in fetchModules()\n * @param {Object} chooserConfig Any PHP config settings that we may need to reference\n * @param {Number} sectionNum Section number to use later on in fetchModules()\n */\nexport const init = (courseId, chooserConfig, sectionNum) => {\n const pendingPromise = new Pending();\n\n registerListenerEvents(courseId, chooserConfig, sectionNum);\n\n pendingPromise.resolve();\n};\n\n/**\n * Once a selection has been made make the modal & module information and pass it along\n *\n * @method registerListenerEvents\n * @param {Number} courseId\n * @param {Object} chooserConfig Any PHP config settings that we may need to reference\n * @param {Number} sectionNum Section number to use later on in fetchModules()\n */\nconst registerListenerEvents = (courseId, chooserConfig, sectionNum) => {\n\n // Ensure we only add our listeners once.\n if (initialized) {\n return;\n }\n\n const events = [\n 'click',\n CustomEvents.events.activate,\n CustomEvents.events.keyboardActivate\n ];\n\n const fetchModuleData = (() => {\n let innerPromise = null;\n\n return () => {\n if (!innerPromise) {\n innerPromise = new Promise((resolve) => {\n resolve(Repository.activityModules(courseId, sectionNum));\n });\n }\n\n return innerPromise;\n };\n })();\n\n const fetchFooterData = (() => {\n let footerInnerPromise = null;\n\n return (sectionnum) => {\n if (!footerInnerPromise) {\n footerInnerPromise = new Promise((resolve) => {\n resolve(Repository.fetchFooterData(courseId, sectionnum));\n });\n }\n\n return footerInnerPromise;\n };\n })();\n\n CustomEvents.define(document, events);\n\n // Display module chooser event listeners.\n events.forEach((event) => {\n document.addEventListener(event, async(e) => {\n if (e.target.closest(selectors.elements.sectionmodchooser)) {\n let caller;\n let sectionnum;\n // We need to know who called this.\n // Standard courses use the ID in the main section info.\n const sectionDiv = e.target.closest(selectors.elements.section);\n // Front page courses need some special handling.\n const button = e.target.closest(selectors.elements.sectionmodchooser);\n\n // If we don't have a section number use the fallback ID.\n // We always want the sectionDiv caller first as it keeps track of section number's after DnD changes.\n // The button attribute is always just a fallback for us as the section div is not always available.\n // A YUI change could be done maybe to only update the button attribute but we are going for minimal change here.\n if (sectionDiv !== null && sectionDiv.hasAttribute('data-number')) {\n // We check for attributes just in case of outdated contrib course formats.\n caller = sectionDiv;\n sectionnum = sectionDiv.getAttribute('data-number');\n } else {\n caller = button;\n\n if (caller.hasAttribute('data-sectionid')) {\n window.console.warn(\n 'The data-sectionid attribute has been deprecated. ' +\n 'Please update your code to use data-sectionnum instead.'\n );\n caller.setAttribute('data-sectionnum', caller.dataset.sectionid);\n }\n sectionnum = caller.dataset.sectionnum;\n }\n\n // We want to show the modal instantly but loading whilst waiting for our data.\n let bodyPromiseResolver;\n const bodyPromise = new Promise(resolve => {\n bodyPromiseResolver = resolve;\n });\n\n const footerData = await fetchFooterData(sectionnum);\n const sectionModal = buildModal(bodyPromise, footerData);\n\n // Now we have a modal we should start fetching data.\n // If an error occurs while fetching the data, display the error within the modal.\n const data = await fetchModuleData().catch(async(e) => {\n const errorTemplateData = {\n 'errormessage': e.message\n };\n bodyPromiseResolver(await Templates.render('core_course/local/activitychooser/error', errorTemplateData));\n });\n\n // Early return if there is no module data.\n if (!data) {\n return;\n }\n\n // Apply the section num to all the module instance links.\n const builtModuleData = sectionMapper(\n data,\n sectionnum,\n caller.dataset.sectionreturnnum,\n caller.dataset.beforemod\n );\n\n ChooserDialogue.displayChooser(\n sectionModal,\n builtModuleData,\n partiallyAppliedFavouriteManager(data, sectionnum),\n footerData,\n );\n\n bodyPromiseResolver(await Templates.render(\n 'core_course/activitychooser',\n templateDataBuilder(builtModuleData, chooserConfig)\n ));\n }\n });\n });\n\n initialized = true;\n};\n\n/**\n * Given the web service data and an ID we want to make a deep copy\n * of the WS data then add on the section num to the addoption URL\n *\n * @method sectionMapper\n * @param {Object} webServiceData Our original data from the Web service call\n * @param {Number} num The number of the section we need to append to the links\n * @param {Number|null} sectionreturnnum The number of the section return we need to append to the links\n * @param {Number|null} beforemod The ID of the cm we need to append to the links\n * @return {Array} [modules] with URL's built\n */\nconst sectionMapper = (webServiceData, num, sectionreturnnum, beforemod) => {\n // We need to take a fresh deep copy of the original data as an object is a reference type.\n const newData = JSON.parse(JSON.stringify(webServiceData));\n newData.content_items.forEach((module) => {\n module.link += '§ion=' + num + '&beforemod=' + (beforemod ?? 0);\n if (sectionreturnnum) {\n module.link += '&sr=' + sectionreturnnum;\n }\n });\n return newData.content_items;\n};\n\n/**\n * Given an array of modules we want to figure out where & how to place them into our template object\n *\n * @method templateDataBuilder\n * @param {Array} data our modules to manipulate into a Templatable object\n * @param {Object} chooserConfig Any PHP config settings that we may need to reference\n * @return {Object} Our built object ready to render out\n */\nconst templateDataBuilder = (data, chooserConfig) => {\n // Setup of various bits and pieces we need to mutate before throwing it to the wolves.\n let activities = [];\n let resources = [];\n let showAll = true;\n let showActivities = false;\n let showResources = false;\n\n // Tab mode can be the following [All, Resources & Activities, All & Activities & Resources].\n const tabMode = parseInt(chooserConfig.tabmode);\n\n // Filter the incoming data to find favourite & recommended modules.\n const favourites = data.filter(mod => mod.favourite === true);\n const recommended = data.filter(mod => mod.recommended === true);\n\n // Whether the activities and resources tabs should be displayed or not.\n const showActivitiesAndResources = (tabMode) => {\n const acceptableModes = [\n ALLACTIVITIESRESOURCES,\n ALLACTIVITIESRESOURCESREC,\n ACTIVITIESRESOURCES,\n ACTIVITIESRESOURCESREC,\n ];\n\n return acceptableModes.indexOf(tabMode) !== -1;\n };\n\n // These modes need Activity & Resource tabs.\n if (showActivitiesAndResources(tabMode)) {\n // Filter the incoming data to find activities then resources.\n activities = data.filter(mod => mod.archetype === ACTIVITY);\n resources = data.filter(mod => mod.archetype === RESOURCE);\n showActivities = true;\n showResources = true;\n\n // We want all of the previous information but no 'All' tab.\n if (tabMode === ACTIVITIESRESOURCES || tabMode === ACTIVITIESRESOURCESREC) {\n showAll = false;\n }\n }\n\n const recommendedBeforeTabs = [\n ALLACTIVITIESRESOURCESREC,\n ONLYALLREC,\n ACTIVITIESRESOURCESREC,\n ];\n // Whether the recommended tab should be displayed before the All/Activities/Resources tabs.\n const recommendedBeginning = recommendedBeforeTabs.indexOf(tabMode) !== -1;\n\n // Given the results of the above filters lets figure out what tab to set active.\n // We have some favourites.\n const favouritesFirst = !!favourites.length;\n const recommendedFirst = favouritesFirst === false && recommendedBeginning === true && !!recommended.length;\n // We are in tabMode 2 without any favourites.\n const activitiesFirst = showAll === false && favouritesFirst === false && recommendedFirst === false;\n // We have nothing fallback to show all modules.\n const fallback = showAll === true && favouritesFirst === false && recommendedFirst === false;\n\n return {\n 'default': data,\n showAll: showAll,\n activities: activities,\n showActivities: showActivities,\n activitiesFirst: activitiesFirst,\n resources: resources,\n showResources: showResources,\n favourites: favourites,\n recommended: recommended,\n recommendedFirst: recommendedFirst,\n recommendedBeginning: recommendedBeginning,\n favouritesFirst: favouritesFirst,\n fallback: fallback,\n };\n};\n\n/**\n * Given an object we want to build a modal ready to show\n *\n * @method buildModal\n * @param {Promise} body\n * @param {String|Boolean} footer Either a footer to add or nothing\n * @return {Object} The modal ready to display immediately and render body in later.\n */\nconst buildModal = (body, footer) => Modal.create({\n body,\n title: getString('addresourceoractivity'),\n footer: footer.customfootertemplate,\n large: true,\n scrollable: false,\n templateContext: {\n classes: 'modchooser'\n },\n show: true,\n});\n\n/**\n * A small helper function to handle the case where there are no more favourites\n * and we need to mess a bit with the available tabs in the chooser\n *\n * @method nullFavouriteDomManager\n * @param {HTMLElement} favouriteTabNav Dom node of the favourite tab nav\n * @param {HTMLElement} modalBody Our current modals' body\n */\nconst nullFavouriteDomManager = (favouriteTabNav, modalBody) => {\n favouriteTabNav.tabIndex = -1;\n favouriteTabNav.classList.add('d-none');\n // Need to set active to an available tab.\n if (favouriteTabNav.classList.contains('active')) {\n favouriteTabNav.classList.remove('active');\n favouriteTabNav.setAttribute('aria-selected', 'false');\n const favouriteTab = modalBody.querySelector(selectors.regions.favouriteTab);\n favouriteTab.classList.remove('active');\n const defaultTabNav = modalBody.querySelector(selectors.regions.defaultTabNav);\n const activitiesTabNav = modalBody.querySelector(selectors.regions.activityTabNav);\n if (defaultTabNav.classList.contains('d-none') === false) {\n defaultTabNav.classList.add('active');\n defaultTabNav.setAttribute('aria-selected', 'true');\n defaultTabNav.tabIndex = 0;\n defaultTabNav.focus();\n const defaultTab = modalBody.querySelector(selectors.regions.defaultTab);\n defaultTab.classList.add('active');\n } else {\n activitiesTabNav.classList.add('active');\n activitiesTabNav.setAttribute('aria-selected', 'true');\n activitiesTabNav.tabIndex = 0;\n activitiesTabNav.focus();\n const activitiesTab = modalBody.querySelector(selectors.regions.activityTab);\n activitiesTab.classList.add('active');\n }\n\n }\n};\n\n/**\n * Export a curried function where the builtModules has been applied.\n * We have our array of modules so we can rerender the favourites area and have all of the items sorted.\n *\n * @method partiallyAppliedFavouriteManager\n * @param {Array} moduleData This is our raw WS data that we need to manipulate\n * @param {Number} sectionnum We need this to add the sectionnum to the URL's in the faves area after rerender\n * @return {Function} partially applied function so we can manipulate DOM nodes easily & update our internal array\n */\nconst partiallyAppliedFavouriteManager = (moduleData, sectionnum) => {\n /**\n * Curried function that is being returned.\n *\n * @param {String} internal Internal name of the module to manage\n * @param {Boolean} favourite Is the caller adding a favourite or removing one?\n * @param {HTMLElement} modalBody What we need to update whilst we are here\n */\n return async(internal, favourite, modalBody) => {\n const favouriteArea = modalBody.querySelector(selectors.render.favourites);\n\n // eslint-disable-next-line max-len\n const favouriteButtons = modalBody.querySelectorAll(`[data-internal=\"${internal}\"] ${selectors.actions.optionActions.manageFavourite}`);\n const favouriteTabNav = modalBody.querySelector(selectors.regions.favouriteTabNav);\n const result = moduleData.content_items.find(({name}) => name === internal);\n const newFaves = {};\n if (result) {\n if (favourite) {\n result.favourite = true;\n\n // eslint-disable-next-line camelcase\n newFaves.content_items = moduleData.content_items.filter(mod => mod.favourite === true);\n\n const builtFaves = sectionMapper(newFaves, sectionnum);\n\n const {html, js} = await Templates.renderForPromise('core_course/local/activitychooser/favourites',\n {favourites: builtFaves});\n\n await Templates.replaceNodeContents(favouriteArea, html, js);\n\n Array.from(favouriteButtons).forEach((element) => {\n element.classList.remove('text-muted');\n element.classList.add('text-primary');\n element.dataset.favourited = 'true';\n element.setAttribute('aria-pressed', true);\n element.firstElementChild.classList.remove('fa-star-o');\n element.firstElementChild.classList.add('fa-star');\n });\n\n favouriteTabNav.classList.remove('d-none');\n } else {\n result.favourite = false;\n\n const nodeToRemove = favouriteArea.querySelector(`[data-internal=\"${internal}\"]`);\n\n nodeToRemove.parentNode.removeChild(nodeToRemove);\n\n Array.from(favouriteButtons).forEach((element) => {\n element.classList.add('text-muted');\n element.classList.remove('text-primary');\n element.dataset.favourited = 'false';\n element.setAttribute('aria-pressed', false);\n element.firstElementChild.classList.remove('fa-star');\n element.firstElementChild.classList.add('fa-star-o');\n });\n const newFaves = moduleData.content_items.filter(mod => mod.favourite === true);\n\n if (newFaves.length === 0) {\n nullFavouriteDomManager(favouriteTabNav, modalBody);\n }\n }\n }\n };\n};\n"],"names":["initialized","courseId","chooserConfig","sectionNum","pendingPromise","Pending","registerListenerEvents","resolve","events","CustomEvents","activate","keyboardActivate","fetchModuleData","innerPromise","Promise","Repository","activityModules","fetchFooterData","footerInnerPromise","sectionnum","define","document","forEach","event","addEventListener","async","e","target","closest","selectors","elements","sectionmodchooser","caller","sectionDiv","section","button","bodyPromiseResolver","hasAttribute","getAttribute","window","console","warn","setAttribute","dataset","sectionid","bodyPromise","footerData","sectionModal","buildModal","data","catch","errorTemplateData","message","Templates","render","builtModuleData","sectionMapper","sectionreturnnum","beforemod","ChooserDialogue","displayChooser","partiallyAppliedFavouriteManager","templateDataBuilder","webServiceData","num","newData","JSON","parse","stringify","content_items","module","link","activities","resources","showAll","showActivities","showResources","tabMode","parseInt","tabmode","favourites","filter","mod","favourite","recommended","indexOf","showActivitiesAndResources","archetype","recommendedBeginning","favouritesFirst","length","recommendedFirst","activitiesFirst","fallback","body","footer","Modal","create","title","customfootertemplate","large","scrollable","templateContext","classes","show","moduleData","internal","modalBody","favouriteArea","querySelector","favouriteButtons","querySelectorAll","actions","optionActions","manageFavourite","favouriteTabNav","regions","result","find","_ref","name","newFaves","builtFaves","html","js","renderForPromise","replaceNodeContents","Array","from","element","classList","remove","add","favourited","firstElementChild","nodeToRemove","parentNode","removeChild","tabIndex","contains","favouriteTab","defaultTabNav","activitiesTabNav","activityTabNav","focus","defaultTab","activityTab","nullFavouriteDomManager"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"activitychooser.min.js","sources":["../src/activitychooser.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 * A type of dialogue used as for choosing modules in a course.\n *\n * @module core_course/activitychooser\n * @copyright 2020 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as ChooserDialogue from 'core_course/local/activitychooser/dialogue';\nimport * as Repository from 'core_course/local/activitychooser/repository';\nimport selectors from 'core_course/local/activitychooser/selectors';\nimport CustomEvents from 'core/custom_interaction_events';\nimport * as Templates from 'core/templates';\nimport {getString} from 'core/str';\nimport Modal from 'core/modal';\nimport Pending from 'core/pending';\n\n// Set up some JS module wide constants that can be added to in the future.\n\n// Tab config options.\nconst ALLACTIVITIESRESOURCES = 0;\nconst ACTIVITIESRESOURCES = 2;\nconst ALLACTIVITIESRESOURCESREC = 3;\nconst ONLYALLREC = 4;\nconst ACTIVITIESRESOURCESREC = 5;\n\n\n// Module types.\nconst ACTIVITY = 0;\nconst RESOURCE = 1;\n\nlet initialized = false;\n\n/**\n * Set up the activity chooser.\n *\n * @method init\n * @param {Number} courseId Course ID to use later on in fetchModules()\n * @param {Object} chooserConfig Any PHP config settings that we may need to reference\n */\nexport const init = (courseId, chooserConfig) => {\n const pendingPromise = new Pending();\n\n registerListenerEvents(courseId, chooserConfig);\n\n pendingPromise.resolve();\n};\n\n/**\n * Once a selection has been made make the modal & module information and pass it along\n *\n * @method registerListenerEvents\n * @param {Number} courseId\n * @param {Object} chooserConfig Any PHP config settings that we may need to reference\n */\nconst registerListenerEvents = (courseId, chooserConfig) => {\n\n // Ensure we only add our listeners once.\n if (initialized) {\n return;\n }\n\n const events = [\n 'click',\n CustomEvents.events.activate,\n CustomEvents.events.keyboardActivate\n ];\n\n const fetchModuleData = (() => {\n let innerPromises = new Map();\n\n return (sectionNum) => {\n if (innerPromises.has(sectionNum)) {\n return innerPromises.get(sectionNum);\n }\n\n innerPromises.set(\n sectionNum,\n new Promise((resolve) => {\n resolve(Repository.activityModules(courseId, sectionNum));\n })\n );\n return innerPromises.get(sectionNum);\n };\n })();\n\n const fetchFooterData = (() => {\n let footerInnerPromise = null;\n\n return (sectionNum) => {\n if (!footerInnerPromise) {\n footerInnerPromise = new Promise((resolve) => {\n resolve(Repository.fetchFooterData(courseId, sectionNum));\n });\n }\n\n return footerInnerPromise;\n };\n })();\n\n CustomEvents.define(document, events);\n\n // Display module chooser event listeners.\n events.forEach((event) => {\n document.addEventListener(event, async(e) => {\n if (e.target.closest(selectors.elements.sectionmodchooser)) {\n let caller;\n let sectionnum;\n // We need to know who called this.\n // Standard courses use the ID in the main section info.\n const sectionDiv = e.target.closest(selectors.elements.section);\n // Front page courses need some special handling.\n const button = e.target.closest(selectors.elements.sectionmodchooser);\n\n // If we don't have a section number use the fallback ID.\n // We always want the sectionDiv caller first as it keeps track of section number's after DnD changes.\n // The button attribute is always just a fallback for us as the section div is not always available.\n // A YUI change could be done maybe to only update the button attribute but we are going for minimal change here.\n if (sectionDiv !== null && sectionDiv.hasAttribute('data-number')) {\n // We check for attributes just in case of outdated contrib course formats.\n caller = sectionDiv;\n sectionnum = sectionDiv.getAttribute('data-number');\n } else {\n caller = button;\n\n if (caller.hasAttribute('data-sectionid')) {\n window.console.warn(\n 'The data-sectionid attribute has been deprecated. ' +\n 'Please update your code to use data-sectionnum instead.'\n );\n caller.setAttribute('data-sectionnum', caller.dataset.sectionid);\n }\n sectionnum = caller.dataset.sectionnum;\n }\n\n // We want to show the modal instantly but loading whilst waiting for our data.\n let bodyPromiseResolver;\n const bodyPromise = new Promise(resolve => {\n bodyPromiseResolver = resolve;\n });\n\n const footerData = await fetchFooterData(sectionnum);\n const sectionModal = buildModal(bodyPromise, footerData);\n\n // Now we have a modal we should start fetching data.\n // If an error occurs while fetching the data, display the error within the modal.\n const data = await fetchModuleData(sectionnum).catch(async(e) => {\n const errorTemplateData = {\n 'errormessage': e.message\n };\n bodyPromiseResolver(await Templates.render('core_course/local/activitychooser/error', errorTemplateData));\n });\n\n // Early return if there is no module data.\n if (!data) {\n return;\n }\n\n // Apply the section num to all the module instance links.\n const builtModuleData = sectionMapper(\n data,\n sectionnum,\n caller.dataset.sectionreturnnum,\n caller.dataset.beforemod\n );\n\n ChooserDialogue.displayChooser(\n sectionModal,\n builtModuleData,\n partiallyAppliedFavouriteManager(data, sectionnum),\n footerData,\n );\n\n bodyPromiseResolver(await Templates.render(\n 'core_course/activitychooser',\n templateDataBuilder(builtModuleData, chooserConfig)\n ));\n }\n });\n });\n\n initialized = true;\n};\n\n/**\n * Given the web service data and an ID we want to make a deep copy\n * of the WS data then add on the section num to the addoption URL\n *\n * @method sectionMapper\n * @param {Object} webServiceData Our original data from the Web service call\n * @param {Number} num The number of the section we need to append to the links\n * @param {Number|null} sectionreturnnum The number of the section return we need to append to the links\n * @param {Number|null} beforemod The ID of the cm we need to append to the links\n * @return {Array} [modules] with URL's built\n */\nconst sectionMapper = (webServiceData, num, sectionreturnnum, beforemod) => {\n // We need to take a fresh deep copy of the original data as an object is a reference type.\n const newData = JSON.parse(JSON.stringify(webServiceData));\n newData.content_items.forEach((module) => {\n module.link += '§ion=' + num + '&beforemod=' + (beforemod ?? 0);\n if (sectionreturnnum) {\n module.link += '&sr=' + sectionreturnnum;\n }\n });\n return newData.content_items;\n};\n\n/**\n * Given an array of modules we want to figure out where & how to place them into our template object\n *\n * @method templateDataBuilder\n * @param {Array} data our modules to manipulate into a Templatable object\n * @param {Object} chooserConfig Any PHP config settings that we may need to reference\n * @return {Object} Our built object ready to render out\n */\nconst templateDataBuilder = (data, chooserConfig) => {\n // Setup of various bits and pieces we need to mutate before throwing it to the wolves.\n let activities = [];\n let resources = [];\n let showAll = true;\n let showActivities = false;\n let showResources = false;\n\n // Tab mode can be the following [All, Resources & Activities, All & Activities & Resources].\n const tabMode = parseInt(chooserConfig.tabmode);\n\n // Filter the incoming data to find favourite & recommended modules.\n const favourites = data.filter(mod => mod.favourite === true);\n const recommended = data.filter(mod => mod.recommended === true);\n\n // Whether the activities and resources tabs should be displayed or not.\n const showActivitiesAndResources = (tabMode) => {\n const acceptableModes = [\n ALLACTIVITIESRESOURCES,\n ALLACTIVITIESRESOURCESREC,\n ACTIVITIESRESOURCES,\n ACTIVITIESRESOURCESREC,\n ];\n\n return acceptableModes.indexOf(tabMode) !== -1;\n };\n\n // These modes need Activity & Resource tabs.\n if (showActivitiesAndResources(tabMode)) {\n // Filter the incoming data to find activities then resources.\n activities = data.filter(mod => mod.archetype === ACTIVITY);\n resources = data.filter(mod => mod.archetype === RESOURCE);\n showActivities = true;\n showResources = true;\n\n // We want all of the previous information but no 'All' tab.\n if (tabMode === ACTIVITIESRESOURCES || tabMode === ACTIVITIESRESOURCESREC) {\n showAll = false;\n }\n }\n\n const recommendedBeforeTabs = [\n ALLACTIVITIESRESOURCESREC,\n ONLYALLREC,\n ACTIVITIESRESOURCESREC,\n ];\n // Whether the recommended tab should be displayed before the All/Activities/Resources tabs.\n const recommendedBeginning = recommendedBeforeTabs.indexOf(tabMode) !== -1;\n\n // Given the results of the above filters lets figure out what tab to set active.\n // We have some favourites.\n const favouritesFirst = !!favourites.length;\n const recommendedFirst = favouritesFirst === false && recommendedBeginning === true && !!recommended.length;\n // We are in tabMode 2 without any favourites.\n const activitiesFirst = showAll === false && favouritesFirst === false && recommendedFirst === false;\n // We have nothing fallback to show all modules.\n const fallback = showAll === true && favouritesFirst === false && recommendedFirst === false;\n\n return {\n 'default': data,\n showAll: showAll,\n activities: activities,\n showActivities: showActivities,\n activitiesFirst: activitiesFirst,\n resources: resources,\n showResources: showResources,\n favourites: favourites,\n recommended: recommended,\n recommendedFirst: recommendedFirst,\n recommendedBeginning: recommendedBeginning,\n favouritesFirst: favouritesFirst,\n fallback: fallback,\n };\n};\n\n/**\n * Given an object we want to build a modal ready to show\n *\n * @method buildModal\n * @param {Promise} body\n * @param {String|Boolean} footer Either a footer to add or nothing\n * @return {Object} The modal ready to display immediately and render body in later.\n */\nconst buildModal = (body, footer) => Modal.create({\n body,\n title: getString('addresourceoractivity'),\n footer: footer.customfootertemplate,\n large: true,\n scrollable: false,\n templateContext: {\n classes: 'modchooser'\n },\n show: true,\n});\n\n/**\n * A small helper function to handle the case where there are no more favourites\n * and we need to mess a bit with the available tabs in the chooser\n *\n * @method nullFavouriteDomManager\n * @param {HTMLElement} favouriteTabNav Dom node of the favourite tab nav\n * @param {HTMLElement} modalBody Our current modals' body\n */\nconst nullFavouriteDomManager = (favouriteTabNav, modalBody) => {\n favouriteTabNav.tabIndex = -1;\n favouriteTabNav.classList.add('d-none');\n // Need to set active to an available tab.\n if (favouriteTabNav.classList.contains('active')) {\n favouriteTabNav.classList.remove('active');\n favouriteTabNav.setAttribute('aria-selected', 'false');\n const favouriteTab = modalBody.querySelector(selectors.regions.favouriteTab);\n favouriteTab.classList.remove('active');\n const defaultTabNav = modalBody.querySelector(selectors.regions.defaultTabNav);\n const activitiesTabNav = modalBody.querySelector(selectors.regions.activityTabNav);\n if (defaultTabNav.classList.contains('d-none') === false) {\n defaultTabNav.classList.add('active');\n defaultTabNav.setAttribute('aria-selected', 'true');\n defaultTabNav.tabIndex = 0;\n defaultTabNav.focus();\n const defaultTab = modalBody.querySelector(selectors.regions.defaultTab);\n defaultTab.classList.add('active');\n } else {\n activitiesTabNav.classList.add('active');\n activitiesTabNav.setAttribute('aria-selected', 'true');\n activitiesTabNav.tabIndex = 0;\n activitiesTabNav.focus();\n const activitiesTab = modalBody.querySelector(selectors.regions.activityTab);\n activitiesTab.classList.add('active');\n }\n\n }\n};\n\n/**\n * Export a curried function where the builtModules has been applied.\n * We have our array of modules so we can rerender the favourites area and have all of the items sorted.\n *\n * @method partiallyAppliedFavouriteManager\n * @param {Array} moduleData This is our raw WS data that we need to manipulate\n * @param {Number} sectionnum We need this to add the sectionnum to the URL's in the faves area after rerender\n * @return {Function} partially applied function so we can manipulate DOM nodes easily & update our internal array\n */\nconst partiallyAppliedFavouriteManager = (moduleData, sectionnum) => {\n /**\n * Curried function that is being returned.\n *\n * @param {String} internal Internal name of the module to manage\n * @param {Boolean} favourite Is the caller adding a favourite or removing one?\n * @param {HTMLElement} modalBody What we need to update whilst we are here\n */\n return async(internal, favourite, modalBody) => {\n const favouriteArea = modalBody.querySelector(selectors.render.favourites);\n\n // eslint-disable-next-line max-len\n const favouriteButtons = modalBody.querySelectorAll(`[data-internal=\"${internal}\"] ${selectors.actions.optionActions.manageFavourite}`);\n const favouriteTabNav = modalBody.querySelector(selectors.regions.favouriteTabNav);\n const result = moduleData.content_items.find(({name}) => name === internal);\n const newFaves = {};\n if (result) {\n if (favourite) {\n result.favourite = true;\n\n // eslint-disable-next-line camelcase\n newFaves.content_items = moduleData.content_items.filter(mod => mod.favourite === true);\n\n const builtFaves = sectionMapper(newFaves, sectionnum);\n\n const {html, js} = await Templates.renderForPromise('core_course/local/activitychooser/favourites',\n {favourites: builtFaves});\n\n await Templates.replaceNodeContents(favouriteArea, html, js);\n\n Array.from(favouriteButtons).forEach((element) => {\n element.classList.remove('text-muted');\n element.classList.add('text-primary');\n element.dataset.favourited = 'true';\n element.setAttribute('aria-pressed', true);\n element.firstElementChild.classList.remove('fa-star-o');\n element.firstElementChild.classList.add('fa-star');\n });\n\n favouriteTabNav.classList.remove('d-none');\n } else {\n result.favourite = false;\n\n const nodeToRemove = favouriteArea.querySelector(`[data-internal=\"${internal}\"]`);\n\n nodeToRemove.parentNode.removeChild(nodeToRemove);\n\n Array.from(favouriteButtons).forEach((element) => {\n element.classList.add('text-muted');\n element.classList.remove('text-primary');\n element.dataset.favourited = 'false';\n element.setAttribute('aria-pressed', false);\n element.firstElementChild.classList.remove('fa-star');\n element.firstElementChild.classList.add('fa-star-o');\n });\n const newFaves = moduleData.content_items.filter(mod => mod.favourite === true);\n\n if (newFaves.length === 0) {\n nullFavouriteDomManager(favouriteTabNav, modalBody);\n }\n }\n }\n };\n};\n"],"names":["initialized","courseId","chooserConfig","pendingPromise","Pending","registerListenerEvents","resolve","events","CustomEvents","activate","keyboardActivate","fetchModuleData","innerPromises","Map","sectionNum","has","set","Promise","Repository","activityModules","get","fetchFooterData","footerInnerPromise","define","document","forEach","event","addEventListener","async","e","target","closest","selectors","elements","sectionmodchooser","caller","sectionnum","sectionDiv","section","button","bodyPromiseResolver","hasAttribute","getAttribute","window","console","warn","setAttribute","dataset","sectionid","bodyPromise","footerData","sectionModal","buildModal","data","catch","errorTemplateData","message","Templates","render","builtModuleData","sectionMapper","sectionreturnnum","beforemod","ChooserDialogue","displayChooser","partiallyAppliedFavouriteManager","templateDataBuilder","webServiceData","num","newData","JSON","parse","stringify","content_items","module","link","activities","resources","showAll","showActivities","showResources","tabMode","parseInt","tabmode","favourites","filter","mod","favourite","recommended","indexOf","showActivitiesAndResources","archetype","recommendedBeginning","favouritesFirst","length","recommendedFirst","activitiesFirst","fallback","body","footer","Modal","create","title","customfootertemplate","large","scrollable","templateContext","classes","show","moduleData","internal","modalBody","favouriteArea","querySelector","favouriteButtons","querySelectorAll","actions","optionActions","manageFavourite","favouriteTabNav","regions","result","find","_ref","name","newFaves","builtFaves","html","js","renderForPromise","replaceNodeContents","Array","from","element","classList","remove","add","favourited","firstElementChild","nodeToRemove","parentNode","removeChild","tabIndex","contains","favouriteTab","defaultTabNav","activitiesTabNav","activityTabNav","focus","defaultTab","activityTab","nullFavouriteDomManager"],"mappings":"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"} \ No newline at end of file diff --git a/course/amd/src/actions.js b/course/amd/src/actions.js index a00bf23daf1aa..7a6b77bd13df1 100644 --- a/course/amd/src/actions.js +++ b/course/amd/src/actions.js @@ -1004,7 +1004,7 @@ define( } } }); - $('body').on('updated', `${SELECTOR.ACTIVITYLI} [data-inplaceeditable]`, function(e) { + $('body').on('updated', `${SELECTOR.ACTIVITYLI} [data-itemtype="activityname"][data-inplaceeditable]`, function(e) { if (e.ajaxreturn && e.ajaxreturn.itemid) { courseeditor.dispatch('cmState', [e.ajaxreturn.itemid]); } diff --git a/course/amd/src/activitychooser.js b/course/amd/src/activitychooser.js index 449831c4fd42e..1bc047ef9f014 100644 --- a/course/amd/src/activitychooser.js +++ b/course/amd/src/activitychooser.js @@ -52,12 +52,11 @@ let initialized = false; * @method init * @param {Number} courseId Course ID to use later on in fetchModules() * @param {Object} chooserConfig Any PHP config settings that we may need to reference - * @param {Number} sectionNum Section number to use later on in fetchModules() */ -export const init = (courseId, chooserConfig, sectionNum) => { +export const init = (courseId, chooserConfig) => { const pendingPromise = new Pending(); - registerListenerEvents(courseId, chooserConfig, sectionNum); + registerListenerEvents(courseId, chooserConfig); pendingPromise.resolve(); }; @@ -68,9 +67,8 @@ export const init = (courseId, chooserConfig, sectionNum) => { * @method registerListenerEvents * @param {Number} courseId * @param {Object} chooserConfig Any PHP config settings that we may need to reference - * @param {Number} sectionNum Section number to use later on in fetchModules() */ -const registerListenerEvents = (courseId, chooserConfig, sectionNum) => { +const registerListenerEvents = (courseId, chooserConfig) => { // Ensure we only add our listeners once. if (initialized) { @@ -84,26 +82,30 @@ const registerListenerEvents = (courseId, chooserConfig, sectionNum) => { ]; const fetchModuleData = (() => { - let innerPromise = null; + let innerPromises = new Map(); - return () => { - if (!innerPromise) { - innerPromise = new Promise((resolve) => { - resolve(Repository.activityModules(courseId, sectionNum)); - }); + return (sectionNum) => { + if (innerPromises.has(sectionNum)) { + return innerPromises.get(sectionNum); } - return innerPromise; + innerPromises.set( + sectionNum, + new Promise((resolve) => { + resolve(Repository.activityModules(courseId, sectionNum)); + }) + ); + return innerPromises.get(sectionNum); }; })(); const fetchFooterData = (() => { let footerInnerPromise = null; - return (sectionnum) => { + return (sectionNum) => { if (!footerInnerPromise) { footerInnerPromise = new Promise((resolve) => { - resolve(Repository.fetchFooterData(courseId, sectionnum)); + resolve(Repository.fetchFooterData(courseId, sectionNum)); }); } @@ -157,7 +159,7 @@ const registerListenerEvents = (courseId, chooserConfig, sectionNum) => { // Now we have a modal we should start fetching data. // If an error occurs while fetching the data, display the error within the modal. - const data = await fetchModuleData().catch(async(e) => { + const data = await fetchModuleData(sectionnum).catch(async(e) => { const errorTemplateData = { 'errormessage': e.message }; diff --git a/course/classes/local/service/content_item_service.php b/course/classes/local/service/content_item_service.php index 0e16419ec6421..e828b2cbc80c5 100644 --- a/course/classes/local/service/content_item_service.php +++ b/course/classes/local/service/content_item_service.php @@ -277,8 +277,11 @@ public function get_content_items_for_user_in_course(\stdClass $user, \stdClass return course_allowed_module($course, explode('_', $parents[$contentitem->get_component_name()])[1], $user); }); + $format = course_get_format($course); + $maxsectionsreached = ($format->get_last_section_number() >= $format->get_max_sections()); + // Now, check there is no delegated section into a delegated section. - if (is_null($sectioninfo) || $sectioninfo->is_delegated()) { + if (is_null($sectioninfo) || $sectioninfo->is_delegated() || $maxsectionsreached) { $availablecontentitems = array_filter($availablecontentitems, function($contentitem){ return !sectiondelegate::has_delegate_class($contentitem->get_component_name()); }); diff --git a/course/classes/output/actionbar/renderer.php b/course/classes/output/actionbar/renderer.php index 26f0f1b243fa3..adc5b7e89a24c 100644 --- a/course/classes/output/actionbar/renderer.php +++ b/course/classes/output/actionbar/renderer.php @@ -25,6 +25,17 @@ */ class renderer extends \plugin_renderer_base { + /** + * Renders the user selector trigger element in the action bar. + * + * @param user_selector $userselector The user selector object. + * @return string The HTML output. + */ + public function render_user_selector(user_selector $userselector): string { + $data = $userselector->export_for_template($this); + return parent::render_from_template($userselector->get_template(), $data); + } + /** * Renders the group selector trigger element in the action bar. * diff --git a/course/classes/output/actionbar/user_selector.php b/course/classes/output/actionbar/user_selector.php new file mode 100644 index 0000000000000..6afba7196c295 --- /dev/null +++ b/course/classes/output/actionbar/user_selector.php @@ -0,0 +1,96 @@ +. + +namespace core_course\output\actionbar; + +use core\output\comboboxsearch; +use moodle_url; +use stdClass; + +/** + * Renderable class for the user selector element in the action bar. + * + * @package core_course + * @copyright 2024 Ilya Tregubov + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_selector extends comboboxsearch { + + /** + * The class constructor. + * + * @param stdClass $course The course object. + * @param moodle_url $resetlink The reset link. + * @param int|null $userid The user ID. + * @param int|null $groupid The group ID. + * @param string $usersearch The user search query. + * @param int|null $instanceid The instance ID. + */ + public function __construct( + stdClass $course, + moodle_url $resetlink, + ?int $userid = null, + ?int $groupid = null, + string $usersearch = '', + ?int $instanceid = null + ) { + + $userselectorontent = $this->user_selector_output($course, $resetlink, $userid, $groupid, $usersearch, $instanceid); + parent::__construct(true, $userselectorontent, null, 'user-search d-flex', + null, 'usersearchdropdown overflow-auto', null, false); + } + + /** + * Method that generates the output for the user selector. + * + * @param stdClass $course The course object. + * @param moodle_url|null $resetlink The reset link. + * @param int|null $userid The user ID. + * @param int|null $groupid The group ID. + * @param string $usersearch The user search query. + * @param int|null $instanceid The instance ID. + * @return string The HTML output. + */ + private function user_selector_output( + stdClass $course, + ?moodle_url $resetlink = null, + ?int $userid = null, + ?int $groupid = null, + string $usersearch = '', + ?int $instanceid = null + ): string { + global $OUTPUT; + + // If the user ID is set, it indicates that a user has been selected. In this case, override the user search + // string with the full name of the selected user. + if ($userid) { + $usersearch = fullname(\core_user::get_user($userid)); + } + + return $OUTPUT->render_from_template('core_user/comboboxsearch/user_selector', [ + 'currentvalue' => $usersearch, + 'courseid' => $course->id, + 'instance' => $instanceid ?? rand(), + 'resetlink' => $resetlink->out(false), + 'group' => $groupid ?? 0, + 'name' => 'usersearch', + 'value' => json_encode([ + 'userid' => $userid, + 'search' => $usersearch, + ]), + ]); + } +} diff --git a/course/classes/reportbuilder/local/entities/completion.php b/course/classes/reportbuilder/local/entities/completion.php index e3fa21df4ce11..e851c56380b65 100644 --- a/course/classes/reportbuilder/local/entities/completion.php +++ b/course/classes/reportbuilder/local/entities/completion.php @@ -107,15 +107,14 @@ protected function get_all_columns(): array { )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_BOOLEAN) - ->add_field("CASE WHEN {$coursecompletion}.timecompleted > 0 THEN 1 ELSE 0 END", 'completed') - ->add_field("{$user}.id", 'userid') + ->add_field(" + CASE + WHEN {$coursecompletion}.id IS NULL THEN NULL + WHEN {$coursecompletion}.timecompleted > 0 THEN 1 + ELSE 0 + END", 'completed') ->set_is_sortable(true) - ->add_callback(static function(bool $value, stdClass $row): string { - if (!$row->userid) { - return ''; - } - return format::boolean_as_text($value); - }); + ->add_callback([format::class, 'boolean_as_text']); // Completion criteria column. $criterias = database::generate_alias(); @@ -225,21 +224,22 @@ protected function get_all_columns(): array { $currenttime = time(); $columns[] = (new column( 'dayscourse', - new lang_string('daystakingcourse', 'course'), + new lang_string('daystakingcourse', 'completion'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_INTEGER) ->add_field("( CASE - WHEN {$coursecompletion}.timecompleted > 0 THEN + WHEN {$coursecompletion}.id IS NULL THEN NULL + ELSE (CASE WHEN {$coursecompletion}.timecompleted > 0 THEN {$coursecompletion}.timecompleted - ELSE + ELSE {$currenttime} - END - {$course}.startdate) / " . DAYSECS, 'dayscourse') - ->add_field("{$user}.id", 'userid') + END - {$course}.startdate) + END)", 'dayscourse') ->set_is_sortable(true) - ->add_callback([completion_formatter::class, 'get_days']); + ->set_callback([format::class, 'format_time']); // Days since last completion (days since last enrolment date until completion or until current date if not completed). $columns[] = (new column( @@ -251,14 +251,15 @@ protected function get_all_columns(): array { ->set_type(column::TYPE_INTEGER) ->add_field("( CASE - WHEN {$coursecompletion}.timecompleted > 0 THEN + WHEN {$coursecompletion}.id IS NULL THEN NULL + ELSE (CASE WHEN {$coursecompletion}.timecompleted > 0 THEN {$coursecompletion}.timecompleted - ELSE + ELSE {$currenttime} - END - {$coursecompletion}.timeenrolled) / " . DAYSECS, 'daysuntilcompletion') - ->add_field("{$user}.id", 'userid') + END - {$coursecompletion}.timeenrolled) + END)", 'daysuntilcompletion') ->set_is_sortable(true) - ->add_callback([completion_formatter::class, 'get_days']); + ->set_callback([format::class, 'format_time']); // Student course grade. $columns[] = (new column( diff --git a/course/classes/reportbuilder/local/formatters/completion.php b/course/classes/reportbuilder/local/formatters/completion.php index c031b4e4e8f6d..786b04d05f238 100644 --- a/course/classes/reportbuilder/local/formatters/completion.php +++ b/course/classes/reportbuilder/local/formatters/completion.php @@ -66,8 +66,13 @@ public static function completion_progress(?string $value, stdClass $row): strin * @param int|null $value * @param stdClass $row * @return int|null + * + * @deprecated since Moodle 4.5 - please do not use this function any more */ + #[\core\attribute\deprecated(null, mdl: 'MDL-82467', since: '4.5')] public static function get_days(?int $value, stdClass $row): ?int { + \core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]); + // Do not show anything if there is no userid. if (!$row->userid) { return null; diff --git a/course/externallib.php b/course/externallib.php index 4be56550e6a47..74f714cd789d4 100644 --- a/course/externallib.php +++ b/course/externallib.php @@ -3232,13 +3232,13 @@ public static function get_courses_by_field_parameters() { shortname: course short name idnumber: course id number category: category id the course belongs to + sectionid: section id that belongs to a course ', VALUE_DEFAULT, ''), 'value' => new external_value(PARAM_RAW, 'The value to match', VALUE_DEFAULT, '') ) ); } - /** * Get courses matching a specific field (id/s, shortname, idnumber, category) * @@ -3267,6 +3267,7 @@ public static function get_courses_by_field($field = '', $value = '') { switch ($params['field']) { case 'id': case 'category': + case 'sectionid': $value = clean_param($params['value'], PARAM_INT); break; case 'ids': @@ -3297,6 +3298,9 @@ public static function get_courses_by_field($field = '', $value = '') { // more efficiently. list ($courses, $warnings) = util::validate_courses($courseids, [], false, true); + } else if ($params['field'] === 'sectionid') { + $courseid = $DB->get_field('course_sections', 'course', ['id' => $value]); + $courses = $courseid ? [$DB->get_record('course', ['id' => $courseid])] : []; } else { $courses = $DB->get_records('course', array($params['field'] => $value), 'id ASC'); } diff --git a/course/format/UPGRADING.md b/course/format/UPGRADING.md index 3670e853278e8..4899051070e64 100644 --- a/course/format/UPGRADING.md +++ b/course/format/UPGRADING.md @@ -8,3 +8,12 @@ If `istrackeduser` is pre-computed for the course module's course, it can be provided here to avoid an additional function call. For more information see [MDL-81610](https://tracker.moodle.org/browse/MDL-81610) +- New $disabled parameter has been added to select, select_optgroup and select_option html_writers to create disabled option elements. + + For more information see [MDL-82146](https://tracker.moodle.org/browse/MDL-82146) +- New \core_courseformat\output\local\content\basecontrolmenu class has been created. Existing \core_courseformat\output\local\content\cm\controlmenu and \core_courseformat\output\local\content\section\controlmenu classes extend the new \core_courseformat\output\local\content\basecontrolmenu class. + + For more information see [MDL-82510](https://tracker.moodle.org/browse/MDL-82510) +- New \core_courseformat\output\local\content\cm\delegatedcontrolmenu class has been created extending \core_courseformat\output\local\content\basecontrolmenu class to render delegated section action menu combining section and module action menu. + + For more information see [MDL-82510](https://tracker.moodle.org/browse/MDL-82510) diff --git a/course/format/amd/build/local/content/actions.min.js b/course/format/amd/build/local/content/actions.min.js index b9978d36cc438..20fde4f47604c 100644 --- a/course/format/amd/build/local/content/actions.min.js +++ b/course/format/amd/build/local/content/actions.min.js @@ -1,4 +1,4 @@ -define("core_courseformat/local/content/actions",["exports","core/reactive","core/modal","core/modal_save_cancel","core/modal_delete_cancel","core/modal_events","core/templates","core/prefetch","core/str","core/normalise","core_courseformat/local/content/actions/bulkselection","core_course/events","core/pending","core_courseformat/local/courseeditor/contenttree","jquery"],(function(_exports,_reactive,_modal,_modal_save_cancel,_modal_delete_cancel,_modal_events,_templates,_prefetch,_str,_normalise,_bulkselection,CourseEvents,_pending,_contenttree,_jquery){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +define("core_courseformat/local/content/actions",["exports","core/reactive","core/modal","core/modal_save_cancel","core/modal_delete_cancel","core/modal_events","core/templates","core/prefetch","core/str","core/normalise","core_courseformat/local/content/actions/bulkselection","core_course/events","core/pending","core_courseformat/local/courseeditor/contenttree","jquery","core/notification"],(function(_exports,_reactive,_modal,_modal_save_cancel,_modal_delete_cancel,_modal_events,_templates,_prefetch,_str,_normalise,_bulkselection,CourseEvents,_pending,_contenttree,_jquery,_notification){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Course state actions dispatcher. * @@ -9,6 +9,6 @@ define("core_courseformat/local/content/actions",["exports","core/reactive","cor * @class core_courseformat/local/content/actions * @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,_modal=_interopRequireDefault(_modal),_modal_save_cancel=_interopRequireDefault(_modal_save_cancel),_modal_delete_cancel=_interopRequireDefault(_modal_delete_cancel),_modal_events=_interopRequireDefault(_modal_events),_templates=_interopRequireDefault(_templates),CourseEvents=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(CourseEvents),_pending=_interopRequireDefault(_pending),_contenttree=_interopRequireDefault(_contenttree),_jquery=_interopRequireDefault(_jquery),(0,_prefetch.prefetchStrings)("core",["movecoursesection","movecoursemodule","confirm","delete"]);const directMutations={sectionHide:"sectionHide",sectionShow:"sectionShow",cmHide:"cmHide",cmShow:"cmShow",cmStealth:"cmStealth",cmMoveRight:"cmMoveRight",cmMoveLeft:"cmMoveLeft",cmNoGroups:"cmNoGroups",cmSeparateGroups:"cmSeparateGroups",cmVisibleGroups:"cmVisibleGroups"};class _default extends _reactive.BaseComponent{create(){this.name="content_actions",this.selectors={ACTIONLINK:"[data-action]",SECTIONLINK:"[data-for='section']",CMLINK:"[data-for='cm']",SECTIONNODE:"[data-for='sectionnode']",MODALTOGGLER:"[data-toggle='collapse']",ADDSECTION:"[data-action='addSection']",CONTENTTREE:"#destination-selector",ACTIONMENU:".action-menu",ACTIONMENUTOGGLER:'[data-toggle="dropdown"]',OPTIONSRADIO:"[type='radio']"},this.classes={DISABLED:"text-body",ITALIC:"font-italic"}}static addActions(actions){for(const[action,mutationReference]of Object.entries(actions)){if("function"!=typeof mutationReference&&"string"!=typeof mutationReference)throw new Error("".concat(action," action must be a mutation name or a function"));directMutations[action]=mutationReference}}stateReady(state){this.addEventListener(this.element,"click",this._dispatchClick),this._checkSectionlist({state:state}),this.addEventListener(this.element,CourseEvents.sectionRefreshed,(()=>this._checkSectionlist({state:state})))}getWatchers(){return[{watch:"course.sectionlist:updated",handler:this._checkSectionlist}]}_dispatchClick(event){const target=event.target.closest(this.selectors.ACTIONLINK);if(!target)return;if(target.classList.contains(this.classes.DISABLED))return void event.preventDefault();const actionName=target.dataset.action,methodName=this._actionMethodName(actionName);if(void 0===this[methodName])return void 0!==directMutations[actionName]?"function"==typeof directMutations[actionName]?void directMutations[actionName](target,event):void this._requestMutationAction(target,event,directMutations[actionName]):void 0;this[methodName](target,event)}_actionMethodName(name){const requestName=name.charAt(0).toUpperCase()+name.slice(1);return"_request".concat(requestName)}_checkSectionlist(_ref){let{state:state}=_ref;this._setAddSectionLocked(state.course.sectionlist.length>state.course.maxsections)}_getTargetIds(target){var _target$dataset,_target$dataset2;let ids=[];null!=target&&null!==(_target$dataset=target.dataset)&&void 0!==_target$dataset&&_target$dataset.id&&ids.push(target.dataset.id);const bulkType=null==target||null===(_target$dataset2=target.dataset)||void 0===_target$dataset2?void 0:_target$dataset2.bulk;if(!bulkType)return ids;const bulk=this.reactive.get("bulk");return bulk.enabled&&bulk.selectedType===bulkType&&(ids=[...ids,...bulk.selection]),ids}async _requestMoveSection(target,event){const sectionIds=this._getTargetIds(target);if(0==sectionIds.length)return;event.preventDefault();const pendingModalReady=new _pending.default("courseformat/actions:prepareMoveSectionModal"),editTools=this._getClosestActionMenuToogler(target),data=this.reactive.getExporter().course(this.reactive.state);let titleText=null,sectionInfo=null;1==sectionIds.length?(sectionInfo=this.reactive.get("section",sectionIds[0]),data.sectionid=sectionInfo.id,data.sectiontitle=sectionInfo.title,data.information=await this.reactive.getFormatString("sectionmove_info",data.sectiontitle),titleText=this.reactive.getFormatString("sectionmove_title")):(data.information=await this.reactive.getFormatString("sectionsmove_info",sectionIds.length),titleText=this.reactive.getFormatString("sectionsmove_title"));const modal=await this._modalBodyRenderedPromise(_modal.default,{title:titleText,body:_templates.default.render("core_courseformat/local/content/movesection",data)}),modalBody=(0,_normalise.getFirst)(modal.getBody());sectionIds.forEach((sectionId=>{const currentElement=modalBody.querySelector("".concat(this.selectors.SECTIONLINK,"[data-id='").concat(sectionId,"']"));this._disableLink(currentElement)})),new _contenttree.default(modalBody.querySelector(this.selectors.CONTENTTREE),{SECTION:this.selectors.SECTIONNODE,TOGGLER:this.selectors.MODALTOGGLER,COLLAPSE:this.selectors.MODALTOGGLER},!0),modalBody.addEventListener("click",(event=>{const target=event.target;target.matches("a")&&"section"==target.dataset.for&&void 0!==target.dataset.id&&(target.getAttribute("aria-disabled")||(event.preventDefault(),this.reactive.dispatch("sectionMoveAfter",sectionIds,target.dataset.id),this._destroyModal(modal,editTools)))})),pendingModalReady.resolve()}async _requestMoveCm(target,event){const cmIds=this._getTargetIds(target);if(0==cmIds.length)return;event.preventDefault();const pendingModalReady=new _pending.default("courseformat/actions:prepareMoveCmModal"),editTools=this._getClosestActionMenuToogler(target),exporter=this.reactive.getExporter(),data=exporter.course(this.reactive.state);let titleText=null;if(1==cmIds.length){const cmInfo=this.reactive.get("cm",cmIds[0]);data.cmid=cmInfo.id,data.cmname=cmInfo.name,data.information=await this.reactive.getFormatString("cmmove_info",data.cmname),titleText=this.reactive.getFormatString("cmmove_title")}else data.information=await this.reactive.getFormatString("cmsmove_info",cmIds.length),titleText=this.reactive.getFormatString("cmsmove_title");const modal=await this._modalBodyRenderedPromise(_modal.default,{title:titleText,body:_templates.default.render("core_courseformat/local/content/movecm",data)}),modalBody=(0,_normalise.getFirst)(modal.getBody());cmIds.forEach((cmId=>{const currentElement=modalBody.querySelector("".concat(this.selectors.CMLINK,"[data-id='").concat(cmId,"']"));this._disableLink(currentElement)})),new _contenttree.default(modalBody.querySelector(this.selectors.CONTENTTREE),{SECTION:this.selectors.SECTIONNODE,TOGGLER:this.selectors.MODALTOGGLER,COLLAPSE:this.selectors.MODALTOGGLER,ENTER:this.selectors.SECTIONLINK}),cmIds.forEach((cmId=>{var _toggler$data;const sectionnode=modalBody.querySelector("".concat(this.selectors.CMLINK,"[data-id='").concat(cmId,"']")).closest(this.selectors.SECTIONNODE),toggler=(0,_jquery.default)(sectionnode).find(this.selectors.MODALTOGGLER);let collapsibleId=null!==(_toggler$data=toggler.data("target"))&&void 0!==_toggler$data?_toggler$data:toggler.attr("href");if(collapsibleId){collapsibleId=collapsibleId.replace("#","");const expandNode=modalBody.querySelector("#".concat(collapsibleId));(0,_jquery.default)(expandNode).collapse("show")}})),modalBody.addEventListener("click",(event=>{const target=event.target;if(!target.matches("a")||void 0===target.dataset.for||void 0===target.dataset.id)return;if(target.getAttribute("aria-disabled"))return;let targetSectionId,targetCmId;if(event.preventDefault(),"cm"==target.dataset.for){const dropData=exporter.cmDraggableData(this.reactive.state,target.dataset.id);targetSectionId=dropData.sectionid,targetCmId=dropData.nextcmid}else{const section=this.reactive.get("section",target.dataset.id);targetSectionId=target.dataset.id,targetCmId=null==section?void 0:section.cmlist[0]}this.reactive.dispatch("cmMove",cmIds,targetSectionId,targetCmId),this._destroyModal(modal,editTools)})),pendingModalReady.resolve()}async _requestAddSection(target,event){var _target$dataset$id;event.preventDefault(),this.reactive.dispatch("addSection",null!==(_target$dataset$id=target.dataset.id)&&void 0!==_target$dataset$id?_target$dataset$id:0)}async _requestDeleteSection(target,event){const sectionIds=this._getTargetIds(target);if(0==sectionIds.length)return;if(event.preventDefault(),!sectionIds.some((sectionId=>{var _sectionInfo$cmlist;const sectionInfo=this.reactive.get("section",sectionId);return(null!==(_sectionInfo$cmlist=sectionInfo.cmlist)&&void 0!==_sectionInfo$cmlist?_sectionInfo$cmlist:[]).length||sectionInfo.hassummary||sectionInfo.rawtitle})))return void this._dispatchSectionDelete(sectionIds,target);let bodyText=null,titleText=null;if(1==sectionIds.length){titleText=this.reactive.getFormatString("sectiondelete_title");const sectionInfo=this.reactive.get("section",sectionIds[0]);bodyText=this.reactive.getFormatString("sectiondelete_info",{name:sectionInfo.title})}else titleText=this.reactive.getFormatString("sectionsdelete_title"),bodyText=this.reactive.getFormatString("sectionsdelete_info",{count:sectionIds.length});const modal=await this._modalBodyRenderedPromise(_modal_delete_cancel.default,{title:titleText,body:bodyText});modal.getRoot().on(_modal_events.default.delete,(e=>{e.preventDefault(),modal.destroy(),this._dispatchSectionDelete(sectionIds,target)}))}async _dispatchSectionDelete(sectionIds,target){await this.reactive.dispatch("sectionDelete",sectionIds),target.baseURI.includes("section.php")&&(window.location.href=this.reactive.get("course").baseurl)}async _requestToggleSelectionCm(target,event){(0,_bulkselection.toggleBulkSelectionAction)(this.reactive,target,event,"cm")}async _requestToggleSelectionSection(target,event){(0,_bulkselection.toggleBulkSelectionAction)(this.reactive,target,event,"section")}async _requestMutationAction(target,event,mutationName){(target.dataset.id||"bulkaction"===target.dataset.for)&&(event.preventDefault(),"bulkaction"===target.dataset.for?this.reactive.dispatch(mutationName,this.reactive.get("bulk").selection):this.reactive.dispatch(mutationName,[target.dataset.id]))}async _requestCmDuplicate(target,event){var _target$dataset$secti;const cmIds=this._getTargetIds(target);if(0==cmIds.length)return;const sectionId=null!==(_target$dataset$secti=target.dataset.sectionid)&&void 0!==_target$dataset$secti?_target$dataset$secti:null;event.preventDefault(),this.reactive.dispatch("cmDuplicate",cmIds,sectionId)}async _requestCmDelete(target,event){const cmIds=this._getTargetIds(target);if(0==cmIds.length)return;event.preventDefault();let bodyText=null,titleText=null;if(1==cmIds.length){const cmInfo=this.reactive.get("cm",cmIds[0]);titleText=(0,_str.getString)("cmdelete_title","core_courseformat"),bodyText=(0,_str.getString)("cmdelete_info","core_courseformat",{type:cmInfo.modname,name:cmInfo.name})}else titleText=(0,_str.getString)("cmsdelete_title","core_courseformat"),bodyText=(0,_str.getString)("cmsdelete_info","core_courseformat",{count:cmIds.length});const modal=await this._modalBodyRenderedPromise(_modal_delete_cancel.default,{title:titleText,body:bodyText});modal.getRoot().on(_modal_events.default.delete,(e=>{e.preventDefault(),modal.destroy(),this.reactive.dispatch("cmDelete",cmIds)}))}async _requestCmAvailability(target){const cmIds=this._getTargetIds(target);if(0==cmIds.length)return;const data={allowstealth:this.reactive.getExporter().canUseStealth(this.reactive.state,cmIds)},modal=await this._modalBodyRenderedPromise(_modal_save_cancel.default,{title:(0,_str.getString)("availability","core"),body:_templates.default.render("core_courseformat/local/content/cm/availabilitymodal",data),saveButtonText:(0,_str.getString)("apply","core")});this._setupMutationRadioButtonModal(modal,cmIds)}async _requestSectionAvailability(target){const sectionIds=this._getTargetIds(target);if(0==sectionIds.length)return;const title=1==sectionIds.length?"sectionavailability_title":"sectionsavailability_title",modal=await this._modalBodyRenderedPromise(_modal_save_cancel.default,{title:this.reactive.getFormatString(title),body:_templates.default.render("core_courseformat/local/content/section/availabilitymodal",[]),saveButtonText:(0,_str.getString)("apply","core")});this._setupMutationRadioButtonModal(modal,sectionIds)}_setupMutationRadioButtonModal(modal,ids){modal.setButtonDisabled("save",!0);const submitFunction=radio=>{const mutation=null==radio?void 0:radio.value;return!!mutation&&(this.reactive.dispatch(mutation,ids),!0)},modalBody=(0,_normalise.getFirst)(modal.getBody());modalBody.querySelectorAll(this.selectors.OPTIONSRADIO).forEach((radio=>{radio.addEventListener("change",(()=>{modal.setButtonDisabled("save",!1)})),radio.parentNode.addEventListener("click",(()=>{radio.checked=!0,modal.setButtonDisabled("save",!1)})),radio.parentNode.addEventListener("dblclick",(dbClickEvent=>{submitFunction(radio)&&(dbClickEvent.preventDefault(),modal.destroy())}))})),modal.getRoot().on(_modal_events.default.save,(()=>{const radio=modalBody.querySelector("".concat(this.selectors.OPTIONSRADIO,":checked"));submitFunction(radio)}))}_setAddSectionLocked(locked){this.getElements(this.selectors.ADDSECTION).forEach((element=>{element.classList.toggle(this.classes.DISABLED,locked),element.classList.toggle(this.classes.ITALIC,locked),this.setElementLocked(element,locked)}))}_disableLink(element){element&&(element.style.pointerEvents="none",element.style.userSelect="none",element.classList.add(this.classes.DISABLED),element.classList.add(this.classes.ITALIC),element.setAttribute("aria-disabled",!0),element.addEventListener("click",(event=>event.preventDefault())))}_modalBodyRenderedPromise(ModalClass,modalParams){return new Promise(((resolve,reject)=>{ModalClass.create(modalParams).then((modal=>{modal.setRemoveOnClose(!0),modal.getRoot().on(_modal_events.default.bodyRendered,(()=>{resolve(modal)})),void 0!==modalParams.saveButtonText&&modal.setSaveButtonText(modalParams.saveButtonText),void 0!==modalParams.deleteButtonText&&modal.setDeleteButtonText(modalParams.saveButtonText),modal.show()})).catch((()=>{reject("Cannot load modal content")}))}))}_destroyModal(modal,element){modal.hide();const pendingDestroy=new _pending.default("courseformat/actions:destroyModal");element&&element.focus(),setTimeout((()=>{modal.destroy(),pendingDestroy.resolve()}),500)}_getClosestActionMenuToogler(element){const actionMenu=element.closest(this.selectors.ACTIONMENU);if(actionMenu)return actionMenu.querySelector(this.selectors.ACTIONMENUTOGGLER)}}return _exports.default=_default,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=_interopRequireDefault(_modal),_modal_save_cancel=_interopRequireDefault(_modal_save_cancel),_modal_delete_cancel=_interopRequireDefault(_modal_delete_cancel),_modal_events=_interopRequireDefault(_modal_events),_templates=_interopRequireDefault(_templates),CourseEvents=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(CourseEvents),_pending=_interopRequireDefault(_pending),_contenttree=_interopRequireDefault(_contenttree),_jquery=_interopRequireDefault(_jquery),_notification=_interopRequireDefault(_notification),(0,_prefetch.prefetchStrings)("core",["movecoursesection","movecoursemodule","confirm","delete"]);const directMutations={sectionHide:"sectionHide",sectionShow:"sectionShow",cmHide:"cmHide",cmShow:"cmShow",cmStealth:"cmStealth",cmMoveRight:"cmMoveRight",cmMoveLeft:"cmMoveLeft",cmNoGroups:"cmNoGroups",cmSeparateGroups:"cmSeparateGroups",cmVisibleGroups:"cmVisibleGroups"};class _default extends _reactive.BaseComponent{create(){this.name="content_actions",this.selectors={ACTIONLINK:"[data-action]",SECTIONLINK:"[data-for='section']",CMLINK:"[data-for='cm']",SECTIONNODE:"[data-for='sectionnode']",MODALTOGGLER:"[data-toggle='collapse']",ADDSECTION:"[data-action='addSection']",CONTENTTREE:"#destination-selector",ACTIONMENU:".action-menu",ACTIONMENUTOGGLER:'[data-toggle="dropdown"]',OPTIONSRADIO:"[type='radio']",COURSEADDSECTION:"#course-addsection",MAXSECTIONSWARNING:"[data-region='max-sections-warning']",ADDSECTIONREGION:"[data-region='section-addsection']"},this.classes={DISABLED:"disabled",ITALIC:"font-italic",DISPLAYNONE:"d-none"}}static addActions(actions){for(const[action,mutationReference]of Object.entries(actions)){if("function"!=typeof mutationReference&&"string"!=typeof mutationReference)throw new Error("".concat(action," action must be a mutation name or a function"));directMutations[action]=mutationReference}}stateReady(state){this.addEventListener(this.element,"click",this._dispatchClick),this._checkSectionlist({state:state}),this.addEventListener(this.element,CourseEvents.sectionRefreshed,(()=>this._checkSectionlist({state:state})))}getWatchers(){return[{watch:"course.sectionlist:updated",handler:this._checkSectionlist}]}_dispatchClick(event){const target=event.target.closest(this.selectors.ACTIONLINK);if(!target)return;if(target.classList.contains(this.classes.DISABLED))return void event.preventDefault();const actionName=target.dataset.action,methodName=this._actionMethodName(actionName);if(void 0===this[methodName])return void 0!==directMutations[actionName]?"function"==typeof directMutations[actionName]?void directMutations[actionName](target,event):void this._requestMutationAction(target,event,directMutations[actionName]):void 0;this[methodName](target,event)}_actionMethodName(name){const requestName=name.charAt(0).toUpperCase()+name.slice(1);return"_request".concat(requestName)}_checkSectionlist(_ref){let{state:state}=_ref;this._setAddSectionLocked(state.course.sectionlist.length>state.course.maxsections)}_getTargetIds(target){var _target$dataset,_target$dataset2;let ids=[];null!=target&&null!==(_target$dataset=target.dataset)&&void 0!==_target$dataset&&_target$dataset.id&&ids.push(target.dataset.id);const bulkType=null==target||null===(_target$dataset2=target.dataset)||void 0===_target$dataset2?void 0:_target$dataset2.bulk;if(!bulkType)return ids;const bulk=this.reactive.get("bulk");return bulk.enabled&&bulk.selectedType===bulkType&&(ids=[...ids,...bulk.selection]),ids}async _requestMoveSection(target,event){const sectionIds=this._getTargetIds(target);if(0==sectionIds.length)return;event.preventDefault();const pendingModalReady=new _pending.default("courseformat/actions:prepareMoveSectionModal"),editTools=this._getClosestActionMenuToogler(target),data=this.reactive.getExporter().course(this.reactive.state);let titleText=null,sectionInfo=null;1==sectionIds.length?(sectionInfo=this.reactive.get("section",sectionIds[0]),data.sectionid=sectionInfo.id,data.sectiontitle=sectionInfo.title,data.information=await this.reactive.getFormatString("sectionmove_info",data.sectiontitle),titleText=this.reactive.getFormatString("sectionmove_title")):(data.information=await this.reactive.getFormatString("sectionsmove_info",sectionIds.length),titleText=this.reactive.getFormatString("sectionsmove_title"));const modal=await this._modalBodyRenderedPromise(_modal.default,{title:titleText,body:_templates.default.render("core_courseformat/local/content/movesection",data)}),modalBody=(0,_normalise.getFirst)(modal.getBody());sectionIds.forEach((sectionId=>{const currentElement=modalBody.querySelector("".concat(this.selectors.SECTIONLINK,"[data-id='").concat(sectionId,"']"));this._disableLink(currentElement)})),new _contenttree.default(modalBody.querySelector(this.selectors.CONTENTTREE),{SECTION:this.selectors.SECTIONNODE,TOGGLER:this.selectors.MODALTOGGLER,COLLAPSE:this.selectors.MODALTOGGLER},!0),modalBody.addEventListener("click",(event=>{const target=event.target;target.matches("a")&&"section"==target.dataset.for&&void 0!==target.dataset.id&&(target.getAttribute("aria-disabled")||(event.preventDefault(),this.reactive.dispatch("sectionMoveAfter",sectionIds,target.dataset.id),this._destroyModal(modal,editTools)))})),pendingModalReady.resolve()}async _requestMoveCm(target,event){const cmIds=this._getTargetIds(target);if(0==cmIds.length)return;event.preventDefault();const pendingModalReady=new _pending.default("courseformat/actions:prepareMoveCmModal"),editTools=this._getClosestActionMenuToogler(target),exporter=this.reactive.getExporter(),data=exporter.course(this.reactive.state);let titleText=null;if(1==cmIds.length){const cmInfo=this.reactive.get("cm",cmIds[0]);data.cmid=cmInfo.id,data.cmname=cmInfo.name,data.information=await this.reactive.getFormatString("cmmove_info",data.cmname),titleText=this.reactive.getFormatString("cmmove_title")}else data.information=await this.reactive.getFormatString("cmsmove_info",cmIds.length),titleText=this.reactive.getFormatString("cmsmove_title");const modal=await this._modalBodyRenderedPromise(_modal.default,{title:titleText,body:_templates.default.render("core_courseformat/local/content/movecm",data)}),modalBody=(0,_normalise.getFirst)(modal.getBody());cmIds.forEach((cmId=>{const currentElement=modalBody.querySelector("".concat(this.selectors.CMLINK,"[data-id='").concat(cmId,"']"));this._disableLink(currentElement)})),new _contenttree.default(modalBody.querySelector(this.selectors.CONTENTTREE),{SECTION:this.selectors.SECTIONNODE,TOGGLER:this.selectors.MODALTOGGLER,COLLAPSE:this.selectors.MODALTOGGLER,ENTER:this.selectors.SECTIONLINK}),cmIds.forEach((cmId=>{const cmInfo=this.reactive.get("cm",cmId);let selector;selector=cmInfo.hasdelegatedsection?"".concat(this.selectors.SECTIONLINK,"[data-id='").concat(cmInfo.sectionid,"']"):"".concat(this.selectors.CMLINK,"[data-id='").concat(cmId,"']");const currentElement=modalBody.querySelector(selector);this._expandCmMoveModalParentSections(modalBody,currentElement)})),modalBody.addEventListener("click",(event=>{const target=event.target;if(!target.matches("a")||void 0===target.dataset.for||void 0===target.dataset.id)return;if(target.getAttribute("aria-disabled"))return;let targetSectionId,targetCmId;event.preventDefault();let droppedCmIds=[...cmIds];if("cm"==target.dataset.for){const dropData=exporter.cmDraggableData(this.reactive.state,target.dataset.id);targetSectionId=dropData.sectionid,targetCmId=dropData.nextcmid}else{const section=this.reactive.get("section",target.dataset.id);targetSectionId=target.dataset.id,targetCmId=null==section?void 0:section.cmlist[0]}this.reactive.get("section",targetSectionId).component&&(droppedCmIds=droppedCmIds.filter((cmId=>!this.reactive.get("cm",cmId).hasdelegatedsection))),0!==droppedCmIds.length&&(this.reactive.dispatch("cmMove",droppedCmIds,targetSectionId,targetCmId),this._destroyModal(modal,editTools))})),pendingModalReady.resolve()}_expandCmMoveModalParentSections(modalBody,element){var _toggler$data;const sectionnode=element.closest(this.selectors.SECTIONNODE);if(!sectionnode)return;const toggler=(0,_jquery.default)(sectionnode).find(this.selectors.MODALTOGGLER);let collapsibleId=null!==(_toggler$data=toggler.data("target"))&&void 0!==_toggler$data?_toggler$data:toggler.attr("href");if(collapsibleId){collapsibleId=collapsibleId.replace("#","");const expandNode=modalBody.querySelector("#".concat(collapsibleId));(0,_jquery.default)(expandNode).collapse("show")}this._expandCmMoveModalParentSections(modalBody,sectionnode.parentElement)}async _requestAddSection(target,event){var _target$dataset$id;event.preventDefault(),this.reactive.dispatch("addSection",null!==(_target$dataset$id=target.dataset.id)&&void 0!==_target$dataset$id?_target$dataset$id:0)}async _requestDeleteSection(target,event){const sectionIds=this._getTargetIds(target);if(0==sectionIds.length)return;if(event.preventDefault(),!sectionIds.some((sectionId=>{var _sectionInfo$cmlist;const sectionInfo=this.reactive.get("section",sectionId);return(null!==(_sectionInfo$cmlist=sectionInfo.cmlist)&&void 0!==_sectionInfo$cmlist?_sectionInfo$cmlist:[]).length||sectionInfo.hassummary||sectionInfo.rawtitle})))return void this._dispatchSectionDelete(sectionIds,target);let bodyText=null,titleText=null;if(1==sectionIds.length){titleText=this.reactive.getFormatString("sectiondelete_title");const sectionInfo=this.reactive.get("section",sectionIds[0]);bodyText=this.reactive.getFormatString("sectiondelete_info",{name:sectionInfo.title})}else titleText=this.reactive.getFormatString("sectionsdelete_title"),bodyText=this.reactive.getFormatString("sectionsdelete_info",{count:sectionIds.length});const modal=await this._modalBodyRenderedPromise(_modal_delete_cancel.default,{title:titleText,body:bodyText});modal.getRoot().on(_modal_events.default.delete,(e=>{e.preventDefault(),modal.destroy(),this._dispatchSectionDelete(sectionIds,target)}))}async _dispatchSectionDelete(sectionIds,target){await this.reactive.dispatch("sectionDelete",sectionIds),target.baseURI.includes("section.php")&&(window.location.href=this.reactive.get("course").baseurl)}async _requestToggleSelectionCm(target,event){(0,_bulkselection.toggleBulkSelectionAction)(this.reactive,target,event,"cm")}async _requestToggleSelectionSection(target,event){(0,_bulkselection.toggleBulkSelectionAction)(this.reactive,target,event,"section")}async _requestMutationAction(target,event,mutationName){(target.dataset.id||"bulkaction"===target.dataset.for)&&(event.preventDefault(),"bulkaction"===target.dataset.for?this.reactive.dispatch(mutationName,this.reactive.get("bulk").selection):this.reactive.dispatch(mutationName,[target.dataset.id]))}async _requestCmDuplicate(target,event){var _target$dataset$secti;const cmIds=this._getTargetIds(target);if(0==cmIds.length)return;const sectionId=null!==(_target$dataset$secti=target.dataset.sectionid)&&void 0!==_target$dataset$secti?_target$dataset$secti:null;event.preventDefault(),this.reactive.dispatch("cmDuplicate",cmIds,sectionId)}async _requestCmDelete(target,event){const cmIds=this._getTargetIds(target);if(0==cmIds.length)return;event.preventDefault();let bodyText=null,titleText=null;if(1==cmIds.length){const cmInfo=this.reactive.get("cm",cmIds[0]);titleText=(0,_str.getString)("cmdelete_title","core_courseformat"),bodyText=(0,_str.getString)("cmdelete_info","core_courseformat",{type:cmInfo.modname,name:cmInfo.name})}else titleText=(0,_str.getString)("cmsdelete_title","core_courseformat"),bodyText=(0,_str.getString)("cmsdelete_info","core_courseformat",{count:cmIds.length});const modal=await this._modalBodyRenderedPromise(_modal_delete_cancel.default,{title:titleText,body:bodyText});modal.getRoot().on(_modal_events.default.delete,(e=>{e.preventDefault(),modal.destroy(),this.reactive.dispatch("cmDelete",cmIds)}))}async _requestCmAvailability(target){const cmIds=this._getTargetIds(target);if(0==cmIds.length)return;const data={allowstealth:this.reactive.getExporter().canUseStealth(this.reactive.state,cmIds)},modal=await this._modalBodyRenderedPromise(_modal_save_cancel.default,{title:(0,_str.getString)("availability","core"),body:_templates.default.render("core_courseformat/local/content/cm/availabilitymodal",data),saveButtonText:(0,_str.getString)("apply","core")});this._setupMutationRadioButtonModal(modal,cmIds)}async _requestSectionAvailability(target){const sectionIds=this._getTargetIds(target);if(0==sectionIds.length)return;const title=1==sectionIds.length?"sectionavailability_title":"sectionsavailability_title",modal=await this._modalBodyRenderedPromise(_modal_save_cancel.default,{title:this.reactive.getFormatString(title),body:_templates.default.render("core_courseformat/local/content/section/availabilitymodal",[]),saveButtonText:(0,_str.getString)("apply","core")});this._setupMutationRadioButtonModal(modal,sectionIds)}_setupMutationRadioButtonModal(modal,ids){modal.setButtonDisabled("save",!0);const submitFunction=radio=>{const mutation=null==radio?void 0:radio.value;return!!mutation&&(this.reactive.dispatch(mutation,ids),!0)},modalBody=(0,_normalise.getFirst)(modal.getBody());modalBody.querySelectorAll(this.selectors.OPTIONSRADIO).forEach((radio=>{radio.addEventListener("change",(()=>{modal.setButtonDisabled("save",!1)})),radio.parentNode.addEventListener("click",(()=>{radio.checked=!0,modal.setButtonDisabled("save",!1)})),radio.parentNode.addEventListener("dblclick",(dbClickEvent=>{submitFunction(radio)&&(dbClickEvent.preventDefault(),modal.destroy())}))})),modal.getRoot().on(_modal_events.default.save,(()=>{const radio=modalBody.querySelector("".concat(this.selectors.OPTIONSRADIO,":checked"));submitFunction(radio)}))}_setAddSectionLocked(locked){this.getElements(this.selectors.ADDSECTIONREGION).forEach((element=>{element.classList.toggle(this.classes.DISABLED,locked);const addSectionElement=element.querySelector(this.selectors.ADDSECTION);addSectionElement.classList.toggle(this.classes.DISABLED,locked),this.setElementLocked(addSectionElement,locked),locked?((0,_str.getString)("sectionaddmax","core_courseformat").then((text=>addSectionElement.setAttribute("title",text))).catch(_notification.default.exception),addSectionElement.style.pointerEvents=null,addSectionElement.style.userSelect=null):addSectionElement.setAttribute("title",addSectionElement.dataset.addSections)}));const courseAddSection=this.getElement(this.selectors.COURSEADDSECTION);courseAddSection.querySelector(this.selectors.ADDSECTION).classList.toggle(this.classes.DISPLAYNONE,locked);courseAddSection.querySelector(this.selectors.MAXSECTIONSWARNING).classList.toggle(this.classes.DISPLAYNONE,!locked)}_disableLink(element){element&&(element.style.pointerEvents="none",element.style.userSelect="none",element.classList.add(this.classes.DISABLED),element.classList.add(this.classes.ITALIC),element.setAttribute("aria-disabled",!0),element.addEventListener("click",(event=>event.preventDefault())))}_modalBodyRenderedPromise(ModalClass,modalParams){return new Promise(((resolve,reject)=>{ModalClass.create(modalParams).then((modal=>{modal.setRemoveOnClose(!0),modal.getRoot().on(_modal_events.default.bodyRendered,(()=>{resolve(modal)})),void 0!==modalParams.saveButtonText&&modal.setSaveButtonText(modalParams.saveButtonText),void 0!==modalParams.deleteButtonText&&modal.setDeleteButtonText(modalParams.saveButtonText),modal.show()})).catch((()=>{reject("Cannot load modal content")}))}))}_destroyModal(modal,element){modal.hide();const pendingDestroy=new _pending.default("courseformat/actions:destroyModal");element&&element.focus(),setTimeout((()=>{modal.destroy(),pendingDestroy.resolve()}),500)}_getClosestActionMenuToogler(element){const actionMenu=element.closest(this.selectors.ACTIONMENU);if(actionMenu)return actionMenu.querySelector(this.selectors.ACTIONMENUTOGGLER)}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=actions.min.js.map \ No newline at end of file diff --git a/course/format/amd/build/local/content/actions.min.js.map b/course/format/amd/build/local/content/actions.min.js.map index afe841c6dc832..22efa205a2131 100644 --- a/course/format/amd/build/local/content/actions.min.js.map +++ b/course/format/amd/build/local/content/actions.min.js.map @@ -1 +1 @@ -{"version":3,"file":"actions.min.js","sources":["../../../src/local/content/actions.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 state actions dispatcher.\n *\n * This module captures all data-dispatch links in the course content and dispatch the proper\n * state mutation, including any confirmation and modal required.\n *\n * @module core_courseformat/local/content/actions\n * @class core_courseformat/local/content/actions\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 Modal from 'core/modal';\nimport ModalSaveCancel from 'core/modal_save_cancel';\nimport ModalDeleteCancel from 'core/modal_delete_cancel';\nimport ModalEvents from 'core/modal_events';\nimport Templates from 'core/templates';\nimport {prefetchStrings} from 'core/prefetch';\nimport {getString} from 'core/str';\nimport {getFirst} from 'core/normalise';\nimport {toggleBulkSelectionAction} from 'core_courseformat/local/content/actions/bulkselection';\nimport * as CourseEvents from 'core_course/events';\nimport Pending from 'core/pending';\nimport ContentTree from 'core_courseformat/local/courseeditor/contenttree';\n// The jQuery module is only used for interacting with Boostrap 4. It can we removed when MDL-71979 is integrated.\nimport jQuery from 'jquery';\n\n// Load global strings.\nprefetchStrings('core', ['movecoursesection', 'movecoursemodule', 'confirm', 'delete']);\n\n// Mutations are dispatched by the course content actions.\n// Formats can use this module addActions static method to add custom actions.\n// Direct mutations can be simple strings (mutation) name or functions.\nconst directMutations = {\n sectionHide: 'sectionHide',\n sectionShow: 'sectionShow',\n cmHide: 'cmHide',\n cmShow: 'cmShow',\n cmStealth: 'cmStealth',\n cmMoveRight: 'cmMoveRight',\n cmMoveLeft: 'cmMoveLeft',\n cmNoGroups: 'cmNoGroups',\n cmSeparateGroups: 'cmSeparateGroups',\n cmVisibleGroups: 'cmVisibleGroups',\n};\n\nexport default class extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'content_actions';\n // Default query selectors.\n this.selectors = {\n ACTIONLINK: `[data-action]`,\n // Move modal selectors.\n SECTIONLINK: `[data-for='section']`,\n CMLINK: `[data-for='cm']`,\n SECTIONNODE: `[data-for='sectionnode']`,\n MODALTOGGLER: `[data-toggle='collapse']`,\n ADDSECTION: `[data-action='addSection']`,\n CONTENTTREE: `#destination-selector`,\n ACTIONMENU: `.action-menu`,\n ACTIONMENUTOGGLER: `[data-toggle=\"dropdown\"]`,\n // Availability modal selectors.\n OPTIONSRADIO: `[type='radio']`,\n };\n // Component css classes.\n this.classes = {\n DISABLED: `text-body`,\n ITALIC: `font-italic`,\n };\n }\n\n /**\n * Add extra actions to the module.\n *\n * @param {array} actions array of methods to execute\n */\n static addActions(actions) {\n for (const [action, mutationReference] of Object.entries(actions)) {\n if (typeof mutationReference !== 'function' && typeof mutationReference !== 'string') {\n throw new Error(`${action} action must be a mutation name or a function`);\n }\n directMutations[action] = mutationReference;\n }\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the state data.\n *\n */\n stateReady(state) {\n // Delegate dispatch clicks.\n this.addEventListener(\n this.element,\n 'click',\n this._dispatchClick\n );\n // Check section limit.\n this._checkSectionlist({state});\n // Add an Event listener to recalculate limits it if a section HTML is altered.\n this.addEventListener(\n this.element,\n CourseEvents.sectionRefreshed,\n () => this._checkSectionlist({state})\n );\n }\n\n /**\n * Return the component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n // Check section limit.\n {watch: `course.sectionlist:updated`, handler: this._checkSectionlist},\n ];\n }\n\n _dispatchClick(event) {\n const target = event.target.closest(this.selectors.ACTIONLINK);\n if (!target) {\n return;\n }\n if (target.classList.contains(this.classes.DISABLED)) {\n event.preventDefault();\n return;\n }\n\n // Invoke proper method.\n const actionName = target.dataset.action;\n const methodName = this._actionMethodName(actionName);\n\n if (this[methodName] !== undefined) {\n this[methodName](target, event);\n return;\n }\n\n // Check direct mutations or mutations handlers.\n if (directMutations[actionName] !== undefined) {\n if (typeof directMutations[actionName] === 'function') {\n directMutations[actionName](target, event);\n return;\n }\n this._requestMutationAction(target, event, directMutations[actionName]);\n return;\n }\n }\n\n _actionMethodName(name) {\n const requestName = name.charAt(0).toUpperCase() + name.slice(1);\n return `_request${requestName}`;\n }\n\n /**\n * Check the section list and disable some options if needed.\n *\n * @param {Object} detail the update details.\n * @param {Object} detail.state the state object.\n */\n _checkSectionlist({state}) {\n // Disable \"add section\" actions if the course max sections has been exceeded.\n this._setAddSectionLocked(state.course.sectionlist.length > state.course.maxsections);\n }\n\n /**\n * Return the ids represented by this element.\n *\n * Depending on the dataset attributes the action could represent a single id\n * or a bulk actions with all the current selected ids.\n *\n * @param {HTMLElement} target\n * @returns {Number[]} array of Ids\n */\n _getTargetIds(target) {\n let ids = [];\n if (target?.dataset?.id) {\n ids.push(target.dataset.id);\n }\n const bulkType = target?.dataset?.bulk;\n if (!bulkType) {\n return ids;\n }\n const bulk = this.reactive.get('bulk');\n if (bulk.enabled && bulk.selectedType === bulkType) {\n ids = [...ids, ...bulk.selection];\n }\n return ids;\n }\n\n /**\n * Handle a move section request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestMoveSection(target, event) {\n // Check we have an id.\n const sectionIds = this._getTargetIds(target);\n if (sectionIds.length == 0) {\n return;\n }\n\n event.preventDefault();\n\n const pendingModalReady = new Pending(`courseformat/actions:prepareMoveSectionModal`);\n\n // The section edit menu to refocus on end.\n const editTools = this._getClosestActionMenuToogler(target);\n\n // Collect section information from the state.\n const exporter = this.reactive.getExporter();\n const data = exporter.course(this.reactive.state);\n let titleText = null;\n\n // Add the target section id and title.\n let sectionInfo = null;\n if (sectionIds.length == 1) {\n sectionInfo = this.reactive.get('section', sectionIds[0]);\n data.sectionid = sectionInfo.id;\n data.sectiontitle = sectionInfo.title;\n data.information = await this.reactive.getFormatString('sectionmove_info', data.sectiontitle);\n titleText = this.reactive.getFormatString('sectionmove_title');\n } else {\n data.information = await this.reactive.getFormatString('sectionsmove_info', sectionIds.length);\n titleText = this.reactive.getFormatString('sectionsmove_title');\n }\n\n\n // Create the modal.\n // Build the modal parameters from the event data.\n const modal = await this._modalBodyRenderedPromise(Modal, {\n title: titleText,\n body: Templates.render('core_courseformat/local/content/movesection', data),\n });\n\n const modalBody = getFirst(modal.getBody());\n\n // Disable current selected section ids.\n sectionIds.forEach(sectionId => {\n const currentElement = modalBody.querySelector(`${this.selectors.SECTIONLINK}[data-id='${sectionId}']`);\n this._disableLink(currentElement);\n });\n\n // Setup keyboard navigation.\n new ContentTree(\n modalBody.querySelector(this.selectors.CONTENTTREE),\n {\n SECTION: this.selectors.SECTIONNODE,\n TOGGLER: this.selectors.MODALTOGGLER,\n COLLAPSE: this.selectors.MODALTOGGLER,\n },\n true\n );\n\n // Capture click.\n modalBody.addEventListener('click', (event) => {\n const target = event.target;\n if (!target.matches('a') || target.dataset.for != 'section' || target.dataset.id === undefined) {\n return;\n }\n if (target.getAttribute('aria-disabled')) {\n return;\n }\n event.preventDefault();\n this.reactive.dispatch('sectionMoveAfter', sectionIds, target.dataset.id);\n this._destroyModal(modal, editTools);\n });\n\n pendingModalReady.resolve();\n }\n\n /**\n * Handle a move cm request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestMoveCm(target, event) {\n // Check we have an id.\n const cmIds = this._getTargetIds(target);\n if (cmIds.length == 0) {\n return;\n }\n\n event.preventDefault();\n\n const pendingModalReady = new Pending(`courseformat/actions:prepareMoveCmModal`);\n\n // The section edit menu to refocus on end.\n const editTools = this._getClosestActionMenuToogler(target);\n\n // Collect information from the state.\n const exporter = this.reactive.getExporter();\n const data = exporter.course(this.reactive.state);\n\n let titleText = null;\n if (cmIds.length == 1) {\n const cmInfo = this.reactive.get('cm', cmIds[0]);\n data.cmid = cmInfo.id;\n data.cmname = cmInfo.name;\n data.information = await this.reactive.getFormatString('cmmove_info', data.cmname);\n titleText = this.reactive.getFormatString('cmmove_title');\n } else {\n data.information = await this.reactive.getFormatString('cmsmove_info', cmIds.length);\n titleText = this.reactive.getFormatString('cmsmove_title');\n }\n\n // Create the modal.\n // Build the modal parameters from the event data.\n const modal = await this._modalBodyRenderedPromise(Modal, {\n title: titleText,\n body: Templates.render('core_courseformat/local/content/movecm', data),\n });\n\n const modalBody = getFirst(modal.getBody());\n\n // Disable current selected section ids.\n cmIds.forEach(cmId => {\n const currentElement = modalBody.querySelector(`${this.selectors.CMLINK}[data-id='${cmId}']`);\n this._disableLink(currentElement);\n });\n\n // Setup keyboard navigation.\n new ContentTree(\n modalBody.querySelector(this.selectors.CONTENTTREE),\n {\n SECTION: this.selectors.SECTIONNODE,\n TOGGLER: this.selectors.MODALTOGGLER,\n COLLAPSE: this.selectors.MODALTOGGLER,\n ENTER: this.selectors.SECTIONLINK,\n }\n );\n\n // Open the cm section node if possible (Bootstrap 4 uses jQuery to interact with collapsibles).\n // All jQuery in this code can be replaced when MDL-71979 is integrated.\n cmIds.forEach(cmId => {\n const currentElement = modalBody.querySelector(`${this.selectors.CMLINK}[data-id='${cmId}']`);\n const sectionnode = currentElement.closest(this.selectors.SECTIONNODE);\n const toggler = jQuery(sectionnode).find(this.selectors.MODALTOGGLER);\n let collapsibleId = toggler.data('target') ?? toggler.attr('href');\n if (collapsibleId) {\n // We cannot be sure we have # in the id element name.\n collapsibleId = collapsibleId.replace('#', '');\n const expandNode = modalBody.querySelector(`#${collapsibleId}`);\n jQuery(expandNode).collapse('show');\n }\n });\n\n modalBody.addEventListener('click', (event) => {\n const target = event.target;\n if (!target.matches('a') || target.dataset.for === undefined || target.dataset.id === undefined) {\n return;\n }\n if (target.getAttribute('aria-disabled')) {\n return;\n }\n event.preventDefault();\n\n let targetSectionId;\n let targetCmId;\n if (target.dataset.for == 'cm') {\n const dropData = exporter.cmDraggableData(this.reactive.state, target.dataset.id);\n targetSectionId = dropData.sectionid;\n targetCmId = dropData.nextcmid;\n } else {\n const section = this.reactive.get('section', target.dataset.id);\n targetSectionId = target.dataset.id;\n targetCmId = section?.cmlist[0];\n }\n this.reactive.dispatch('cmMove', cmIds, targetSectionId, targetCmId);\n this._destroyModal(modal, editTools);\n });\n\n pendingModalReady.resolve();\n }\n\n /**\n * Handle a create section request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestAddSection(target, event) {\n event.preventDefault();\n this.reactive.dispatch('addSection', target.dataset.id ?? 0);\n }\n\n /**\n * Handle a delete section request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestDeleteSection(target, event) {\n const sectionIds = this._getTargetIds(target);\n if (sectionIds.length == 0) {\n return;\n }\n\n event.preventDefault();\n\n // We don't need confirmation to delete empty sections.\n let needsConfirmation = sectionIds.some(sectionId => {\n const sectionInfo = this.reactive.get('section', sectionId);\n const cmList = sectionInfo.cmlist ?? [];\n return (cmList.length || sectionInfo.hassummary || sectionInfo.rawtitle);\n });\n if (!needsConfirmation) {\n this._dispatchSectionDelete(sectionIds, target);\n return;\n }\n\n let bodyText = null;\n let titleText = null;\n if (sectionIds.length == 1) {\n titleText = this.reactive.getFormatString('sectiondelete_title');\n const sectionInfo = this.reactive.get('section', sectionIds[0]);\n bodyText = this.reactive.getFormatString('sectiondelete_info', {name: sectionInfo.title});\n } else {\n titleText = this.reactive.getFormatString('sectionsdelete_title');\n bodyText = this.reactive.getFormatString('sectionsdelete_info', {count: sectionIds.length});\n }\n\n const modal = await this._modalBodyRenderedPromise(ModalDeleteCancel, {\n title: titleText,\n body: bodyText,\n });\n\n modal.getRoot().on(\n ModalEvents.delete,\n e => {\n // Stop the default save button behaviour which is to close the modal.\n e.preventDefault();\n modal.destroy();\n this._dispatchSectionDelete(sectionIds, target);\n }\n );\n }\n\n /**\n * Dispatch the section delete action and handle the redirection if necessary.\n *\n * @param {Array} sectionIds the IDs of the sections to delete.\n * @param {Element} target the dispatch action element\n */\n async _dispatchSectionDelete(sectionIds, target) {\n await this.reactive.dispatch('sectionDelete', sectionIds);\n if (target.baseURI.includes('section.php')) {\n // Redirect to the course main page if the section is the current page.\n window.location.href = this.reactive.get('course').baseurl;\n }\n }\n\n /**\n * Handle a toggle cm selection.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestToggleSelectionCm(target, event) {\n toggleBulkSelectionAction(this.reactive, target, event, 'cm');\n }\n\n /**\n * Handle a toggle section selection.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestToggleSelectionSection(target, event) {\n toggleBulkSelectionAction(this.reactive, target, event, 'section');\n }\n\n /**\n * Basic mutation action helper.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n * @param {string} mutationName the mutation name\n */\n async _requestMutationAction(target, event, mutationName) {\n if (!target.dataset.id && target.dataset.for !== 'bulkaction') {\n return;\n }\n event.preventDefault();\n if (target.dataset.for === 'bulkaction') {\n // If the mutation is a bulk action we use the current selection.\n this.reactive.dispatch(mutationName, this.reactive.get('bulk').selection);\n } else {\n this.reactive.dispatch(mutationName, [target.dataset.id]);\n }\n }\n\n /**\n * Handle a course module duplicate request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestCmDuplicate(target, event) {\n const cmIds = this._getTargetIds(target);\n if (cmIds.length == 0) {\n return;\n }\n const sectionId = target.dataset.sectionid ?? null;\n event.preventDefault();\n this.reactive.dispatch('cmDuplicate', cmIds, sectionId);\n }\n\n /**\n * Handle a delete cm request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestCmDelete(target, event) {\n const cmIds = this._getTargetIds(target);\n if (cmIds.length == 0) {\n return;\n }\n\n event.preventDefault();\n\n let bodyText = null;\n let titleText = null;\n if (cmIds.length == 1) {\n const cmInfo = this.reactive.get('cm', cmIds[0]);\n titleText = getString('cmdelete_title', 'core_courseformat');\n bodyText = getString(\n 'cmdelete_info',\n 'core_courseformat',\n {\n type: cmInfo.modname,\n name: cmInfo.name,\n }\n );\n } else {\n titleText = getString('cmsdelete_title', 'core_courseformat');\n bodyText = getString(\n 'cmsdelete_info',\n 'core_courseformat',\n {count: cmIds.length}\n );\n }\n\n const modal = await this._modalBodyRenderedPromise(ModalDeleteCancel, {\n title: titleText,\n body: bodyText,\n });\n\n modal.getRoot().on(\n ModalEvents.delete,\n e => {\n // Stop the default save button behaviour which is to close the modal.\n e.preventDefault();\n modal.destroy();\n this.reactive.dispatch('cmDelete', cmIds);\n }\n );\n }\n\n /**\n * Handle a cm availability change request.\n *\n * @param {Element} target the dispatch action element\n */\n async _requestCmAvailability(target) {\n const cmIds = this._getTargetIds(target);\n if (cmIds.length == 0) {\n return;\n }\n // Show the availability modal to decide which action to trigger.\n const exporter = this.reactive.getExporter();\n const data = {\n allowstealth: exporter.canUseStealth(this.reactive.state, cmIds),\n };\n const modal = await this._modalBodyRenderedPromise(ModalSaveCancel, {\n title: getString('availability', 'core'),\n body: Templates.render('core_courseformat/local/content/cm/availabilitymodal', data),\n saveButtonText: getString('apply', 'core'),\n });\n\n this._setupMutationRadioButtonModal(modal, cmIds);\n }\n\n /**\n * Handle a section availability change request.\n *\n * @param {Element} target the dispatch action element\n */\n async _requestSectionAvailability(target) {\n const sectionIds = this._getTargetIds(target);\n if (sectionIds.length == 0) {\n return;\n }\n const title = (sectionIds.length == 1) ? 'sectionavailability_title' : 'sectionsavailability_title';\n // Show the availability modal to decide which action to trigger.\n const modal = await this._modalBodyRenderedPromise(ModalSaveCancel, {\n title: this.reactive.getFormatString(title),\n body: Templates.render('core_courseformat/local/content/section/availabilitymodal', []),\n saveButtonText: getString('apply', 'core'),\n });\n\n this._setupMutationRadioButtonModal(modal, sectionIds);\n }\n\n /**\n * Add events to a mutation selector radio buttons modal.\n * @param {Modal} modal\n * @param {Number[]} ids the section or cm ids to apply the mutation\n */\n _setupMutationRadioButtonModal(modal, ids) {\n // The save button is not enabled until the user selects an option.\n modal.setButtonDisabled('save', true);\n\n const submitFunction = (radio) => {\n const mutation = radio?.value;\n if (!mutation) {\n return false;\n }\n this.reactive.dispatch(mutation, ids);\n return true;\n };\n\n const modalBody = getFirst(modal.getBody());\n const radioOptions = modalBody.querySelectorAll(this.selectors.OPTIONSRADIO);\n radioOptions.forEach(radio => {\n radio.addEventListener('change', () => {\n modal.setButtonDisabled('save', false);\n });\n radio.parentNode.addEventListener('click', () => {\n radio.checked = true;\n modal.setButtonDisabled('save', false);\n });\n radio.parentNode.addEventListener('dblclick', dbClickEvent => {\n if (submitFunction(radio)) {\n dbClickEvent.preventDefault();\n modal.destroy();\n }\n });\n });\n\n modal.getRoot().on(\n ModalEvents.save,\n () => {\n const radio = modalBody.querySelector(`${this.selectors.OPTIONSRADIO}:checked`);\n submitFunction(radio);\n }\n );\n }\n\n /**\n * Disable all add sections actions.\n *\n * @param {boolean} locked the new locked value.\n */\n _setAddSectionLocked(locked) {\n const targets = this.getElements(this.selectors.ADDSECTION);\n targets.forEach(element => {\n element.classList.toggle(this.classes.DISABLED, locked);\n element.classList.toggle(this.classes.ITALIC, locked);\n this.setElementLocked(element, locked);\n });\n }\n\n /**\n * Replace an element with a copy with a different tag name.\n *\n * @param {Element} element the original element\n */\n _disableLink(element) {\n if (element) {\n element.style.pointerEvents = 'none';\n element.style.userSelect = 'none';\n element.classList.add(this.classes.DISABLED);\n element.classList.add(this.classes.ITALIC);\n element.setAttribute('aria-disabled', true);\n element.addEventListener('click', event => event.preventDefault());\n }\n }\n\n /**\n * Render a modal and return a body ready promise.\n *\n * @param {Modal} ModalClass the modal class\n * @param {object} modalParams the modal params\n * @return {Promise} the modal body ready promise\n */\n _modalBodyRenderedPromise(ModalClass, modalParams) {\n return new Promise((resolve, reject) => {\n ModalClass.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 if (modalParams.deleteButtonText !== undefined) {\n modal.setDeleteButtonText(modalParams.saveButtonText);\n }\n modal.show();\n return;\n }).catch(() => {\n reject(`Cannot load modal content`);\n });\n });\n }\n\n /**\n * Hide and later destroy a modal.\n *\n * Behat will fail if we remove the modal while some boostrap collapse is executing.\n *\n * @param {Modal} modal\n * @param {HTMLElement} element the dom element to focus on.\n */\n _destroyModal(modal, element) {\n modal.hide();\n const pendingDestroy = new Pending(`courseformat/actions:destroyModal`);\n if (element) {\n element.focus();\n }\n setTimeout(() =>{\n modal.destroy();\n pendingDestroy.resolve();\n }, 500);\n }\n\n /**\n * Get the closest actions menu toggler to an action element.\n *\n * @param {HTMLElement} element the action link element\n * @returns {HTMLElement|undefined}\n */\n _getClosestActionMenuToogler(element) {\n const actionMenu = element.closest(this.selectors.ACTIONMENU);\n if (!actionMenu) {\n return undefined;\n }\n return actionMenu.querySelector(this.selectors.ACTIONMENUTOGGLER);\n }\n}\n"],"names":["directMutations","sectionHide","sectionShow","cmHide","cmShow","cmStealth","cmMoveRight","cmMoveLeft","cmNoGroups","cmSeparateGroups","cmVisibleGroups","BaseComponent","create","name","selectors","ACTIONLINK","SECTIONLINK","CMLINK","SECTIONNODE","MODALTOGGLER","ADDSECTION","CONTENTTREE","ACTIONMENU","ACTIONMENUTOGGLER","OPTIONSRADIO","classes","DISABLED","ITALIC","actions","action","mutationReference","Object","entries","Error","stateReady","state","addEventListener","this","element","_dispatchClick","_checkSectionlist","CourseEvents","sectionRefreshed","getWatchers","watch","handler","event","target","closest","classList","contains","preventDefault","actionName","dataset","methodName","_actionMethodName","undefined","_requestMutationAction","requestName","charAt","toUpperCase","slice","_setAddSectionLocked","course","sectionlist","length","maxsections","_getTargetIds","ids","_target$dataset","id","push","bulkType","_target$dataset2","bulk","reactive","get","enabled","selectedType","selection","sectionIds","pendingModalReady","Pending","editTools","_getClosestActionMenuToogler","data","getExporter","titleText","sectionInfo","sectionid","sectiontitle","title","information","getFormatString","modal","_modalBodyRenderedPromise","Modal","body","Templates","render","modalBody","getBody","forEach","sectionId","currentElement","querySelector","_disableLink","ContentTree","SECTION","TOGGLER","COLLAPSE","matches","for","getAttribute","dispatch","_destroyModal","resolve","cmIds","exporter","cmInfo","cmid","cmname","cmId","ENTER","sectionnode","toggler","find","collapsibleId","attr","replace","expandNode","collapse","targetSectionId","targetCmId","dropData","cmDraggableData","nextcmid","section","cmlist","some","hassummary","rawtitle","_dispatchSectionDelete","bodyText","count","ModalDeleteCancel","getRoot","on","ModalEvents","delete","e","destroy","baseURI","includes","window","location","href","baseurl","mutationName","type","modname","allowstealth","canUseStealth","ModalSaveCancel","saveButtonText","_setupMutationRadioButtonModal","setButtonDisabled","submitFunction","radio","mutation","value","querySelectorAll","parentNode","checked","dbClickEvent","save","locked","getElements","toggle","setElementLocked","style","pointerEvents","userSelect","add","setAttribute","ModalClass","modalParams","Promise","reject","then","setRemoveOnClose","bodyRendered","setSaveButtonText","deleteButtonText","setDeleteButtonText","show","catch","hide","pendingDestroy","focus","setTimeout","actionMenu"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"actions.min.js","sources":["../../../src/local/content/actions.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 state actions dispatcher.\n *\n * This module captures all data-dispatch links in the course content and dispatch the proper\n * state mutation, including any confirmation and modal required.\n *\n * @module core_courseformat/local/content/actions\n * @class core_courseformat/local/content/actions\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 Modal from 'core/modal';\nimport ModalSaveCancel from 'core/modal_save_cancel';\nimport ModalDeleteCancel from 'core/modal_delete_cancel';\nimport ModalEvents from 'core/modal_events';\nimport Templates from 'core/templates';\nimport {prefetchStrings} from 'core/prefetch';\nimport {getString} from 'core/str';\nimport {getFirst} from 'core/normalise';\nimport {toggleBulkSelectionAction} from 'core_courseformat/local/content/actions/bulkselection';\nimport * as CourseEvents from 'core_course/events';\nimport Pending from 'core/pending';\nimport ContentTree from 'core_courseformat/local/courseeditor/contenttree';\n// The jQuery module is only used for interacting with Boostrap 4. It can we removed when MDL-71979 is integrated.\nimport jQuery from 'jquery';\nimport Notification from \"core/notification\";\n\n// Load global strings.\nprefetchStrings('core', ['movecoursesection', 'movecoursemodule', 'confirm', 'delete']);\n\n// Mutations are dispatched by the course content actions.\n// Formats can use this module addActions static method to add custom actions.\n// Direct mutations can be simple strings (mutation) name or functions.\nconst directMutations = {\n sectionHide: 'sectionHide',\n sectionShow: 'sectionShow',\n cmHide: 'cmHide',\n cmShow: 'cmShow',\n cmStealth: 'cmStealth',\n cmMoveRight: 'cmMoveRight',\n cmMoveLeft: 'cmMoveLeft',\n cmNoGroups: 'cmNoGroups',\n cmSeparateGroups: 'cmSeparateGroups',\n cmVisibleGroups: 'cmVisibleGroups',\n};\n\nexport default class extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'content_actions';\n // Default query selectors.\n this.selectors = {\n ACTIONLINK: `[data-action]`,\n // Move modal selectors.\n SECTIONLINK: `[data-for='section']`,\n CMLINK: `[data-for='cm']`,\n SECTIONNODE: `[data-for='sectionnode']`,\n MODALTOGGLER: `[data-toggle='collapse']`,\n ADDSECTION: `[data-action='addSection']`,\n CONTENTTREE: `#destination-selector`,\n ACTIONMENU: `.action-menu`,\n ACTIONMENUTOGGLER: `[data-toggle=\"dropdown\"]`,\n // Availability modal selectors.\n OPTIONSRADIO: `[type='radio']`,\n COURSEADDSECTION: `#course-addsection`,\n MAXSECTIONSWARNING: `[data-region='max-sections-warning']`,\n ADDSECTIONREGION: `[data-region='section-addsection']`,\n };\n // Component css classes.\n this.classes = {\n DISABLED: `disabled`,\n ITALIC: `font-italic`,\n DISPLAYNONE: `d-none`,\n };\n }\n\n /**\n * Add extra actions to the module.\n *\n * @param {array} actions array of methods to execute\n */\n static addActions(actions) {\n for (const [action, mutationReference] of Object.entries(actions)) {\n if (typeof mutationReference !== 'function' && typeof mutationReference !== 'string') {\n throw new Error(`${action} action must be a mutation name or a function`);\n }\n directMutations[action] = mutationReference;\n }\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the state data.\n *\n */\n stateReady(state) {\n // Delegate dispatch clicks.\n this.addEventListener(\n this.element,\n 'click',\n this._dispatchClick\n );\n // Check section limit.\n this._checkSectionlist({state});\n // Add an Event listener to recalculate limits it if a section HTML is altered.\n this.addEventListener(\n this.element,\n CourseEvents.sectionRefreshed,\n () => this._checkSectionlist({state})\n );\n }\n\n /**\n * Return the component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n // Check section limit.\n {watch: `course.sectionlist:updated`, handler: this._checkSectionlist},\n ];\n }\n\n _dispatchClick(event) {\n const target = event.target.closest(this.selectors.ACTIONLINK);\n if (!target) {\n return;\n }\n if (target.classList.contains(this.classes.DISABLED)) {\n event.preventDefault();\n return;\n }\n\n // Invoke proper method.\n const actionName = target.dataset.action;\n const methodName = this._actionMethodName(actionName);\n\n if (this[methodName] !== undefined) {\n this[methodName](target, event);\n return;\n }\n\n // Check direct mutations or mutations handlers.\n if (directMutations[actionName] !== undefined) {\n if (typeof directMutations[actionName] === 'function') {\n directMutations[actionName](target, event);\n return;\n }\n this._requestMutationAction(target, event, directMutations[actionName]);\n return;\n }\n }\n\n _actionMethodName(name) {\n const requestName = name.charAt(0).toUpperCase() + name.slice(1);\n return `_request${requestName}`;\n }\n\n /**\n * Check the section list and disable some options if needed.\n *\n * @param {Object} detail the update details.\n * @param {Object} detail.state the state object.\n */\n _checkSectionlist({state}) {\n // Disable \"add section\" actions if the course max sections has been exceeded.\n this._setAddSectionLocked(state.course.sectionlist.length > state.course.maxsections);\n }\n\n /**\n * Return the ids represented by this element.\n *\n * Depending on the dataset attributes the action could represent a single id\n * or a bulk actions with all the current selected ids.\n *\n * @param {HTMLElement} target\n * @returns {Number[]} array of Ids\n */\n _getTargetIds(target) {\n let ids = [];\n if (target?.dataset?.id) {\n ids.push(target.dataset.id);\n }\n const bulkType = target?.dataset?.bulk;\n if (!bulkType) {\n return ids;\n }\n const bulk = this.reactive.get('bulk');\n if (bulk.enabled && bulk.selectedType === bulkType) {\n ids = [...ids, ...bulk.selection];\n }\n return ids;\n }\n\n /**\n * Handle a move section request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestMoveSection(target, event) {\n // Check we have an id.\n const sectionIds = this._getTargetIds(target);\n if (sectionIds.length == 0) {\n return;\n }\n\n event.preventDefault();\n\n const pendingModalReady = new Pending(`courseformat/actions:prepareMoveSectionModal`);\n\n // The section edit menu to refocus on end.\n const editTools = this._getClosestActionMenuToogler(target);\n\n // Collect section information from the state.\n const exporter = this.reactive.getExporter();\n const data = exporter.course(this.reactive.state);\n let titleText = null;\n\n // Add the target section id and title.\n let sectionInfo = null;\n if (sectionIds.length == 1) {\n sectionInfo = this.reactive.get('section', sectionIds[0]);\n data.sectionid = sectionInfo.id;\n data.sectiontitle = sectionInfo.title;\n data.information = await this.reactive.getFormatString('sectionmove_info', data.sectiontitle);\n titleText = this.reactive.getFormatString('sectionmove_title');\n } else {\n data.information = await this.reactive.getFormatString('sectionsmove_info', sectionIds.length);\n titleText = this.reactive.getFormatString('sectionsmove_title');\n }\n\n\n // Create the modal.\n // Build the modal parameters from the event data.\n const modal = await this._modalBodyRenderedPromise(Modal, {\n title: titleText,\n body: Templates.render('core_courseformat/local/content/movesection', data),\n });\n\n const modalBody = getFirst(modal.getBody());\n\n // Disable current selected section ids.\n sectionIds.forEach(sectionId => {\n const currentElement = modalBody.querySelector(`${this.selectors.SECTIONLINK}[data-id='${sectionId}']`);\n this._disableLink(currentElement);\n });\n\n // Setup keyboard navigation.\n new ContentTree(\n modalBody.querySelector(this.selectors.CONTENTTREE),\n {\n SECTION: this.selectors.SECTIONNODE,\n TOGGLER: this.selectors.MODALTOGGLER,\n COLLAPSE: this.selectors.MODALTOGGLER,\n },\n true\n );\n\n // Capture click.\n modalBody.addEventListener('click', (event) => {\n const target = event.target;\n if (!target.matches('a') || target.dataset.for != 'section' || target.dataset.id === undefined) {\n return;\n }\n if (target.getAttribute('aria-disabled')) {\n return;\n }\n event.preventDefault();\n this.reactive.dispatch('sectionMoveAfter', sectionIds, target.dataset.id);\n this._destroyModal(modal, editTools);\n });\n\n pendingModalReady.resolve();\n }\n\n /**\n * Handle a move cm request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestMoveCm(target, event) {\n // Check we have an id.\n const cmIds = this._getTargetIds(target);\n if (cmIds.length == 0) {\n return;\n }\n\n event.preventDefault();\n\n const pendingModalReady = new Pending(`courseformat/actions:prepareMoveCmModal`);\n\n // The section edit menu to refocus on end.\n const editTools = this._getClosestActionMenuToogler(target);\n\n // Collect information from the state.\n const exporter = this.reactive.getExporter();\n const data = exporter.course(this.reactive.state);\n\n let titleText = null;\n if (cmIds.length == 1) {\n const cmInfo = this.reactive.get('cm', cmIds[0]);\n data.cmid = cmInfo.id;\n data.cmname = cmInfo.name;\n data.information = await this.reactive.getFormatString('cmmove_info', data.cmname);\n titleText = this.reactive.getFormatString('cmmove_title');\n } else {\n data.information = await this.reactive.getFormatString('cmsmove_info', cmIds.length);\n titleText = this.reactive.getFormatString('cmsmove_title');\n }\n\n // Create the modal.\n // Build the modal parameters from the event data.\n const modal = await this._modalBodyRenderedPromise(Modal, {\n title: titleText,\n body: Templates.render('core_courseformat/local/content/movecm', data),\n });\n\n const modalBody = getFirst(modal.getBody());\n\n // Disable current selected section ids.\n cmIds.forEach(cmId => {\n const currentElement = modalBody.querySelector(`${this.selectors.CMLINK}[data-id='${cmId}']`);\n this._disableLink(currentElement);\n });\n\n // Setup keyboard navigation.\n new ContentTree(\n modalBody.querySelector(this.selectors.CONTENTTREE),\n {\n SECTION: this.selectors.SECTIONNODE,\n TOGGLER: this.selectors.MODALTOGGLER,\n COLLAPSE: this.selectors.MODALTOGGLER,\n ENTER: this.selectors.SECTIONLINK,\n }\n );\n\n // Open the cm section node if possible (Bootstrap 4 uses jQuery to interact with collapsibles).\n // All jQuery in this code can be replaced when MDL-71979 is integrated.\n cmIds.forEach(cmId => {\n const cmInfo = this.reactive.get('cm', cmId);\n let selector;\n if (!cmInfo.hasdelegatedsection) {\n selector = `${this.selectors.CMLINK}[data-id='${cmId}']`;\n } else {\n selector = `${this.selectors.SECTIONLINK}[data-id='${cmInfo.sectionid}']`;\n }\n const currentElement = modalBody.querySelector(selector);\n this._expandCmMoveModalParentSections(modalBody, currentElement);\n });\n\n modalBody.addEventListener('click', (event) => {\n const target = event.target;\n if (!target.matches('a') || target.dataset.for === undefined || target.dataset.id === undefined) {\n return;\n }\n if (target.getAttribute('aria-disabled')) {\n return;\n }\n event.preventDefault();\n\n let targetSectionId;\n let targetCmId;\n let droppedCmIds = [...cmIds];\n if (target.dataset.for == 'cm') {\n const dropData = exporter.cmDraggableData(this.reactive.state, target.dataset.id);\n targetSectionId = dropData.sectionid;\n targetCmId = dropData.nextcmid;\n } else {\n const section = this.reactive.get('section', target.dataset.id);\n targetSectionId = target.dataset.id;\n targetCmId = section?.cmlist[0];\n }\n const section = this.reactive.get('section', targetSectionId);\n if (section.component) {\n // Remove cmIds which are not allowed to be moved to this delegated section (mostly\n // all other delegated cm).\n droppedCmIds = droppedCmIds.filter(cmId => {\n const cmInfo = this.reactive.get('cm', cmId);\n return !cmInfo.hasdelegatedsection;\n });\n }\n if (droppedCmIds.length === 0) {\n return; // No cm to move.\n }\n this.reactive.dispatch('cmMove', droppedCmIds, targetSectionId, targetCmId);\n this._destroyModal(modal, editTools);\n });\n\n pendingModalReady.resolve();\n }\n\n /**\n * Expand all the modal tree branches that contains the element.\n *\n * Bootstrap 4 uses jQuery to interact with collapsibles.\n * All jQuery in this code can be replaced when MDL-71979 is integrated.\n *\n * @private\n * @param {HTMLElement} modalBody the modal body element\n * @param {HTMLElement} element the element to display\n */\n _expandCmMoveModalParentSections(modalBody, element) {\n const sectionnode = element.closest(this.selectors.SECTIONNODE);\n if (!sectionnode) {\n return;\n }\n\n const toggler = jQuery(sectionnode).find(this.selectors.MODALTOGGLER);\n let collapsibleId = toggler.data('target') ?? toggler.attr('href');\n if (collapsibleId) {\n // We cannot be sure we have # in the id element name.\n collapsibleId = collapsibleId.replace('#', '');\n const expandNode = modalBody.querySelector(`#${collapsibleId}`);\n jQuery(expandNode).collapse('show');\n }\n\n // Section are a tree structure, we need to expand all the parents.\n this._expandCmMoveModalParentSections(modalBody, sectionnode.parentElement);\n }\n\n /**\n * Handle a create section request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestAddSection(target, event) {\n event.preventDefault();\n this.reactive.dispatch('addSection', target.dataset.id ?? 0);\n }\n\n /**\n * Handle a delete section request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestDeleteSection(target, event) {\n const sectionIds = this._getTargetIds(target);\n if (sectionIds.length == 0) {\n return;\n }\n\n event.preventDefault();\n\n // We don't need confirmation to delete empty sections.\n let needsConfirmation = sectionIds.some(sectionId => {\n const sectionInfo = this.reactive.get('section', sectionId);\n const cmList = sectionInfo.cmlist ?? [];\n return (cmList.length || sectionInfo.hassummary || sectionInfo.rawtitle);\n });\n if (!needsConfirmation) {\n this._dispatchSectionDelete(sectionIds, target);\n return;\n }\n\n let bodyText = null;\n let titleText = null;\n if (sectionIds.length == 1) {\n titleText = this.reactive.getFormatString('sectiondelete_title');\n const sectionInfo = this.reactive.get('section', sectionIds[0]);\n bodyText = this.reactive.getFormatString('sectiondelete_info', {name: sectionInfo.title});\n } else {\n titleText = this.reactive.getFormatString('sectionsdelete_title');\n bodyText = this.reactive.getFormatString('sectionsdelete_info', {count: sectionIds.length});\n }\n\n const modal = await this._modalBodyRenderedPromise(ModalDeleteCancel, {\n title: titleText,\n body: bodyText,\n });\n\n modal.getRoot().on(\n ModalEvents.delete,\n e => {\n // Stop the default save button behaviour which is to close the modal.\n e.preventDefault();\n modal.destroy();\n this._dispatchSectionDelete(sectionIds, target);\n }\n );\n }\n\n /**\n * Dispatch the section delete action and handle the redirection if necessary.\n *\n * @param {Array} sectionIds the IDs of the sections to delete.\n * @param {Element} target the dispatch action element\n */\n async _dispatchSectionDelete(sectionIds, target) {\n await this.reactive.dispatch('sectionDelete', sectionIds);\n if (target.baseURI.includes('section.php')) {\n // Redirect to the course main page if the section is the current page.\n window.location.href = this.reactive.get('course').baseurl;\n }\n }\n\n /**\n * Handle a toggle cm selection.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestToggleSelectionCm(target, event) {\n toggleBulkSelectionAction(this.reactive, target, event, 'cm');\n }\n\n /**\n * Handle a toggle section selection.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestToggleSelectionSection(target, event) {\n toggleBulkSelectionAction(this.reactive, target, event, 'section');\n }\n\n /**\n * Basic mutation action helper.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n * @param {string} mutationName the mutation name\n */\n async _requestMutationAction(target, event, mutationName) {\n if (!target.dataset.id && target.dataset.for !== 'bulkaction') {\n return;\n }\n event.preventDefault();\n if (target.dataset.for === 'bulkaction') {\n // If the mutation is a bulk action we use the current selection.\n this.reactive.dispatch(mutationName, this.reactive.get('bulk').selection);\n } else {\n this.reactive.dispatch(mutationName, [target.dataset.id]);\n }\n }\n\n /**\n * Handle a course module duplicate request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestCmDuplicate(target, event) {\n const cmIds = this._getTargetIds(target);\n if (cmIds.length == 0) {\n return;\n }\n const sectionId = target.dataset.sectionid ?? null;\n event.preventDefault();\n this.reactive.dispatch('cmDuplicate', cmIds, sectionId);\n }\n\n /**\n * Handle a delete cm request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestCmDelete(target, event) {\n const cmIds = this._getTargetIds(target);\n if (cmIds.length == 0) {\n return;\n }\n\n event.preventDefault();\n\n let bodyText = null;\n let titleText = null;\n if (cmIds.length == 1) {\n const cmInfo = this.reactive.get('cm', cmIds[0]);\n titleText = getString('cmdelete_title', 'core_courseformat');\n bodyText = getString(\n 'cmdelete_info',\n 'core_courseformat',\n {\n type: cmInfo.modname,\n name: cmInfo.name,\n }\n );\n } else {\n titleText = getString('cmsdelete_title', 'core_courseformat');\n bodyText = getString(\n 'cmsdelete_info',\n 'core_courseformat',\n {count: cmIds.length}\n );\n }\n\n const modal = await this._modalBodyRenderedPromise(ModalDeleteCancel, {\n title: titleText,\n body: bodyText,\n });\n\n modal.getRoot().on(\n ModalEvents.delete,\n e => {\n // Stop the default save button behaviour which is to close the modal.\n e.preventDefault();\n modal.destroy();\n this.reactive.dispatch('cmDelete', cmIds);\n }\n );\n }\n\n /**\n * Handle a cm availability change request.\n *\n * @param {Element} target the dispatch action element\n */\n async _requestCmAvailability(target) {\n const cmIds = this._getTargetIds(target);\n if (cmIds.length == 0) {\n return;\n }\n // Show the availability modal to decide which action to trigger.\n const exporter = this.reactive.getExporter();\n const data = {\n allowstealth: exporter.canUseStealth(this.reactive.state, cmIds),\n };\n const modal = await this._modalBodyRenderedPromise(ModalSaveCancel, {\n title: getString('availability', 'core'),\n body: Templates.render('core_courseformat/local/content/cm/availabilitymodal', data),\n saveButtonText: getString('apply', 'core'),\n });\n\n this._setupMutationRadioButtonModal(modal, cmIds);\n }\n\n /**\n * Handle a section availability change request.\n *\n * @param {Element} target the dispatch action element\n */\n async _requestSectionAvailability(target) {\n const sectionIds = this._getTargetIds(target);\n if (sectionIds.length == 0) {\n return;\n }\n const title = (sectionIds.length == 1) ? 'sectionavailability_title' : 'sectionsavailability_title';\n // Show the availability modal to decide which action to trigger.\n const modal = await this._modalBodyRenderedPromise(ModalSaveCancel, {\n title: this.reactive.getFormatString(title),\n body: Templates.render('core_courseformat/local/content/section/availabilitymodal', []),\n saveButtonText: getString('apply', 'core'),\n });\n\n this._setupMutationRadioButtonModal(modal, sectionIds);\n }\n\n /**\n * Add events to a mutation selector radio buttons modal.\n * @param {Modal} modal\n * @param {Number[]} ids the section or cm ids to apply the mutation\n */\n _setupMutationRadioButtonModal(modal, ids) {\n // The save button is not enabled until the user selects an option.\n modal.setButtonDisabled('save', true);\n\n const submitFunction = (radio) => {\n const mutation = radio?.value;\n if (!mutation) {\n return false;\n }\n this.reactive.dispatch(mutation, ids);\n return true;\n };\n\n const modalBody = getFirst(modal.getBody());\n const radioOptions = modalBody.querySelectorAll(this.selectors.OPTIONSRADIO);\n radioOptions.forEach(radio => {\n radio.addEventListener('change', () => {\n modal.setButtonDisabled('save', false);\n });\n radio.parentNode.addEventListener('click', () => {\n radio.checked = true;\n modal.setButtonDisabled('save', false);\n });\n radio.parentNode.addEventListener('dblclick', dbClickEvent => {\n if (submitFunction(radio)) {\n dbClickEvent.preventDefault();\n modal.destroy();\n }\n });\n });\n\n modal.getRoot().on(\n ModalEvents.save,\n () => {\n const radio = modalBody.querySelector(`${this.selectors.OPTIONSRADIO}:checked`);\n submitFunction(radio);\n }\n );\n }\n\n /**\n * Disable all add sections actions.\n *\n * @param {boolean} locked the new locked value.\n */\n _setAddSectionLocked(locked) {\n const targets = this.getElements(this.selectors.ADDSECTIONREGION);\n targets.forEach(element => {\n element.classList.toggle(this.classes.DISABLED, locked);\n const addSectionElement = element.querySelector(this.selectors.ADDSECTION);\n addSectionElement.classList.toggle(this.classes.DISABLED, locked);\n this.setElementLocked(addSectionElement, locked);\n // We tweak the element to show a tooltip as a title attribute.\n if (locked) {\n getString('sectionaddmax', 'core_courseformat')\n .then((text) => addSectionElement.setAttribute('title', text))\n .catch(Notification.exception);\n addSectionElement.style.pointerEvents = null; // Unlocks the pointer events.\n addSectionElement.style.userSelect = null; // Unlocks the pointer events.\n } else {\n addSectionElement.setAttribute('title', addSectionElement.dataset.addSections);\n }\n });\n const courseAddSection = this.getElement(this.selectors.COURSEADDSECTION);\n const addSection = courseAddSection.querySelector(this.selectors.ADDSECTION);\n addSection.classList.toggle(this.classes.DISPLAYNONE, locked);\n const noMoreSections = courseAddSection.querySelector(this.selectors.MAXSECTIONSWARNING);\n noMoreSections.classList.toggle(this.classes.DISPLAYNONE, !locked);\n }\n\n /**\n * Replace an element with a copy with a different tag name.\n *\n * @param {Element} element the original element\n */\n _disableLink(element) {\n if (element) {\n element.style.pointerEvents = 'none';\n element.style.userSelect = 'none';\n element.classList.add(this.classes.DISABLED);\n element.classList.add(this.classes.ITALIC);\n element.setAttribute('aria-disabled', true);\n element.addEventListener('click', event => event.preventDefault());\n }\n }\n\n /**\n * Render a modal and return a body ready promise.\n *\n * @param {Modal} ModalClass the modal class\n * @param {object} modalParams the modal params\n * @return {Promise} the modal body ready promise\n */\n _modalBodyRenderedPromise(ModalClass, modalParams) {\n return new Promise((resolve, reject) => {\n ModalClass.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 if (modalParams.deleteButtonText !== undefined) {\n modal.setDeleteButtonText(modalParams.saveButtonText);\n }\n modal.show();\n return;\n }).catch(() => {\n reject(`Cannot load modal content`);\n });\n });\n }\n\n /**\n * Hide and later destroy a modal.\n *\n * Behat will fail if we remove the modal while some boostrap collapse is executing.\n *\n * @param {Modal} modal\n * @param {HTMLElement} element the dom element to focus on.\n */\n _destroyModal(modal, element) {\n modal.hide();\n const pendingDestroy = new Pending(`courseformat/actions:destroyModal`);\n if (element) {\n element.focus();\n }\n setTimeout(() =>{\n modal.destroy();\n pendingDestroy.resolve();\n }, 500);\n }\n\n /**\n * Get the closest actions menu toggler to an action element.\n *\n * @param {HTMLElement} element the action link element\n * @returns {HTMLElement|undefined}\n */\n _getClosestActionMenuToogler(element) {\n const actionMenu = element.closest(this.selectors.ACTIONMENU);\n if (!actionMenu) {\n return undefined;\n }\n return actionMenu.querySelector(this.selectors.ACTIONMENUTOGGLER);\n }\n}\n"],"names":["directMutations","sectionHide","sectionShow","cmHide","cmShow","cmStealth","cmMoveRight","cmMoveLeft","cmNoGroups","cmSeparateGroups","cmVisibleGroups","BaseComponent","create","name","selectors","ACTIONLINK","SECTIONLINK","CMLINK","SECTIONNODE","MODALTOGGLER","ADDSECTION","CONTENTTREE","ACTIONMENU","ACTIONMENUTOGGLER","OPTIONSRADIO","COURSEADDSECTION","MAXSECTIONSWARNING","ADDSECTIONREGION","classes","DISABLED","ITALIC","DISPLAYNONE","actions","action","mutationReference","Object","entries","Error","stateReady","state","addEventListener","this","element","_dispatchClick","_checkSectionlist","CourseEvents","sectionRefreshed","getWatchers","watch","handler","event","target","closest","classList","contains","preventDefault","actionName","dataset","methodName","_actionMethodName","undefined","_requestMutationAction","requestName","charAt","toUpperCase","slice","_setAddSectionLocked","course","sectionlist","length","maxsections","_getTargetIds","ids","_target$dataset","id","push","bulkType","_target$dataset2","bulk","reactive","get","enabled","selectedType","selection","sectionIds","pendingModalReady","Pending","editTools","_getClosestActionMenuToogler","data","getExporter","titleText","sectionInfo","sectionid","sectiontitle","title","information","getFormatString","modal","_modalBodyRenderedPromise","Modal","body","Templates","render","modalBody","getBody","forEach","sectionId","currentElement","querySelector","_disableLink","ContentTree","SECTION","TOGGLER","COLLAPSE","matches","for","getAttribute","dispatch","_destroyModal","resolve","cmIds","exporter","cmInfo","cmid","cmname","cmId","ENTER","selector","hasdelegatedsection","_expandCmMoveModalParentSections","targetSectionId","targetCmId","droppedCmIds","dropData","cmDraggableData","nextcmid","section","cmlist","component","filter","sectionnode","toggler","find","collapsibleId","attr","replace","expandNode","collapse","parentElement","some","hassummary","rawtitle","_dispatchSectionDelete","bodyText","count","ModalDeleteCancel","getRoot","on","ModalEvents","delete","e","destroy","baseURI","includes","window","location","href","baseurl","mutationName","type","modname","allowstealth","canUseStealth","ModalSaveCancel","saveButtonText","_setupMutationRadioButtonModal","setButtonDisabled","submitFunction","radio","mutation","value","querySelectorAll","parentNode","checked","dbClickEvent","save","locked","getElements","toggle","addSectionElement","setElementLocked","then","text","setAttribute","catch","Notification","exception","style","pointerEvents","userSelect","addSections","courseAddSection","getElement","add","ModalClass","modalParams","Promise","reject","setRemoveOnClose","bodyRendered","setSaveButtonText","deleteButtonText","setDeleteButtonText","show","hide","pendingDestroy","focus","setTimeout","actionMenu"],"mappings":"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"} \ No newline at end of file diff --git a/course/format/amd/build/local/content/section.min.js b/course/format/amd/build/local/content/section.min.js index 5b97746cd2108..f1a80fad77c15 100644 --- a/course/format/amd/build/local/content/section.min.js +++ b/course/format/amd/build/local/content/section.min.js @@ -6,6 +6,6 @@ define("core_courseformat/local/content/section",["exports","core_courseformat/l * @class core_courseformat/local/content/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,_header=_interopRequireDefault(_header),_dndsection=_interopRequireDefault(_dndsection),_templates=_interopRequireDefault(_templates),_pending=_interopRequireDefault(_pending);class _default extends _dndsection.default{create(){this.name="content_section",this.selectors={SECTION_ITEM:"[data-for='section_title']",CM:'[data-for="cmitem"]',SECTIONINFO:'[data-for="sectioninfo"]',SECTIONBADGES:'[data-region="sectionbadges"]',SHOWSECTION:'[data-action="sectionShow"]',HIDESECTION:'[data-action="sectionHide"]',ACTIONTEXT:".menu-action-text",ICON:".icon"},this.classes={LOCKED:"editinprogress",HASDESCRIPTION:"description",HIDE:"d-none",HIDDEN:"hidden",CURRENT:"current"},this.id=this.element.dataset.id}stateReady(state){if(this.configState(state),this.reactive.isEditing&&this.reactive.supportComponents){const sectionItem=this.getElement(this.selectors.SECTION_ITEM);if(sectionItem){const headerComponent=new _header.default({...this,element:sectionItem,fullregion:this.element});this.configDragDrop(headerComponent)}}this._openSectionIfNecessary()}async _openSectionIfNecessary(){const pageCmInfo=this.reactive.getPageAnchorCmInfo();if(!pageCmInfo||pageCmInfo.sectionid!==this.id)return;await this.reactive.dispatch("sectionContentCollapsed",[this.id],!1);const pendingOpen=new _pending.default("courseformat/section:openSectionIfNecessary");this.element.scrollIntoView({block:"center"}),setTimeout((()=>{this.reactive.dispatch("setPageItem","cm",pageCmInfo.id),pendingOpen.resolve()}),250)}getWatchers(){return[{watch:"section[".concat(this.id,"]:updated"),handler:this._refreshSection}]}validateDropData(dropdata){return("section"!==(null==dropdata?void 0:dropdata.type)||null===this.reactive.sectionReturn)&&super.validateDropData(dropdata)}getLastCm(){const cms=this.getElements(this.selectors.CM);return cms&&0!==cms.length?cms[cms.length-1]:null}_refreshSection(_ref){var _element$dragging,_element$locked,_element$visible,_element$current;let{element:element}=_ref;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.HIDDEN,null!==(_element$visible=!element.visible)&&void 0!==_element$visible&&_element$visible),this.element.classList.toggle(this.classes.CURRENT,null!==(_element$current=element.current)&&void 0!==_element$current&&_element$current),this.locked=element.locked;const sectioninfo=this.getElement(this.selectors.SECTIONINFO);sectioninfo&§ioninfo.classList.toggle(this.classes.HASDESCRIPTION,element.hasrestrictions),this._updateBadges(element),this._updateActionsMenu(element)}_updateBadges(section){const current=this.getElement("".concat(this.selectors.SECTIONBADGES," [data-type='iscurrent']"));null==current||current.classList.toggle(this.classes.HIDE,!section.current);const hiddenFromStudents=this.getElement("".concat(this.selectors.SECTIONBADGES," [data-type='hiddenfromstudents']"));null==hiddenFromStudents||hiddenFromStudents.classList.toggle(this.classes.HIDE,section.visible)}async _updateActionsMenu(section){var _affectedAction$datas,_affectedAction$datas2;let selector,newAction;section.visible?(selector=this.selectors.SHOWSECTION,newAction="sectionHide"):(selector=this.selectors.HIDESECTION,newAction="sectionShow");const affectedAction=this._getActionMenu(selector);if(!affectedAction)return;affectedAction.dataset.action=newAction;const actionText=affectedAction.querySelector(this.selectors.ACTIONTEXT);if(null!==(_affectedAction$datas=affectedAction.dataset)&&void 0!==_affectedAction$datas&&_affectedAction$datas.swapname&&actionText){const oldText=null==actionText?void 0:actionText.innerText;actionText.innerText=affectedAction.dataset.swapname,affectedAction.dataset.swapname=oldText}const icon=affectedAction.querySelector(this.selectors.ICON);if(null!==(_affectedAction$datas2=affectedAction.dataset)&&void 0!==_affectedAction$datas2&&_affectedAction$datas2.swapicon&&icon){const newIcon=affectedAction.dataset.swapicon;if(newIcon){const pixHtml=await _templates.default.renderPix(newIcon,"core");_templates.default.replaceNode(icon,pixHtml,"")}}}_getActionMenu(selector){return this.getElement(".section_action_menu")?this.getElement(selector):document.querySelector(selector)}}return _exports.default=_default,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_header=_interopRequireDefault(_header),_dndsection=_interopRequireDefault(_dndsection),_templates=_interopRequireDefault(_templates),_pending=_interopRequireDefault(_pending);class _default extends _dndsection.default{create(){this.name="content_section",this.selectors={ACTIONMENU:".section-actions",SECTION_ITEM:"[data-for='section_title']",CM:'[data-for="cmitem"]',SECTIONINFO:'[data-for="sectioninfo"]',SECTIONBADGES:'[data-region="sectionbadges"]',SHOWSECTION:'[data-action="sectionShow"]',HIDESECTION:'[data-action="sectionHide"]',ACTIONTEXT:".menu-action-text",ICON:".icon"},this.classes={LOCKED:"editinprogress",HASDESCRIPTION:"description",HIDE:"d-none",HIDDEN:"hidden",CURRENT:"current"},this.id=this.element.dataset.id}stateReady(state){if(this.configState(state),this.reactive.isEditing&&this.reactive.supportComponents){const sectionItem=this.getElement(this.selectors.SECTION_ITEM);if(sectionItem){const headerComponent=new _header.default({...this,element:sectionItem,fullregion:this.element});this.configDragDrop(headerComponent)}}this._openSectionIfNecessary()}async _openSectionIfNecessary(){const pageCmInfo=this.reactive.getPageAnchorCmInfo();if(!pageCmInfo||pageCmInfo.sectionid!==this.id)return;await this.reactive.dispatch("sectionContentCollapsed",[this.id],!1);const pendingOpen=new _pending.default("courseformat/section:openSectionIfNecessary");this.element.scrollIntoView({block:"center"}),setTimeout((()=>{this.reactive.dispatch("setPageItem","cm",pageCmInfo.id),pendingOpen.resolve()}),250)}getWatchers(){return[{watch:"section[".concat(this.id,"]:updated"),handler:this._refreshSection}]}validateDropData(dropdata){return("section"!==(null==dropdata?void 0:dropdata.type)||null===this.reactive.sectionReturn)&&super.validateDropData(dropdata)}getLastCm(){const cms=this.getElements(this.selectors.CM);if(!cms||0===cms.length)return null;const lastCm=cms[cms.length-1];if(null!==this.section.component)return lastCm;const parentSection=lastCm.parentNode.closest(this.selectors.CM);return null!=parentSection?parentSection:lastCm}getLastCmFallback(){return this.getElement(this.selectors.SECTIONINFO)}_refreshSection(_ref){var _element$dragging,_element$locked,_element$visible,_element$current;let{element:element}=_ref;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.HIDDEN,null!==(_element$visible=!element.visible)&&void 0!==_element$visible&&_element$visible),this.element.classList.toggle(this.classes.CURRENT,null!==(_element$current=element.current)&&void 0!==_element$current&&_element$current),this.locked=element.locked;const sectioninfo=this.getElement(this.selectors.SECTIONINFO);sectioninfo&§ioninfo.classList.toggle(this.classes.HASDESCRIPTION,element.hasrestrictions),this._updateBadges(element),this._updateActionsMenu(element)}_updateBadges(section){const current=this.getElement("".concat(this.selectors.SECTIONBADGES," [data-type='iscurrent']"));null==current||current.classList.toggle(this.classes.HIDE,!section.current);const hiddenFromStudents=this.getElement("".concat(this.selectors.SECTIONBADGES," [data-type='hiddenfromstudents']"));null==hiddenFromStudents||hiddenFromStudents.classList.toggle(this.classes.HIDE,section.visible)}async _updateActionsMenu(section){var _affectedAction$datas,_affectedAction$datas2;let selector,newAction;section.visible?(selector=this.selectors.SHOWSECTION,newAction="sectionHide"):(selector=this.selectors.HIDESECTION,newAction="sectionShow");const affectedAction=this._getActionMenu(selector);if(!affectedAction)return;affectedAction.dataset.action=newAction;const actionText=affectedAction.querySelector(this.selectors.ACTIONTEXT);if(null!==(_affectedAction$datas=affectedAction.dataset)&&void 0!==_affectedAction$datas&&_affectedAction$datas.swapname&&actionText){const oldText=null==actionText?void 0:actionText.innerText;actionText.innerText=affectedAction.dataset.swapname,affectedAction.dataset.swapname=oldText}const icon=affectedAction.querySelector(this.selectors.ICON);if(null!==(_affectedAction$datas2=affectedAction.dataset)&&void 0!==_affectedAction$datas2&&_affectedAction$datas2.swapicon&&icon){const newIcon=affectedAction.dataset.swapicon;if(affectedAction.dataset.swapicon=affectedAction.dataset.icon,affectedAction.dataset.icon=newIcon,newIcon){const pixHtml=await _templates.default.renderPix(newIcon,"core");_templates.default.replaceNode(icon,pixHtml,"")}}}_getActionMenu(selector){return document.querySelector("".concat(this.selectors.ACTIONMENU,"[data-sectionid='").concat(this.id,"'] ").concat(selector))}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=section.min.js.map \ No newline at end of file diff --git a/course/format/amd/build/local/content/section.min.js.map b/course/format/amd/build/local/content/section.min.js.map index bcab747c35694..5b7699f96e767 100644 --- a/course/format/amd/build/local/content/section.min.js.map +++ b/course/format/amd/build/local/content/section.min.js.map @@ -1 +1 @@ -{"version":3,"file":"section.min.js","sources":["../../../src/local/content/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 section format component.\n *\n * @module core_courseformat/local/content/section\n * @class core_courseformat/local/content/section\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Header from 'core_courseformat/local/content/section/header';\nimport DndSection from 'core_courseformat/local/courseeditor/dndsection';\nimport Templates from 'core/templates';\nimport Pending from \"core/pending\";\n\nexport default class extends DndSection {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'content_section';\n // Default query selectors.\n this.selectors = {\n SECTION_ITEM: `[data-for='section_title']`,\n CM: `[data-for=\"cmitem\"]`,\n SECTIONINFO: `[data-for=\"sectioninfo\"]`,\n SECTIONBADGES: `[data-region=\"sectionbadges\"]`,\n SHOWSECTION: `[data-action=\"sectionShow\"]`,\n HIDESECTION: `[data-action=\"sectionHide\"]`,\n ACTIONTEXT: `.menu-action-text`,\n ICON: `.icon`,\n };\n // Most classes will be loaded later by DndCmItem.\n this.classes = {\n LOCKED: 'editinprogress',\n HASDESCRIPTION: 'description',\n HIDE: 'd-none',\n HIDDEN: 'hidden',\n CURRENT: 'current',\n };\n\n // We need our id to watch specific events.\n this.id = this.element.dataset.id;\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 // Drag and drop is only available for components compatible course formats.\n if (this.reactive.isEditing && this.reactive.supportComponents) {\n // Section zero and other formats sections may not have a title to drag.\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n if (sectionItem) {\n // Init the inner dragable element.\n const headerComponent = new Header({\n ...this,\n element: sectionItem,\n fullregion: this.element,\n });\n this.configDragDrop(headerComponent);\n }\n }\n this._openSectionIfNecessary();\n }\n\n /**\n * Open the section if the anchored activity is inside.\n */\n async _openSectionIfNecessary() {\n const pageCmInfo = this.reactive.getPageAnchorCmInfo();\n if (!pageCmInfo || pageCmInfo.sectionid !== this.id) {\n return;\n }\n await this.reactive.dispatch('sectionContentCollapsed', [this.id], false);\n const pendingOpen = new Pending(`courseformat/section:openSectionIfNecessary`);\n this.element.scrollIntoView({block: \"center\"});\n setTimeout(() => {\n this.reactive.dispatch('setPageItem', 'cm', pageCmInfo.id);\n pendingOpen.resolve();\n }, 250);\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `section[${this.id}]:updated`, handler: this._refreshSection},\n ];\n }\n\n /**\n * Validate if the drop data can be dropped over the component.\n *\n * @param {Object} dropdata the exported drop data.\n * @returns {boolean}\n */\n validateDropData(dropdata) {\n // If the format uses one section per page sections dropping in the content is ignored.\n if (dropdata?.type === 'section' && this.reactive.sectionReturn !== null) {\n return false;\n }\n return super.validateDropData(dropdata);\n }\n\n /**\n * Get the last CM element of that section.\n *\n * @returns {element|null}\n */\n getLastCm() {\n const cms = this.getElements(this.selectors.CM);\n // DndUpload may add extra elements so :last-child selector cannot be used.\n if (!cms || cms.length === 0) {\n return null;\n }\n return cms[cms.length - 1];\n }\n\n /**\n * Update a content section using the state information.\n *\n * @param {object} param\n * @param {Object} param.element details the update details.\n */\n _refreshSection({element}) {\n // Update classes.\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.HIDDEN, !element.visible ?? false);\n this.element.classList.toggle(this.classes.CURRENT, element.current ?? false);\n this.locked = element.locked;\n // The description box classes depends on the section state.\n const sectioninfo = this.getElement(this.selectors.SECTIONINFO);\n if (sectioninfo) {\n sectioninfo.classList.toggle(this.classes.HASDESCRIPTION, element.hasrestrictions);\n }\n // Update section badges and menus.\n this._updateBadges(element);\n this._updateActionsMenu(element);\n }\n\n /**\n * Update a section badges using the state information.\n *\n * @param {object} section the section state.\n */\n _updateBadges(section) {\n const current = this.getElement(`${this.selectors.SECTIONBADGES} [data-type='iscurrent']`);\n current?.classList.toggle(this.classes.HIDE, !section.current);\n\n const hiddenFromStudents = this.getElement(`${this.selectors.SECTIONBADGES} [data-type='hiddenfromstudents']`);\n hiddenFromStudents?.classList.toggle(this.classes.HIDE, section.visible);\n }\n\n /**\n * Update a section action menus.\n *\n * @param {object} section the section state.\n */\n async _updateActionsMenu(section) {\n let selector;\n let newAction;\n if (section.visible) {\n selector = this.selectors.SHOWSECTION;\n newAction = 'sectionHide';\n } else {\n selector = this.selectors.HIDESECTION;\n newAction = 'sectionShow';\n }\n // Find the affected action.\n const affectedAction = this._getActionMenu(selector);\n if (!affectedAction) {\n return;\n }\n // Change action.\n affectedAction.dataset.action = newAction;\n // Change text.\n const actionText = affectedAction.querySelector(this.selectors.ACTIONTEXT);\n if (affectedAction.dataset?.swapname && actionText) {\n const oldText = actionText?.innerText;\n actionText.innerText = affectedAction.dataset.swapname;\n affectedAction.dataset.swapname = oldText;\n }\n // Change icon.\n const icon = affectedAction.querySelector(this.selectors.ICON);\n if (affectedAction.dataset?.swapicon && icon) {\n const newIcon = affectedAction.dataset.swapicon;\n if (newIcon) {\n const pixHtml = await Templates.renderPix(newIcon, 'core');\n Templates.replaceNode(icon, pixHtml, '');\n }\n }\n }\n\n /**\n * Get the action menu element from the selector.\n *\n * @param {string} selector The selector to find the action menu.\n * @returns The action menu element.\n */\n _getActionMenu(selector) {\n if (this.getElement('.section_action_menu')) {\n return this.getElement(selector);\n }\n\n return document.querySelector(selector);\n }\n}\n"],"names":["DndSection","create","name","selectors","SECTION_ITEM","CM","SECTIONINFO","SECTIONBADGES","SHOWSECTION","HIDESECTION","ACTIONTEXT","ICON","classes","LOCKED","HASDESCRIPTION","HIDE","HIDDEN","CURRENT","id","this","element","dataset","stateReady","state","configState","reactive","isEditing","supportComponents","sectionItem","getElement","headerComponent","Header","fullregion","configDragDrop","_openSectionIfNecessary","pageCmInfo","getPageAnchorCmInfo","sectionid","dispatch","pendingOpen","Pending","scrollIntoView","block","setTimeout","resolve","getWatchers","watch","handler","_refreshSection","validateDropData","dropdata","type","sectionReturn","super","getLastCm","cms","getElements","length","classList","toggle","DRAGGING","dragging","locked","visible","current","sectioninfo","hasrestrictions","_updateBadges","_updateActionsMenu","section","hiddenFromStudents","selector","newAction","affectedAction","_getActionMenu","action","actionText","querySelector","swapname","oldText","innerText","icon","swapicon","newIcon","pixHtml","Templates","renderPix","replaceNode","document"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"section.min.js","sources":["../../../src/local/content/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 section format component.\n *\n * @module core_courseformat/local/content/section\n * @class core_courseformat/local/content/section\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Header from 'core_courseformat/local/content/section/header';\nimport DndSection from 'core_courseformat/local/courseeditor/dndsection';\nimport Templates from 'core/templates';\nimport Pending from \"core/pending\";\n\nexport default class extends DndSection {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'content_section';\n // Default query selectors.\n this.selectors = {\n ACTIONMENU: '.section-actions',\n SECTION_ITEM: `[data-for='section_title']`,\n CM: `[data-for=\"cmitem\"]`,\n SECTIONINFO: `[data-for=\"sectioninfo\"]`,\n SECTIONBADGES: `[data-region=\"sectionbadges\"]`,\n SHOWSECTION: `[data-action=\"sectionShow\"]`,\n HIDESECTION: `[data-action=\"sectionHide\"]`,\n ACTIONTEXT: `.menu-action-text`,\n ICON: `.icon`,\n };\n // Most classes will be loaded later by DndCmItem.\n this.classes = {\n LOCKED: 'editinprogress',\n HASDESCRIPTION: 'description',\n HIDE: 'd-none',\n HIDDEN: 'hidden',\n CURRENT: 'current',\n };\n\n // We need our id to watch specific events.\n this.id = this.element.dataset.id;\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 // Drag and drop is only available for components compatible course formats.\n if (this.reactive.isEditing && this.reactive.supportComponents) {\n // Section zero and other formats sections may not have a title to drag.\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n if (sectionItem) {\n // Init the inner dragable element.\n const headerComponent = new Header({\n ...this,\n element: sectionItem,\n fullregion: this.element,\n });\n this.configDragDrop(headerComponent);\n }\n }\n this._openSectionIfNecessary();\n }\n\n /**\n * Open the section if the anchored activity is inside.\n */\n async _openSectionIfNecessary() {\n const pageCmInfo = this.reactive.getPageAnchorCmInfo();\n if (!pageCmInfo || pageCmInfo.sectionid !== this.id) {\n return;\n }\n await this.reactive.dispatch('sectionContentCollapsed', [this.id], false);\n const pendingOpen = new Pending(`courseformat/section:openSectionIfNecessary`);\n this.element.scrollIntoView({block: \"center\"});\n setTimeout(() => {\n this.reactive.dispatch('setPageItem', 'cm', pageCmInfo.id);\n pendingOpen.resolve();\n }, 250);\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `section[${this.id}]:updated`, handler: this._refreshSection},\n ];\n }\n\n /**\n * Validate if the drop data can be dropped over the component.\n *\n * @param {Object} dropdata the exported drop data.\n * @returns {boolean}\n */\n validateDropData(dropdata) {\n // If the format uses one section per page sections dropping in the content is ignored.\n if (dropdata?.type === 'section' && this.reactive.sectionReturn !== null) {\n return false;\n }\n return super.validateDropData(dropdata);\n }\n\n /**\n * Get the last CM element of that section.\n *\n * @returns {element|null}\n */\n getLastCm() {\n const cms = this.getElements(this.selectors.CM);\n // DndUpload may add extra elements so :last-child selector cannot be used.\n if (!cms || cms.length === 0) {\n return null;\n }\n const lastCm = cms[cms.length - 1];\n // If it is a delegated section return the last item overall.\n if (this.section.component !== null) {\n return lastCm;\n }\n // If it is a regular section and the last item overall has a parent cm, return the parent instead.\n const parentSection = lastCm.parentNode.closest(this.selectors.CM);\n return parentSection ?? lastCm;\n }\n\n /**\n * Get a fallback element when there is no CM in the section.\n *\n * @returns {element|null} the las course module element of the section.\n */\n getLastCmFallback() {\n // The sectioninfo is always present, even when the section is empty.\n return this.getElement(this.selectors.SECTIONINFO);\n }\n\n /**\n * Update a content section using the state information.\n *\n * @param {object} param\n * @param {Object} param.element details the update details.\n */\n _refreshSection({element}) {\n // Update classes.\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.HIDDEN, !element.visible ?? false);\n this.element.classList.toggle(this.classes.CURRENT, element.current ?? false);\n this.locked = element.locked;\n // The description box classes depends on the section state.\n const sectioninfo = this.getElement(this.selectors.SECTIONINFO);\n if (sectioninfo) {\n sectioninfo.classList.toggle(this.classes.HASDESCRIPTION, element.hasrestrictions);\n }\n // Update section badges and menus.\n this._updateBadges(element);\n this._updateActionsMenu(element);\n }\n\n /**\n * Update a section badges using the state information.\n *\n * @param {object} section the section state.\n */\n _updateBadges(section) {\n const current = this.getElement(`${this.selectors.SECTIONBADGES} [data-type='iscurrent']`);\n current?.classList.toggle(this.classes.HIDE, !section.current);\n\n const hiddenFromStudents = this.getElement(`${this.selectors.SECTIONBADGES} [data-type='hiddenfromstudents']`);\n hiddenFromStudents?.classList.toggle(this.classes.HIDE, section.visible);\n }\n\n /**\n * Update a section action menus.\n *\n * @param {object} section the section state.\n */\n async _updateActionsMenu(section) {\n let selector;\n let newAction;\n if (section.visible) {\n selector = this.selectors.SHOWSECTION;\n newAction = 'sectionHide';\n } else {\n selector = this.selectors.HIDESECTION;\n newAction = 'sectionShow';\n }\n // Find the affected action.\n const affectedAction = this._getActionMenu(selector);\n if (!affectedAction) {\n return;\n }\n // Change action.\n affectedAction.dataset.action = newAction;\n // Change text.\n const actionText = affectedAction.querySelector(this.selectors.ACTIONTEXT);\n if (affectedAction.dataset?.swapname && actionText) {\n const oldText = actionText?.innerText;\n actionText.innerText = affectedAction.dataset.swapname;\n affectedAction.dataset.swapname = oldText;\n }\n // Change icon.\n const icon = affectedAction.querySelector(this.selectors.ICON);\n if (affectedAction.dataset?.swapicon && icon) {\n const newIcon = affectedAction.dataset.swapicon;\n affectedAction.dataset.swapicon = affectedAction.dataset.icon;\n affectedAction.dataset.icon = newIcon;\n if (newIcon) {\n const pixHtml = await Templates.renderPix(newIcon, 'core');\n Templates.replaceNode(icon, pixHtml, '');\n }\n }\n }\n\n /**\n * Get the action menu element from the selector.\n *\n * @param {string} selector The selector to find the action menu.\n * @returns The action menu element.\n */\n _getActionMenu(selector) {\n return document.querySelector(`${this.selectors.ACTIONMENU}[data-sectionid='${this.id}'] ${selector}`);\n }\n}\n"],"names":["DndSection","create","name","selectors","ACTIONMENU","SECTION_ITEM","CM","SECTIONINFO","SECTIONBADGES","SHOWSECTION","HIDESECTION","ACTIONTEXT","ICON","classes","LOCKED","HASDESCRIPTION","HIDE","HIDDEN","CURRENT","id","this","element","dataset","stateReady","state","configState","reactive","isEditing","supportComponents","sectionItem","getElement","headerComponent","Header","fullregion","configDragDrop","_openSectionIfNecessary","pageCmInfo","getPageAnchorCmInfo","sectionid","dispatch","pendingOpen","Pending","scrollIntoView","block","setTimeout","resolve","getWatchers","watch","handler","_refreshSection","validateDropData","dropdata","type","sectionReturn","super","getLastCm","cms","getElements","length","lastCm","section","component","parentSection","parentNode","closest","getLastCmFallback","classList","toggle","DRAGGING","dragging","locked","visible","current","sectioninfo","hasrestrictions","_updateBadges","_updateActionsMenu","hiddenFromStudents","selector","newAction","affectedAction","_getActionMenu","action","actionText","querySelector","swapname","oldText","innerText","icon","swapicon","newIcon","pixHtml","Templates","renderPix","replaceNode","document"],"mappings":"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"} \ No newline at end of file diff --git a/course/format/amd/build/local/content/section/cmitem.min.js b/course/format/amd/build/local/content/section/cmitem.min.js index 781e257eb1420..ee132e4e62adb 100644 --- a/course/format/amd/build/local/content/section/cmitem.min.js +++ b/course/format/amd/build/local/content/section/cmitem.min.js @@ -8,6 +8,6 @@ define("core_courseformat/local/content/section/cmitem",["exports","core_coursef * @class core_courseformat/local/content/section/cmitem * @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=(obj=_dndcmitem)&&obj.__esModule?obj:{default:obj};class _default extends _dndcmitem.default{create(){this.name="content_section_cmitem",this.selectors={BULKSELECT:"[data-for='cmBulkSelect']",BULKCHECKBOX:"[data-bulkcheckbox]",CARD:"[data-region='activity-card']",DRAGICON:".editing_move",INPLACEEDITABLE:"[data-inplaceeditablelink]"},this.classes={LOCKED:"editinprogress",HIDE:"d-none",SELECTED:"selected"},this.id=this.element.dataset.id}stateReady(state){var _this$getElement;this.configDragDrop(this.id),null===(_this$getElement=this.getElement(this.selectors.DRAGICON))||void 0===_this$getElement||_this$getElement.classList.add(this.classes.DRAGICON),this._refreshBulk({state:state})}getWatchers(){return[{watch:"cm[".concat(this.id,"]:deleted"),handler:this.unregister},{watch:"cm[".concat(this.id,"]:updated"),handler:this._refreshCm},{watch:"bulk:updated",handler:this._refreshBulk}]}setDragImage(){return this.getElement(this.selectors.CARD)}_refreshCm(_ref){var _element$dragging,_element$locked;let{element:element}=_ref;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}_refreshBulk(_ref2){var _this$getElement2;let{state:state}=_ref2;const bulk=state.bulk;this.setDraggable(!bulk.enabled),bulk.enabled?(this.element.dataset.action="toggleSelectionCm",this.element.dataset.preventDefault=1):(this.element.removeAttribute("data-action"),this.element.removeAttribute("data-preventDefault")),null===(_this$getElement2=this.getElement(this.selectors.BULKSELECT))||void 0===_this$getElement2||_this$getElement2.classList.toggle(this.classes.HIDE,!bulk.enabled);const disabled=!this._isCmBulkEnabled(bulk),selected=this._isSelected(bulk);this._refreshActivityCard(bulk,selected),this._setCheckboxValue(selected,disabled)}_refreshActivityCard(bulk,selected){var _this$getElement3,_this$getElement4;null===(_this$getElement3=this.getElement(this.selectors.INPLACEEDITABLE))||void 0===_this$getElement3||_this$getElement3.classList.toggle(this.classes.HIDE,bulk.enabled),null===(_this$getElement4=this.getElement(this.selectors.CARD))||void 0===_this$getElement4||_this$getElement4.classList.toggle(this.classes.SELECTED,selected),this.element.classList.toggle(this.classes.SELECTED,selected)}_setCheckboxValue(checked,disabled){const checkbox=this.getElement(this.selectors.BULKCHECKBOX);checkbox&&(checkbox.checked=checked,checkbox.disabled=disabled,disabled?checkbox.removeAttribute("data-is-selectable"):checkbox.dataset.isSelectable=1)}_isCmBulkEnabled(bulk){return!!bulk.enabled&&(""===bulk.selectedType||"cm"===bulk.selectedType)}_isSelected(bulk){return"cm"===bulk.selectedType&&bulk.selection.includes(this.id)}}return _exports.default=_default,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_dndcmitem=(obj=_dndcmitem)&&obj.__esModule?obj:{default:obj};class _default extends _dndcmitem.default{create(){this.name="content_section_cmitem",this.selectors={BULKSELECT:"[data-for='cmBulkSelect']",BULKCHECKBOX:"[data-bulkcheckbox]",CARD:"[data-region='activity-card']",DRAGICON:".editing_move",INPLACEEDITABLE:'[data-itemtype="activityname"] > [data-inplaceeditablelink]'},this.classes={LOCKED:"editinprogress",HIDE:"d-none",SELECTED:"selected"},this.id=this.element.dataset.id}stateReady(state){var _this$getElement;this.configDragDrop(this.id),null===(_this$getElement=this.getElement(this.selectors.DRAGICON))||void 0===_this$getElement||_this$getElement.classList.add(this.classes.DRAGICON),this._refreshBulk({state:state})}getWatchers(){return[{watch:"cm[".concat(this.id,"]:deleted"),handler:this.unregister},{watch:"cm[".concat(this.id,"]:updated"),handler:this._refreshCm},{watch:"bulk:updated",handler:this._refreshBulk}]}setDragImage(){return this.getElement(this.selectors.CARD)}_refreshCm(_ref){var _element$dragging,_element$locked;let{element:element}=_ref;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}_refreshBulk(_ref2){var _this$getElement2;let{state:state}=_ref2;const bulk=state.bulk;this.setDraggable(!bulk.enabled),bulk.enabled?(this.element.dataset.action="toggleSelectionCm",this.element.dataset.preventDefault=1):(this.element.removeAttribute("data-action"),this.element.removeAttribute("data-preventDefault")),null===(_this$getElement2=this.getElement(this.selectors.BULKSELECT))||void 0===_this$getElement2||_this$getElement2.classList.toggle(this.classes.HIDE,!bulk.enabled);const disabled=!this._isCmBulkEnabled(bulk),selected=this._isSelected(bulk);this._refreshActivityCard(bulk,selected),this._setCheckboxValue(selected,disabled)}_refreshActivityCard(bulk,selected){var _this$getElement3,_this$getElement4;null===(_this$getElement3=this.getElement(this.selectors.INPLACEEDITABLE))||void 0===_this$getElement3||_this$getElement3.classList.toggle(this.classes.HIDE,bulk.enabled),null===(_this$getElement4=this.getElement(this.selectors.CARD))||void 0===_this$getElement4||_this$getElement4.classList.toggle(this.classes.SELECTED,selected),this.element.classList.toggle(this.classes.SELECTED,selected)}_setCheckboxValue(checked,disabled){const checkbox=this.getElement(this.selectors.BULKCHECKBOX);checkbox&&(checkbox.checked=checked,checkbox.disabled=disabled,disabled?checkbox.removeAttribute("data-is-selectable"):checkbox.dataset.isSelectable=1)}_isCmBulkEnabled(bulk){return!!bulk.enabled&&(""===bulk.selectedType||"cm"===bulk.selectedType)}_isSelected(bulk){return"cm"===bulk.selectedType&&bulk.selection.includes(this.id)}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=cmitem.min.js.map \ No newline at end of file diff --git a/course/format/amd/build/local/content/section/cmitem.min.js.map b/course/format/amd/build/local/content/section/cmitem.min.js.map index e699782d61cf4..557d83af8e602 100644 --- a/course/format/amd/build/local/content/section/cmitem.min.js.map +++ b/course/format/amd/build/local/content/section/cmitem.min.js.map @@ -1 +1 @@ -{"version":3,"file":"cmitem.min.js","sources":["../../../../src/local/content/section/cmitem.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 course module item component.\n *\n * This component is used to control specific course modules interactions like drag and drop.\n *\n * @module core_courseformat/local/content/section/cmitem\n * @class core_courseformat/local/content/section/cmitem\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';\n\nexport default class extends DndCmItem {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'content_section_cmitem';\n // Default query selectors.\n this.selectors = {\n BULKSELECT: `[data-for='cmBulkSelect']`,\n BULKCHECKBOX: `[data-bulkcheckbox]`,\n CARD: `[data-region='activity-card']`,\n DRAGICON: `.editing_move`,\n INPLACEEDITABLE: `[data-inplaceeditablelink]`,\n };\n // Most classes will be loaded later by DndCmItem.\n this.classes = {\n LOCKED: 'editinprogress',\n HIDE: 'd-none',\n SELECTED: 'selected',\n };\n // We need our id to watch specific events.\n this.id = this.element.dataset.id;\n }\n\n /**\n * Initial state ready method.\n * @param {Object} state the state data\n */\n stateReady(state) {\n this.configDragDrop(this.id);\n this.getElement(this.selectors.DRAGICON)?.classList.add(this.classes.DRAGICON);\n this._refreshBulk({state});\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.unregister},\n {watch: `cm[${this.id}]:updated`, handler: this._refreshCm},\n {watch: `bulk:updated`, handler: this._refreshBulk},\n ];\n }\n\n /**\n * Return the custom activity card drag shadow image.\n *\n * The element returned will be used when the user drags the card.\n *\n * @returns {HTMLElement}\n */\n setDragImage() {\n return this.getElement(this.selectors.CARD);\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.DRAGGING, element.dragging ?? false);\n this.element.classList.toggle(this.classes.LOCKED, element.locked ?? false);\n this.locked = element.locked;\n }\n\n /**\n * Update the bulk editing interface.\n *\n * @param {object} param\n * @param {Object} param.state the state data\n */\n _refreshBulk({state}) {\n const bulk = state.bulk;\n // For now, dragging elements in bulk is not possible.\n this.setDraggable(!bulk.enabled);\n // Convert the card into an active element in bulk mode.\n if (bulk.enabled) {\n this.element.dataset.action = 'toggleSelectionCm';\n this.element.dataset.preventDefault = 1;\n } else {\n this.element.removeAttribute('data-action');\n this.element.removeAttribute('data-preventDefault');\n }\n\n this.getElement(this.selectors.BULKSELECT)?.classList.toggle(this.classes.HIDE, !bulk.enabled);\n\n const disabled = !this._isCmBulkEnabled(bulk);\n const selected = this._isSelected(bulk);\n this._refreshActivityCard(bulk, selected);\n this._setCheckboxValue(selected, disabled);\n }\n\n /**\n * Update the activity card depending on the bulk selection.\n *\n * @param {Object} bulk the current bulk state data\n * @param {Boolean} selected if the activity is selected.\n */\n _refreshActivityCard(bulk, selected) {\n this.getElement(this.selectors.INPLACEEDITABLE)?.classList.toggle(this.classes.HIDE, bulk.enabled);\n this.getElement(this.selectors.CARD)?.classList.toggle(this.classes.SELECTED, selected);\n this.element.classList.toggle(this.classes.SELECTED, selected);\n }\n\n /**\n * Modify the checkbox element.\n * @param {Boolean} checked the new checked value\n * @param {Boolean} disabled the new disabled value\n */\n _setCheckboxValue(checked, disabled) {\n const checkbox = this.getElement(this.selectors.BULKCHECKBOX);\n if (!checkbox) {\n return;\n }\n checkbox.checked = checked;\n checkbox.disabled = disabled;\n // Is selectable is used to easily scan the page for bulk checkboxes.\n if (disabled) {\n checkbox.removeAttribute('data-is-selectable');\n } else {\n checkbox.dataset.isSelectable = 1;\n }\n }\n\n /**\n * Check if cm bulk selection is available.\n * @param {Object} bulk the current state bulk attribute\n * @returns {Boolean}\n */\n _isCmBulkEnabled(bulk) {\n if (!bulk.enabled) {\n return false;\n }\n return (bulk.selectedType === '' || bulk.selectedType === 'cm');\n }\n\n /**\n * Check if the cm id is part of the current bulk selection.\n * @param {Object} bulk the current state bulk attribute\n * @returns {Boolean}\n */\n _isSelected(bulk) {\n if (bulk.selectedType !== 'cm') {\n return false;\n }\n return bulk.selection.includes(this.id);\n }\n}\n"],"names":["DndCmItem","create","name","selectors","BULKSELECT","BULKCHECKBOX","CARD","DRAGICON","INPLACEEDITABLE","classes","LOCKED","HIDE","SELECTED","id","this","element","dataset","stateReady","state","configDragDrop","getElement","classList","add","_refreshBulk","getWatchers","watch","handler","unregister","_refreshCm","setDragImage","toggle","DRAGGING","dragging","locked","bulk","setDraggable","enabled","action","preventDefault","removeAttribute","disabled","_isCmBulkEnabled","selected","_isSelected","_refreshActivityCard","_setCheckboxValue","checked","checkbox","isSelectable","selectedType","selection","includes"],"mappings":";;;;;;;;;;0KA4B6BA,mBAKzBC,cAESC,KAAO,8BAEPC,UAAY,CACbC,uCACAC,mCACAC,qCACAC,yBACAC,mDAGCC,QAAU,CACXC,OAAQ,iBACRC,KAAM,SACNC,SAAU,iBAGTC,GAAKC,KAAKC,QAAQC,QAAQH,GAOnCI,WAAWC,iCACFC,eAAeL,KAAKD,kCACpBO,WAAWN,KAAKX,UAAUI,wDAAWc,UAAUC,IAAIR,KAAKL,QAAQF,eAChEgB,aAAa,CAACL,MAAAA,QAQvBM,oBACW,CACH,CAACC,mBAAaX,KAAKD,gBAAea,QAASZ,KAAKa,YAChD,CAACF,mBAAaX,KAAKD,gBAAea,QAASZ,KAAKc,YAChD,CAACH,qBAAuBC,QAASZ,KAAKS,eAW9CM,sBACWf,KAAKM,WAAWN,KAAKX,UAAUG,MAS1CsB,2DAAWb,QAACA,mBAEHA,QAAQM,UAAUS,OAAOhB,KAAKL,QAAQsB,mCAAUhB,QAAQiB,+DACxDjB,QAAQM,UAAUS,OAAOhB,KAAKL,QAAQC,+BAAQK,QAAQkB,yDACtDA,OAASlB,QAAQkB,OAS1BV,8CAAaL,MAACA,mBACJgB,KAAOhB,MAAMgB,UAEdC,cAAcD,KAAKE,SAEpBF,KAAKE,cACArB,QAAQC,QAAQqB,OAAS,yBACzBtB,QAAQC,QAAQsB,eAAiB,SAEjCvB,QAAQwB,gBAAgB,oBACxBxB,QAAQwB,gBAAgB,uDAG5BnB,WAAWN,KAAKX,UAAUC,4DAAaiB,UAAUS,OAAOhB,KAAKL,QAAQE,MAAOuB,KAAKE,eAEhFI,UAAY1B,KAAK2B,iBAAiBP,MAClCQ,SAAW5B,KAAK6B,YAAYT,WAC7BU,qBAAqBV,KAAMQ,eAC3BG,kBAAkBH,SAAUF,UASrCI,qBAAqBV,KAAMQ,iFAClBtB,WAAWN,KAAKX,UAAUK,iEAAkBa,UAAUS,OAAOhB,KAAKL,QAAQE,KAAMuB,KAAKE,wCACrFhB,WAAWN,KAAKX,UAAUG,sDAAOe,UAAUS,OAAOhB,KAAKL,QAAQG,SAAU8B,eACzE3B,QAAQM,UAAUS,OAAOhB,KAAKL,QAAQG,SAAU8B,UAQzDG,kBAAkBC,QAASN,gBACjBO,SAAWjC,KAAKM,WAAWN,KAAKX,UAAUE,cAC3C0C,WAGLA,SAASD,QAAUA,QACnBC,SAASP,SAAWA,SAEhBA,SACAO,SAASR,gBAAgB,sBAEzBQ,SAAS/B,QAAQgC,aAAe,GASxCP,iBAAiBP,cACRA,KAAKE,UAGoB,KAAtBF,KAAKe,cAA6C,OAAtBf,KAAKe,cAQ7CN,YAAYT,YACkB,OAAtBA,KAAKe,cAGFf,KAAKgB,UAAUC,SAASrC,KAAKD"} \ No newline at end of file +{"version":3,"file":"cmitem.min.js","sources":["../../../../src/local/content/section/cmitem.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 course module item component.\n *\n * This component is used to control specific course modules interactions like drag and drop.\n *\n * @module core_courseformat/local/content/section/cmitem\n * @class core_courseformat/local/content/section/cmitem\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';\n\nexport default class extends DndCmItem {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'content_section_cmitem';\n // Default query selectors.\n this.selectors = {\n BULKSELECT: `[data-for='cmBulkSelect']`,\n BULKCHECKBOX: `[data-bulkcheckbox]`,\n CARD: `[data-region='activity-card']`,\n DRAGICON: `.editing_move`,\n INPLACEEDITABLE: `[data-itemtype=\"activityname\"] > [data-inplaceeditablelink]`,\n };\n // Most classes will be loaded later by DndCmItem.\n this.classes = {\n LOCKED: 'editinprogress',\n HIDE: 'd-none',\n SELECTED: 'selected',\n };\n // We need our id to watch specific events.\n this.id = this.element.dataset.id;\n }\n\n /**\n * Initial state ready method.\n * @param {Object} state the state data\n */\n stateReady(state) {\n this.configDragDrop(this.id);\n this.getElement(this.selectors.DRAGICON)?.classList.add(this.classes.DRAGICON);\n this._refreshBulk({state});\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.unregister},\n {watch: `cm[${this.id}]:updated`, handler: this._refreshCm},\n {watch: `bulk:updated`, handler: this._refreshBulk},\n ];\n }\n\n /**\n * Return the custom activity card drag shadow image.\n *\n * The element returned will be used when the user drags the card.\n *\n * @returns {HTMLElement}\n */\n setDragImage() {\n return this.getElement(this.selectors.CARD);\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.DRAGGING, element.dragging ?? false);\n this.element.classList.toggle(this.classes.LOCKED, element.locked ?? false);\n this.locked = element.locked;\n }\n\n /**\n * Update the bulk editing interface.\n *\n * @param {object} param\n * @param {Object} param.state the state data\n */\n _refreshBulk({state}) {\n const bulk = state.bulk;\n // For now, dragging elements in bulk is not possible.\n this.setDraggable(!bulk.enabled);\n // Convert the card into an active element in bulk mode.\n if (bulk.enabled) {\n this.element.dataset.action = 'toggleSelectionCm';\n this.element.dataset.preventDefault = 1;\n } else {\n this.element.removeAttribute('data-action');\n this.element.removeAttribute('data-preventDefault');\n }\n\n this.getElement(this.selectors.BULKSELECT)?.classList.toggle(this.classes.HIDE, !bulk.enabled);\n\n const disabled = !this._isCmBulkEnabled(bulk);\n const selected = this._isSelected(bulk);\n this._refreshActivityCard(bulk, selected);\n this._setCheckboxValue(selected, disabled);\n }\n\n /**\n * Update the activity card depending on the bulk selection.\n *\n * @param {Object} bulk the current bulk state data\n * @param {Boolean} selected if the activity is selected.\n */\n _refreshActivityCard(bulk, selected) {\n this.getElement(this.selectors.INPLACEEDITABLE)?.classList.toggle(this.classes.HIDE, bulk.enabled);\n this.getElement(this.selectors.CARD)?.classList.toggle(this.classes.SELECTED, selected);\n this.element.classList.toggle(this.classes.SELECTED, selected);\n }\n\n /**\n * Modify the checkbox element.\n * @param {Boolean} checked the new checked value\n * @param {Boolean} disabled the new disabled value\n */\n _setCheckboxValue(checked, disabled) {\n const checkbox = this.getElement(this.selectors.BULKCHECKBOX);\n if (!checkbox) {\n return;\n }\n checkbox.checked = checked;\n checkbox.disabled = disabled;\n // Is selectable is used to easily scan the page for bulk checkboxes.\n if (disabled) {\n checkbox.removeAttribute('data-is-selectable');\n } else {\n checkbox.dataset.isSelectable = 1;\n }\n }\n\n /**\n * Check if cm bulk selection is available.\n * @param {Object} bulk the current state bulk attribute\n * @returns {Boolean}\n */\n _isCmBulkEnabled(bulk) {\n if (!bulk.enabled) {\n return false;\n }\n return (bulk.selectedType === '' || bulk.selectedType === 'cm');\n }\n\n /**\n * Check if the cm id is part of the current bulk selection.\n * @param {Object} bulk the current state bulk attribute\n * @returns {Boolean}\n */\n _isSelected(bulk) {\n if (bulk.selectedType !== 'cm') {\n return false;\n }\n return bulk.selection.includes(this.id);\n }\n}\n"],"names":["DndCmItem","create","name","selectors","BULKSELECT","BULKCHECKBOX","CARD","DRAGICON","INPLACEEDITABLE","classes","LOCKED","HIDE","SELECTED","id","this","element","dataset","stateReady","state","configDragDrop","getElement","classList","add","_refreshBulk","getWatchers","watch","handler","unregister","_refreshCm","setDragImage","toggle","DRAGGING","dragging","locked","bulk","setDraggable","enabled","action","preventDefault","removeAttribute","disabled","_isCmBulkEnabled","selected","_isSelected","_refreshActivityCard","_setCheckboxValue","checked","checkbox","isSelectable","selectedType","selection","includes"],"mappings":";;;;;;;;;;0KA4B6BA,mBAKzBC,cAESC,KAAO,8BAEPC,UAAY,CACbC,uCACAC,mCACAC,qCACAC,yBACAC,oFAGCC,QAAU,CACXC,OAAQ,iBACRC,KAAM,SACNC,SAAU,iBAGTC,GAAKC,KAAKC,QAAQC,QAAQH,GAOnCI,WAAWC,iCACFC,eAAeL,KAAKD,kCACpBO,WAAWN,KAAKX,UAAUI,wDAAWc,UAAUC,IAAIR,KAAKL,QAAQF,eAChEgB,aAAa,CAACL,MAAAA,QAQvBM,oBACW,CACH,CAACC,mBAAaX,KAAKD,gBAAea,QAASZ,KAAKa,YAChD,CAACF,mBAAaX,KAAKD,gBAAea,QAASZ,KAAKc,YAChD,CAACH,qBAAuBC,QAASZ,KAAKS,eAW9CM,sBACWf,KAAKM,WAAWN,KAAKX,UAAUG,MAS1CsB,2DAAWb,QAACA,mBAEHA,QAAQM,UAAUS,OAAOhB,KAAKL,QAAQsB,mCAAUhB,QAAQiB,+DACxDjB,QAAQM,UAAUS,OAAOhB,KAAKL,QAAQC,+BAAQK,QAAQkB,yDACtDA,OAASlB,QAAQkB,OAS1BV,8CAAaL,MAACA,mBACJgB,KAAOhB,MAAMgB,UAEdC,cAAcD,KAAKE,SAEpBF,KAAKE,cACArB,QAAQC,QAAQqB,OAAS,yBACzBtB,QAAQC,QAAQsB,eAAiB,SAEjCvB,QAAQwB,gBAAgB,oBACxBxB,QAAQwB,gBAAgB,uDAG5BnB,WAAWN,KAAKX,UAAUC,4DAAaiB,UAAUS,OAAOhB,KAAKL,QAAQE,MAAOuB,KAAKE,eAEhFI,UAAY1B,KAAK2B,iBAAiBP,MAClCQ,SAAW5B,KAAK6B,YAAYT,WAC7BU,qBAAqBV,KAAMQ,eAC3BG,kBAAkBH,SAAUF,UASrCI,qBAAqBV,KAAMQ,iFAClBtB,WAAWN,KAAKX,UAAUK,iEAAkBa,UAAUS,OAAOhB,KAAKL,QAAQE,KAAMuB,KAAKE,wCACrFhB,WAAWN,KAAKX,UAAUG,sDAAOe,UAAUS,OAAOhB,KAAKL,QAAQG,SAAU8B,eACzE3B,QAAQM,UAAUS,OAAOhB,KAAKL,QAAQG,SAAU8B,UAQzDG,kBAAkBC,QAASN,gBACjBO,SAAWjC,KAAKM,WAAWN,KAAKX,UAAUE,cAC3C0C,WAGLA,SAASD,QAAUA,QACnBC,SAASP,SAAWA,SAEhBA,SACAO,SAASR,gBAAgB,sBAEzBQ,SAAS/B,QAAQgC,aAAe,GASxCP,iBAAiBP,cACRA,KAAKE,UAGoB,KAAtBF,KAAKe,cAA6C,OAAtBf,KAAKe,cAQ7CN,YAAYT,YACkB,OAAtBA,KAAKe,cAGFf,KAAKgB,UAAUC,SAASrC,KAAKD"} \ No newline at end of file diff --git a/course/format/amd/build/local/courseeditor/dndsection.min.js b/course/format/amd/build/local/courseeditor/dndsection.min.js index 928f50c95d4b3..81316b9dc0fb8 100644 --- a/course/format/amd/build/local/courseeditor/dndsection.min.js +++ b/course/format/amd/build/local/courseeditor/dndsection.min.js @@ -9,6 +9,6 @@ define("core_courseformat/local/courseeditor/dndsection",["exports","core/reacti * @class core_courseformat/local/courseeditor/dndsection * @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,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj},(0,_prefetch.prefetchStrings)("core",["addfilehere"]);class _default extends _reactive.BaseComponent{configState(state){this.id=this.element.dataset.id,this.section=state.section.get(this.id),this.course=state.course}configDragDrop(sectionitem){this.reactive.isEditing&&this.reactive.supportComponents&&(this.sectionitem=sectionitem,this.dragdrop=new _reactive.DragDrop(this),this.classes=this.dragdrop.getClasses())}destroy(){void 0!==this.sectionitem&&this.sectionitem.unregister(),void 0!==this.dragdrop&&this.dragdrop.unregister()}getLastCm(){return null}dragStart(dropdata){this.reactive.dispatch("sectionDrag",[dropdata.id],!0)}dragEnd(dropdata){this.reactive.dispatch("sectionDrag",[dropdata.id],!1)}validateDropData(dropdata){return"files"===(null==dropdata?void 0:dropdata.type)||("cm"===(null==dropdata?void 0:dropdata.type)?null===(_this$section=this.section)||void 0===_this$section||!_this$section.component||!0!==(null==dropdata?void 0:dropdata.hasdelegatedsection):"section"===(null==dropdata?void 0:dropdata.type)&&(null===this.section.component&&((null==dropdata?void 0:dropdata.id)!=this.id&&(null==dropdata?void 0:dropdata.number)!=this.section.number+1)));var _this$section}showDropZone(dropdata){var _this$getLastCm;("files"==dropdata.type&&this.addOverlay({content:(0,_str.getString)("addfilehere","core"),icon:_templates.default.renderPix("t/download","core")}).then((()=>{var _this$dragdrop;null!==(_this$dragdrop=this.dragdrop)&&void 0!==_this$dragdrop&&_this$dragdrop.isDropzoneVisible()||this.removeOverlay()})).catch((error=>{throw error})),"cm"==dropdata.type)&&(null===(_this$getLastCm=this.getLastCm())||void 0===_this$getLastCm||_this$getLastCm.classList.add(this.classes.DROPDOWN));"section"==dropdata.type&&(this.element.classList.remove(this.classes.DROPUP),this.element.classList.add(this.classes.DROPDOWN))}hideDropZone(){var _this$getLastCm2;null===(_this$getLastCm2=this.getLastCm())||void 0===_this$getLastCm2||_this$getLastCm2.classList.remove(this.classes.DROPDOWN),this.element.classList.remove(this.classes.DROPUP),this.element.classList.remove(this.classes.DROPDOWN),this.removeOverlay()}drop(dropdata,event){if("files"!=dropdata.type){if("cm"==dropdata.type){const mutation=event.altKey?"cmDuplicate":"cmMove";this.reactive.dispatch(mutation,[dropdata.id],this.id)}"section"==dropdata.type&&this.reactive.dispatch("sectionMoveAfter",[dropdata.id],this.id)}else this.reactive.uploadFiles(this.section.id,this.section.number,dropdata.files)}}return _exports.default=_default,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj},(0,_prefetch.prefetchStrings)("core",["addfilehere"]);class _default extends _reactive.BaseComponent{configState(state){this.id=this.element.dataset.id,this.section=state.section.get(this.id),this.course=state.course}configDragDrop(sectionitem){this.reactive.isEditing&&this.reactive.supportComponents&&(this.sectionitem=sectionitem,this.dragdrop=new _reactive.DragDrop(this),this.classes=this.dragdrop.getClasses())}destroy(){void 0!==this.sectionitem&&this.sectionitem.unregister(),void 0!==this.dragdrop&&this.dragdrop.unregister()}getLastCm(){return null}getLastCmFallback(){return null}dragStart(dropdata){this.reactive.dispatch("sectionDrag",[dropdata.id],!0)}dragEnd(dropdata){this.reactive.dispatch("sectionDrag",[dropdata.id],!1)}validateDropData(dropdata){return"files"===(null==dropdata?void 0:dropdata.type)||("cm"===(null==dropdata?void 0:dropdata.type)?null===(_this$section=this.section)||void 0===_this$section||!_this$section.component||!0!==(null==dropdata?void 0:dropdata.hasdelegatedsection):"section"===(null==dropdata?void 0:dropdata.type)&&(null===this.section.component&&((null==dropdata?void 0:dropdata.id)!=this.id&&(null==dropdata?void 0:dropdata.number)!=this.section.number+1)));var _this$section}showDropZone(dropdata){if("files"==dropdata.type&&this.addOverlay({content:(0,_str.getString)("addfilehere","core"),icon:_templates.default.renderPix("t/download","core")}).then((()=>{var _this$dragdrop;null!==(_this$dragdrop=this.dragdrop)&&void 0!==_this$dragdrop&&_this$dragdrop.isDropzoneVisible()||this.removeOverlay()})).catch((error=>{throw error})),"cm"==dropdata.type){const lastCm=this.getLastCm();var _this$getLastCmFallba;if(null==lastCm||lastCm.classList.add(this.classes.DROPDOWN),!lastCm)null===(_this$getLastCmFallba=this.getLastCmFallback())||void 0===_this$getLastCmFallba||_this$getLastCmFallba.classList.add(this.classes.DROPDOWN)}"section"==dropdata.type&&(this.element.classList.remove(this.classes.DROPUP),this.element.classList.add(this.classes.DROPDOWN))}hideDropZone(){var _this$getLastCm,_this$getLastCmFallba2;null===(_this$getLastCm=this.getLastCm())||void 0===_this$getLastCm||_this$getLastCm.classList.remove(this.classes.DROPDOWN),null===(_this$getLastCmFallba2=this.getLastCmFallback())||void 0===_this$getLastCmFallba2||_this$getLastCmFallba2.classList.remove(this.classes.DROPDOWN),this.element.classList.remove(this.classes.DROPUP),this.element.classList.remove(this.classes.DROPDOWN),this.removeOverlay()}drop(dropdata,event){if("files"!=dropdata.type){if("cm"==dropdata.type){const mutation=event.altKey?"cmDuplicate":"cmMove";this.reactive.dispatch(mutation,[dropdata.id],this.id)}"section"==dropdata.type&&this.reactive.dispatch("sectionMoveAfter",[dropdata.id],this.id)}else this.reactive.uploadFiles(this.section.id,this.section.number,dropdata.files)}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=dndsection.min.js.map \ No newline at end of file diff --git a/course/format/amd/build/local/courseeditor/dndsection.min.js.map b/course/format/amd/build/local/courseeditor/dndsection.min.js.map index 7bbd1de85ee47..e51461826517a 100644 --- a/course/format/amd/build/local/courseeditor/dndsection.min.js.map +++ b/course/format/amd/build/local/courseeditor/dndsection.min.js.map @@ -1 +1 @@ -{"version":3,"file":"dndsection.min.js","sources":["../../../src/local/courseeditor/dndsection.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 * in both course index and course content.\n *\n * @module core_courseformat/local/courseeditor/dndsection\n * @class core_courseformat/local/courseeditor/dndsection\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent, DragDrop} from 'core/reactive';\nimport {getString} from 'core/str';\nimport {prefetchStrings} from 'core/prefetch';\nimport Templates from 'core/templates';\n\n// Load global strings.\nprefetchStrings('core', ['addfilehere']);\n\nexport default class extends BaseComponent {\n\n /**\n * Save some values form the state.\n *\n * @param {Object} state the current state\n */\n configState(state) {\n this.id = this.element.dataset.id;\n this.section = state.section.get(this.id);\n this.course = state.course;\n }\n\n /**\n * Register state values and the drag and drop subcomponent.\n *\n * @param {BaseComponent} sectionitem section item component\n */\n configDragDrop(sectionitem) {\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.\n this.sectionitem = sectionitem;\n // Init the dropzone.\n this.dragdrop = new DragDrop(this);\n // Save dropzone classes.\n this.classes = this.dragdrop.getClasses();\n }\n }\n\n /**\n * Remove all subcomponents dependencies.\n */\n destroy() {\n if (this.sectionitem !== undefined) {\n this.sectionitem.unregister();\n }\n if (this.dragdrop !== undefined) {\n this.dragdrop.unregister();\n }\n }\n\n /**\n * Get the last CM element of that section.\n *\n * @returns {element|null} the las course module element of the section.\n */\n getLastCm() {\n return null;\n }\n\n // Drag and drop methods.\n\n /**\n * The element drop start hook.\n *\n * @param {Object} dropdata the dropdata\n */\n dragStart(dropdata) {\n this.reactive.dispatch('sectionDrag', [dropdata.id], true);\n }\n\n /**\n * The element drop end hook.\n *\n * @param {Object} dropdata the dropdata\n */\n dragEnd(dropdata) {\n this.reactive.dispatch('sectionDrag', [dropdata.id], false);\n }\n\n /**\n * Validate if the drop data can be dropped over the component.\n *\n * @param {Object} dropdata the exported drop data.\n * @returns {boolean}\n */\n validateDropData(dropdata) {\n // We accept files.\n if (dropdata?.type === 'files') {\n return true;\n }\n // We accept any course module unless it can form a subsection loop.\n if (dropdata?.type === 'cm') {\n if (this.section?.component && dropdata?.hasdelegatedsection === true) {\n return false;\n }\n return true;\n }\n if (dropdata?.type === 'section') {\n // Sections controlled by a plugin cannot accept sections.\n if (this.section.component !== null) {\n return false;\n }\n // We accept any section but yourself and the next one.\n return dropdata?.id != this.id && dropdata?.number != this.section.number + 1;\n }\n return false;\n }\n\n /**\n * Display the component dropzone.\n *\n * @param {Object} dropdata the accepted drop data\n */\n showDropZone(dropdata) {\n if (dropdata.type == 'files') {\n this.addOverlay({\n content: getString('addfilehere', 'core'),\n icon: Templates.renderPix('t/download', 'core'),\n }).then(() => {\n // Check if we still need the file dropzone.\n if (!this.dragdrop?.isDropzoneVisible()) {\n this.removeOverlay();\n }\n return;\n }).catch((error) => {\n throw error;\n });\n }\n if (dropdata.type == 'cm') {\n this.getLastCm()?.classList.add(this.classes.DROPDOWN);\n }\n if (dropdata.type == 'section') {\n this.element.classList.remove(this.classes.DROPUP);\n this.element.classList.add(this.classes.DROPDOWN);\n }\n }\n\n /**\n * Hide the component dropzone.\n */\n hideDropZone() {\n this.getLastCm()?.classList.remove(this.classes.DROPDOWN);\n this.element.classList.remove(this.classes.DROPUP);\n this.element.classList.remove(this.classes.DROPDOWN);\n this.removeOverlay();\n }\n\n /**\n * Drop event handler.\n *\n * @param {Object} dropdata the accepted drop data\n * @param {Event} event the drop event\n */\n drop(dropdata, event) {\n // File handling.\n if (dropdata.type == 'files') {\n this.reactive.uploadFiles(\n this.section.id,\n this.section.number,\n dropdata.files\n );\n return;\n }\n // Call the move mutation.\n if (dropdata.type == 'cm') {\n const mutation = (event.altKey) ? 'cmDuplicate' : 'cmMove';\n this.reactive.dispatch(mutation, [dropdata.id], this.id);\n }\n if (dropdata.type == 'section') {\n this.reactive.dispatch('sectionMoveAfter', [dropdata.id], this.id);\n }\n }\n}\n"],"names":["BaseComponent","configState","state","id","this","element","dataset","section","get","course","configDragDrop","sectionitem","reactive","isEditing","supportComponents","dragdrop","DragDrop","classes","getClasses","destroy","undefined","unregister","getLastCm","dragStart","dropdata","dispatch","dragEnd","validateDropData","type","component","hasdelegatedsection","number","showDropZone","addOverlay","content","icon","Templates","renderPix","then","_this$dragdrop","isDropzoneVisible","removeOverlay","catch","error","classList","add","DROPDOWN","remove","DROPUP","hideDropZone","drop","event","mutation","altKey","uploadFiles","files"],"mappings":";;;;;;;;;;;iLAiCgB,OAAQ,CAAC,uCAEIA,wBAOzBC,YAAYC,YACHC,GAAKC,KAAKC,QAAQC,QAAQH,QAC1BI,QAAUL,MAAMK,QAAQC,IAAIJ,KAAKD,SACjCM,OAASP,MAAMO,OAQxBC,eAAeC,aAEPP,KAAKQ,SAASC,WAAaT,KAAKQ,SAASE,yBAEpCH,YAAcA,iBAEdI,SAAW,IAAIC,mBAASZ,WAExBa,QAAUb,KAAKW,SAASG,cAOrCC,eAC6BC,IAArBhB,KAAKO,kBACAA,YAAYU,kBAECD,IAAlBhB,KAAKW,eACAA,SAASM,aAStBC,mBACW,KAUXC,UAAUC,eACDZ,SAASa,SAAS,cAAe,CAACD,SAASrB,KAAK,GAQzDuB,QAAQF,eACCZ,SAASa,SAAS,cAAe,CAACD,SAASrB,KAAK,GASzDwB,iBAAiBH,gBAEU,WAAnBA,MAAAA,gBAAAA,SAAUI,QAIS,QAAnBJ,MAAAA,gBAAAA,SAAUI,iCACDrB,iDAASsB,YAA+C,KAAlCL,MAAAA,gBAAAA,SAAUM,qBAKtB,aAAnBN,MAAAA,gBAAAA,SAAUI,QAEqB,OAA3BxB,KAAKG,QAAQsB,aAIVL,MAAAA,gBAAAA,SAAUrB,KAAMC,KAAKD,KAAMqB,MAAAA,gBAAAA,SAAUO,SAAU3B,KAAKG,QAAQwB,OAAS,uBAUpFC,aAAaR,+BACY,SAAjBA,SAASI,WACJK,WAAW,CACZC,SAAS,kBAAU,cAAe,QAClCC,KAAMC,mBAAUC,UAAU,aAAc,UACzCC,MAAK,+CAEClC,KAAKW,oCAALwB,eAAeC,0BACXC,mBAGVC,OAAOC,cACAA,SAGO,MAAjBnB,SAASI,qCACJN,wDAAasB,UAAUC,IAAIzC,KAAKa,QAAQ6B,WAE5B,WAAjBtB,SAASI,YACJvB,QAAQuC,UAAUG,OAAO3C,KAAKa,QAAQ+B,aACtC3C,QAAQuC,UAAUC,IAAIzC,KAAKa,QAAQ6B,WAOhDG,kEACS3B,0DAAasB,UAAUG,OAAO3C,KAAKa,QAAQ6B,eAC3CzC,QAAQuC,UAAUG,OAAO3C,KAAKa,QAAQ+B,aACtC3C,QAAQuC,UAAUG,OAAO3C,KAAKa,QAAQ6B,eACtCL,gBASTS,KAAK1B,SAAU2B,UAEU,SAAjB3B,SAASI,SASQ,MAAjBJ,SAASI,KAAc,OACjBwB,SAAYD,MAAME,OAAU,cAAgB,cAC7CzC,SAASa,SAAS2B,SAAU,CAAC5B,SAASrB,IAAKC,KAAKD,IAEpC,WAAjBqB,SAASI,WACJhB,SAASa,SAAS,mBAAoB,CAACD,SAASrB,IAAKC,KAAKD,cAb1DS,SAAS0C,YACVlD,KAAKG,QAAQJ,GACbC,KAAKG,QAAQwB,OACbP,SAAS+B"} \ No newline at end of file +{"version":3,"file":"dndsection.min.js","sources":["../../../src/local/courseeditor/dndsection.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 * in both course index and course content.\n *\n * @module core_courseformat/local/courseeditor/dndsection\n * @class core_courseformat/local/courseeditor/dndsection\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent, DragDrop} from 'core/reactive';\nimport {getString} from 'core/str';\nimport {prefetchStrings} from 'core/prefetch';\nimport Templates from 'core/templates';\n\n// Load global strings.\nprefetchStrings('core', ['addfilehere']);\n\nexport default class extends BaseComponent {\n\n /**\n * Save some values form the state.\n *\n * @param {Object} state the current state\n */\n configState(state) {\n this.id = this.element.dataset.id;\n this.section = state.section.get(this.id);\n this.course = state.course;\n }\n\n /**\n * Register state values and the drag and drop subcomponent.\n *\n * @param {BaseComponent} sectionitem section item component\n */\n configDragDrop(sectionitem) {\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.\n this.sectionitem = sectionitem;\n // Init the dropzone.\n this.dragdrop = new DragDrop(this);\n // Save dropzone classes.\n this.classes = this.dragdrop.getClasses();\n }\n }\n\n /**\n * Remove all subcomponents dependencies.\n */\n destroy() {\n if (this.sectionitem !== undefined) {\n this.sectionitem.unregister();\n }\n if (this.dragdrop !== undefined) {\n this.dragdrop.unregister();\n }\n }\n\n /**\n * Get the last CM element of that section.\n *\n * @returns {element|null} the las course module element of the section.\n */\n getLastCm() {\n return null;\n }\n\n /**\n * Get a fallback element when there is no CM in the section.\n *\n * This is used to show the correct dropzone position.\n *\n * @returns {element|null} the las course module element of the section.\n */\n getLastCmFallback() {\n return null;\n }\n\n // Drag and drop methods.\n\n /**\n * The element drop start hook.\n *\n * @param {Object} dropdata the dropdata\n */\n dragStart(dropdata) {\n this.reactive.dispatch('sectionDrag', [dropdata.id], true);\n }\n\n /**\n * The element drop end hook.\n *\n * @param {Object} dropdata the dropdata\n */\n dragEnd(dropdata) {\n this.reactive.dispatch('sectionDrag', [dropdata.id], false);\n }\n\n /**\n * Validate if the drop data can be dropped over the component.\n *\n * @param {Object} dropdata the exported drop data.\n * @returns {boolean}\n */\n validateDropData(dropdata) {\n // We accept files.\n if (dropdata?.type === 'files') {\n return true;\n }\n // We accept any course module unless it can form a subsection loop.\n if (dropdata?.type === 'cm') {\n if (this.section?.component && dropdata?.hasdelegatedsection === true) {\n return false;\n }\n return true;\n }\n if (dropdata?.type === 'section') {\n // Sections controlled by a plugin cannot accept sections.\n if (this.section.component !== null) {\n return false;\n }\n // We accept any section but yourself and the next one.\n return dropdata?.id != this.id && dropdata?.number != this.section.number + 1;\n }\n return false;\n }\n\n /**\n * Display the component dropzone.\n *\n * @param {Object} dropdata the accepted drop data\n */\n showDropZone(dropdata) {\n if (dropdata.type == 'files') {\n this.addOverlay({\n content: getString('addfilehere', 'core'),\n icon: Templates.renderPix('t/download', 'core'),\n }).then(() => {\n // Check if we still need the file dropzone.\n if (!this.dragdrop?.isDropzoneVisible()) {\n this.removeOverlay();\n }\n return;\n }).catch((error) => {\n throw error;\n });\n }\n if (dropdata.type == 'cm') {\n const lastCm = this.getLastCm();\n lastCm?.classList.add(this.classes.DROPDOWN);\n if (!lastCm) {\n this.getLastCmFallback()?.classList.add(this.classes.DROPDOWN);\n }\n }\n if (dropdata.type == 'section') {\n this.element.classList.remove(this.classes.DROPUP);\n this.element.classList.add(this.classes.DROPDOWN);\n }\n }\n\n /**\n * Hide the component dropzone.\n */\n hideDropZone() {\n this.getLastCm()?.classList.remove(this.classes.DROPDOWN);\n this.getLastCmFallback()?.classList.remove(this.classes.DROPDOWN);\n this.element.classList.remove(this.classes.DROPUP);\n this.element.classList.remove(this.classes.DROPDOWN);\n this.removeOverlay();\n }\n\n /**\n * Drop event handler.\n *\n * @param {Object} dropdata the accepted drop data\n * @param {Event} event the drop event\n */\n drop(dropdata, event) {\n // File handling.\n if (dropdata.type == 'files') {\n this.reactive.uploadFiles(\n this.section.id,\n this.section.number,\n dropdata.files\n );\n return;\n }\n // Call the move mutation.\n if (dropdata.type == 'cm') {\n const mutation = (event.altKey) ? 'cmDuplicate' : 'cmMove';\n this.reactive.dispatch(mutation, [dropdata.id], this.id);\n }\n if (dropdata.type == 'section') {\n this.reactive.dispatch('sectionMoveAfter', [dropdata.id], this.id);\n }\n }\n}\n"],"names":["BaseComponent","configState","state","id","this","element","dataset","section","get","course","configDragDrop","sectionitem","reactive","isEditing","supportComponents","dragdrop","DragDrop","classes","getClasses","destroy","undefined","unregister","getLastCm","getLastCmFallback","dragStart","dropdata","dispatch","dragEnd","validateDropData","type","component","hasdelegatedsection","number","showDropZone","addOverlay","content","icon","Templates","renderPix","then","_this$dragdrop","isDropzoneVisible","removeOverlay","catch","error","lastCm","classList","add","DROPDOWN","remove","DROPUP","hideDropZone","drop","event","mutation","altKey","uploadFiles","files"],"mappings":";;;;;;;;;;;iLAiCgB,OAAQ,CAAC,uCAEIA,wBAOzBC,YAAYC,YACHC,GAAKC,KAAKC,QAAQC,QAAQH,QAC1BI,QAAUL,MAAMK,QAAQC,IAAIJ,KAAKD,SACjCM,OAASP,MAAMO,OAQxBC,eAAeC,aAEPP,KAAKQ,SAASC,WAAaT,KAAKQ,SAASE,yBAEpCH,YAAcA,iBAEdI,SAAW,IAAIC,mBAASZ,WAExBa,QAAUb,KAAKW,SAASG,cAOrCC,eAC6BC,IAArBhB,KAAKO,kBACAA,YAAYU,kBAECD,IAAlBhB,KAAKW,eACAA,SAASM,aAStBC,mBACW,KAUXC,2BACW,KAUXC,UAAUC,eACDb,SAASc,SAAS,cAAe,CAACD,SAAStB,KAAK,GAQzDwB,QAAQF,eACCb,SAASc,SAAS,cAAe,CAACD,SAAStB,KAAK,GASzDyB,iBAAiBH,gBAEU,WAAnBA,MAAAA,gBAAAA,SAAUI,QAIS,QAAnBJ,MAAAA,gBAAAA,SAAUI,iCACDtB,iDAASuB,YAA+C,KAAlCL,MAAAA,gBAAAA,SAAUM,qBAKtB,aAAnBN,MAAAA,gBAAAA,SAAUI,QAEqB,OAA3BzB,KAAKG,QAAQuB,aAIVL,MAAAA,gBAAAA,SAAUtB,KAAMC,KAAKD,KAAMsB,MAAAA,gBAAAA,SAAUO,SAAU5B,KAAKG,QAAQyB,OAAS,uBAUpFC,aAAaR,aACY,SAAjBA,SAASI,WACJK,WAAW,CACZC,SAAS,kBAAU,cAAe,QAClCC,KAAMC,mBAAUC,UAAU,aAAc,UACzCC,MAAK,+CAECnC,KAAKW,oCAALyB,eAAeC,0BACXC,mBAGVC,OAAOC,cACAA,SAGO,MAAjBnB,SAASI,KAAc,OACjBgB,OAASzC,KAAKkB,yCACpBuB,MAAAA,QAAAA,OAAQC,UAAUC,IAAI3C,KAAKa,QAAQ+B,WAC9BH,0CACItB,4EAAqBuB,UAAUC,IAAI3C,KAAKa,QAAQ+B,UAGxC,WAAjBvB,SAASI,YACJxB,QAAQyC,UAAUG,OAAO7C,KAAKa,QAAQiC,aACtC7C,QAAQyC,UAAUC,IAAI3C,KAAKa,QAAQ+B,WAOhDG,uFACS7B,wDAAawB,UAAUG,OAAO7C,KAAKa,QAAQ+B,8CAC3CzB,8EAAqBuB,UAAUG,OAAO7C,KAAKa,QAAQ+B,eACnD3C,QAAQyC,UAAUG,OAAO7C,KAAKa,QAAQiC,aACtC7C,QAAQyC,UAAUG,OAAO7C,KAAKa,QAAQ+B,eACtCN,gBASTU,KAAK3B,SAAU4B,UAEU,SAAjB5B,SAASI,SASQ,MAAjBJ,SAASI,KAAc,OACjByB,SAAYD,MAAME,OAAU,cAAgB,cAC7C3C,SAASc,SAAS4B,SAAU,CAAC7B,SAAStB,IAAKC,KAAKD,IAEpC,WAAjBsB,SAASI,WACJjB,SAASc,SAAS,mBAAoB,CAACD,SAAStB,IAAKC,KAAKD,cAb1DS,SAAS4C,YACVpD,KAAKG,QAAQJ,GACbC,KAAKG,QAAQyB,OACbP,SAASgC"} \ No newline at end of file diff --git a/course/format/amd/build/local/courseeditor/exporter.min.js b/course/format/amd/build/local/courseeditor/exporter.min.js index d95aa2806223d..781cd247e1840 100644 --- a/course/format/amd/build/local/courseeditor/exporter.min.js +++ b/course/format/amd/build/local/courseeditor/exporter.min.js @@ -8,6 +8,6 @@ define("core_courseformat/local/courseeditor/exporter",["exports"],(function(_ex * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class{constructor(reactive){this.reactive=reactive,this.COMPLETIONS=["incomplete","complete","complete","fail"]}course(state){var _state$course$highlig;const data={sections:[],editmode:this.reactive.isEditing,highlighted:null!==(_state$course$highlig=state.course.highlighted)&&void 0!==_state$course$highlig?_state$course$highlig:""};return this.listedSectionIds(state).forEach((sectionid=>{var _state$section$get;const sectioninfo=null!==(_state$section$get=state.section.get(sectionid))&&void 0!==_state$section$get?_state$section$get:{},section=this.section(state,sectioninfo);data.sections.push(section)})),data.hassections=0!=data.sections.length,data}listedSectionIds(state){var _state$course$section;return(null!==(_state$course$section=state.course.sectionlist)&&void 0!==_state$course$section?_state$course$section:[]).filter((sectionid=>{var _state$section$get2;return null===(null!==(_state$section$get2=state.section.get(sectionid))&&void 0!==_state$section$get2?_state$section$get2:{}).component}))}section(state,sectioninfo){var _state$course$highlig2,_sectioninfo$cmlist;const section={...sectioninfo,highlighted:null!==(_state$course$highlig2=state.course.highlighted)&&void 0!==_state$course$highlig2?_state$course$highlig2:"",cms:[]};return(null!==(_sectioninfo$cmlist=sectioninfo.cmlist)&&void 0!==_sectioninfo$cmlist?_sectioninfo$cmlist:[]).forEach((cmid=>{const cminfo=state.cm.get(cmid),cm=this.cm(state,cminfo);section.cms.push(cm)})),section.hascms=0!=section.cms.length,section}cm(state,cminfo){const cm={...cminfo,isactive:!1};if(cminfo.hasdelegatedsection){const sectioninfo=state.section.get(cminfo.delegatesectionid);cm.sectioninfo=this.section(state,sectioninfo)}return cm}cmDraggableData(state,cmid){const cminfo=state.cm.get(cmid);if(!cminfo)return null;let nextcmid;const section=state.section.get(cminfo.sectionid),currentindex=null==section?void 0:section.cmlist.indexOf(cminfo.id);return void 0!==currentindex&&(nextcmid=null==section?void 0:section.cmlist[currentindex+1]),{type:"cm",id:cminfo.id,name:cminfo.name,sectionid:cminfo.sectionid,hasdelegatedsection:cminfo.hasdelegatedsection,nextcmid:nextcmid}}sectionDraggableData(state,sectionid){const sectioninfo=state.section.get(sectionid);return sectioninfo?{type:"section",id:sectioninfo.id,name:sectioninfo.name,number:sectioninfo.number}:null}fileDraggableData(state,dataTransfer){var _dataTransfer$files;const files=[];return(null===(_dataTransfer$files=dataTransfer.files)||void 0===_dataTransfer$files?void 0:_dataTransfer$files.length)>0&&dataTransfer.files.forEach((file=>{files.push(file)})),{type:"files",files:files}}cmCompletion(state,cminfo){const data={statename:"",state:"NaN"};if(void 0!==cminfo.completionstate){var _this$COMPLETIONS$cmi;data.state=cminfo.completionstate,data.hasstate=!0;let statename=null!==(_this$COMPLETIONS$cmi=this.COMPLETIONS[cminfo.completionstate])&&void 0!==_this$COMPLETIONS$cmi?_this$COMPLETIONS$cmi:"NaN";void 0!==cminfo.isoverallcomplete&&!0===cminfo.isoverallcomplete&&(statename="complete"),data["is".concat(statename)]=!0}return data}allItemsArray(state){var _state$course$section2;const items=[];return(null!==(_state$course$section2=state.course.sectionlist)&&void 0!==_state$course$section2?_state$course$section2:[]).forEach((sectionid=>{var _sectioninfo$cmlist2;const sectioninfo=state.section.get(sectionid);items.push({type:"section",id:sectioninfo.id,url:sectioninfo.sectionurl});(null!==(_sectioninfo$cmlist2=sectioninfo.cmlist)&&void 0!==_sectioninfo$cmlist2?_sectioninfo$cmlist2:[]).forEach((cmid=>{const cminfo=state.cm.get(cmid);items.push({type:"cm",id:cminfo.id,url:cminfo.url})}))})),items}canUseStealth(state,cmIds){return cmIds.some((cmId=>{var _cminfo$allowstealth;const cminfo=state.cm.get(cmId);return null!==(_cminfo$allowstealth=null==cminfo?void 0:cminfo.allowstealth)&&void 0!==_cminfo$allowstealth&&_cminfo$allowstealth}))}},_exports.default})); +class{constructor(reactive){this.reactive=reactive,this.COMPLETIONS=["incomplete","complete","complete","fail"]}course(state){var _state$course$highlig;const data={sections:[],editmode:this.reactive.isEditing,highlighted:null!==(_state$course$highlig=state.course.highlighted)&&void 0!==_state$course$highlig?_state$course$highlig:""};return this.listedSectionIds(state).forEach((sectionid=>{var _state$section$get;const sectioninfo=null!==(_state$section$get=state.section.get(sectionid))&&void 0!==_state$section$get?_state$section$get:{},section=this.section(state,sectioninfo);data.sections.push(section)})),data.hassections=0!=data.sections.length,data}listedSectionIds(state){var _state$course$section;return(null!==(_state$course$section=state.course.sectionlist)&&void 0!==_state$course$section?_state$course$section:[]).filter((sectionid=>{var _state$section$get2;return null===(null!==(_state$section$get2=state.section.get(sectionid))&&void 0!==_state$section$get2?_state$section$get2:{}).component}))}section(state,sectioninfo){var _state$course$highlig2,_sectioninfo$cmlist;const section={...sectioninfo,highlighted:null!==(_state$course$highlig2=state.course.highlighted)&&void 0!==_state$course$highlig2?_state$course$highlig2:"",cms:[]};return(null!==(_sectioninfo$cmlist=sectioninfo.cmlist)&&void 0!==_sectioninfo$cmlist?_sectioninfo$cmlist:[]).forEach((cmid=>{const cminfo=state.cm.get(cmid),cm=this.cm(state,cminfo);section.cms.push(cm)})),section.hascms=0!=section.cms.length,section}cm(state,cminfo){const cm={...cminfo,isactive:!1,sectioninfo:!1};if(cminfo.hasdelegatedsection){const sectioninfo=state.section.get(cminfo.delegatesectionid);cm.sectioninfo=this.section(state,sectioninfo)}return cm}cmDraggableData(state,cmid){const cminfo=state.cm.get(cmid);if(!cminfo)return null;let nextcmid;const section=state.section.get(cminfo.sectionid),currentindex=null==section?void 0:section.cmlist.indexOf(cminfo.id);return void 0!==currentindex&&(nextcmid=null==section?void 0:section.cmlist[currentindex+1]),{type:"cm",id:cminfo.id,name:cminfo.name,sectionid:cminfo.sectionid,hasdelegatedsection:cminfo.hasdelegatedsection,nextcmid:nextcmid}}sectionDraggableData(state,sectionid){const sectioninfo=state.section.get(sectionid);return sectioninfo?{type:"section",id:sectioninfo.id,name:sectioninfo.name,number:sectioninfo.number}:null}fileDraggableData(state,dataTransfer){var _dataTransfer$files;const files=[];return(null===(_dataTransfer$files=dataTransfer.files)||void 0===_dataTransfer$files?void 0:_dataTransfer$files.length)>0&&dataTransfer.files.forEach((file=>{files.push(file)})),{type:"files",files:files}}cmCompletion(state,cminfo){const data={statename:"",state:"NaN"};if(void 0!==cminfo.completionstate){var _this$COMPLETIONS$cmi;data.state=cminfo.completionstate,data.hasstate=!0;let statename=null!==(_this$COMPLETIONS$cmi=this.COMPLETIONS[cminfo.completionstate])&&void 0!==_this$COMPLETIONS$cmi?_this$COMPLETIONS$cmi:"NaN";void 0!==cminfo.isoverallcomplete&&!0===cminfo.isoverallcomplete&&(statename="complete"),data["is".concat(statename)]=!0}return data}allItemsArray(state){var _state$course$section2;const items=[];return(null!==(_state$course$section2=state.course.sectionlist)&&void 0!==_state$course$section2?_state$course$section2:[]).forEach((sectionid=>{var _sectioninfo$cmlist2;const sectioninfo=state.section.get(sectionid);items.push({type:"section",id:sectioninfo.id,url:sectioninfo.sectionurl});(null!==(_sectioninfo$cmlist2=sectioninfo.cmlist)&&void 0!==_sectioninfo$cmlist2?_sectioninfo$cmlist2:[]).forEach((cmid=>{const cminfo=state.cm.get(cmid);items.push({type:"cm",id:cminfo.id,url:cminfo.url})}))})),items}canUseStealth(state,cmIds){return cmIds.some((cmId=>{var _cminfo$allowstealth;const cminfo=state.cm.get(cmId);return null!==(_cminfo$allowstealth=null==cminfo?void 0:cminfo.allowstealth)&&void 0!==_cminfo$allowstealth&&_cminfo$allowstealth}))}},_exports.default})); //# sourceMappingURL=exporter.min.js.map \ No newline at end of file diff --git a/course/format/amd/build/local/courseeditor/exporter.min.js.map b/course/format/amd/build/local/courseeditor/exporter.min.js.map index 6f665ee8002ae..0b9fd79c4ab6f 100644 --- a/course/format/amd/build/local/courseeditor/exporter.min.js.map +++ b/course/format/amd/build/local/courseeditor/exporter.min.js.map @@ -1 +1 @@ -{"version":3,"file":"exporter.min.js","sources":["../../../src/local/courseeditor/exporter.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 * Module to export parts of the state and transform them to be used in templates\n * and as draggable data.\n *\n * @module core_courseformat/local/courseeditor/exporter\n * @class core_courseformat/local/courseeditor/exporter\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class {\n\n /**\n * Class constructor.\n *\n * @param {CourseEditor} reactive the course editor object\n */\n constructor(reactive) {\n this.reactive = reactive;\n\n // Completions states are defined in lib/completionlib.php. There are 4 different completion\n // state values, however, the course index uses the same state for complete and complete_pass.\n // This is the reason why completed appears twice in the array.\n this.COMPLETIONS = ['incomplete', 'complete', 'complete', 'fail'];\n }\n\n /**\n * Generate the course export data from the state.\n *\n * @param {Object} state the current state.\n * @returns {Object}\n */\n course(state) {\n // Collect section information from the state.\n const data = {\n sections: [],\n editmode: this.reactive.isEditing,\n highlighted: state.course.highlighted ?? '',\n };\n const sectionlist = this.listedSectionIds(state);\n sectionlist.forEach(sectionid => {\n const sectioninfo = state.section.get(sectionid) ?? {};\n const section = this.section(state, sectioninfo);\n data.sections.push(section);\n });\n data.hassections = (data.sections.length != 0);\n\n return data;\n }\n\n /**\n * Get the IDs of the sections that are listed as regular sections.\n * @param {Object} state the current state.\n * @returns {Number[]} the list of section ids that are listed.\n */\n listedSectionIds(state) {\n const fullSectionList = state.course.sectionlist ?? [];\n return fullSectionList.filter(sectionid => {\n const sectioninfo = state.section.get(sectionid) ?? {};\n // Delegated sections (controlled by a component) are not listed in course.\n return sectioninfo.component === null;\n });\n }\n\n /**\n * Generate a section export data from the state.\n *\n * @param {Object} state the current state.\n * @param {Object} sectioninfo the section state data.\n * @returns {Object}\n */\n section(state, sectioninfo) {\n const section = {\n ...sectioninfo,\n highlighted: state.course.highlighted ?? '',\n cms: [],\n };\n const cmlist = sectioninfo.cmlist ?? [];\n cmlist.forEach(cmid => {\n const cminfo = state.cm.get(cmid);\n const cm = this.cm(state, cminfo);\n section.cms.push(cm);\n });\n section.hascms = (section.cms.length != 0);\n\n return section;\n }\n\n /**\n * Generate a cm export data from the state.\n *\n * @param {Object} state the current state.\n * @param {Object} cminfo the course module state data.\n * @returns {Object}\n */\n cm(state, cminfo) {\n const cm = {\n ...cminfo,\n isactive: false,\n };\n if (cminfo.hasdelegatedsection) {\n const sectioninfo = state.section.get(cminfo.delegatesectionid);\n cm.sectioninfo = this.section(state, sectioninfo);\n }\n return cm;\n }\n\n /**\n * Generate a dragable cm data structure.\n *\n * This method is used by any draggable course module element to generate drop data\n * for its reactive/dragdrop instance.\n *\n * @param {*} state the state object\n * @param {*} cmid the cours emodule id\n * @returns {Object|null}\n */\n cmDraggableData(state, cmid) {\n const cminfo = state.cm.get(cmid);\n if (!cminfo) {\n return null;\n }\n\n // Drop an activity over the next activity is the same as doing anything.\n let nextcmid;\n const section = state.section.get(cminfo.sectionid);\n const currentindex = section?.cmlist.indexOf(cminfo.id);\n if (currentindex !== undefined) {\n nextcmid = section?.cmlist[currentindex + 1];\n }\n\n return {\n type: 'cm',\n id: cminfo.id,\n name: cminfo.name,\n sectionid: cminfo.sectionid,\n hasdelegatedsection: cminfo.hasdelegatedsection,\n nextcmid,\n };\n }\n\n /**\n * Generate a dragable cm data structure.\n *\n * This method is used by any draggable section element to generate drop data\n * for its reactive/dragdrop instance.\n *\n * @param {*} state the state object\n * @param {*} sectionid the cours section id\n * @returns {Object|null}\n */\n sectionDraggableData(state, sectionid) {\n const sectioninfo = state.section.get(sectionid);\n if (!sectioninfo) {\n return null;\n }\n return {\n type: 'section',\n id: sectioninfo.id,\n name: sectioninfo.name,\n number: sectioninfo.number,\n };\n }\n\n /**\n * Generate a file draggable structure.\n *\n * This method is used when files are dragged on the browser.\n *\n * @param {*} state the state object\n * @param {*} dataTransfer the current data tranfer data\n * @returns {Object|null}\n */\n fileDraggableData(state, dataTransfer) {\n const files = [];\n // Browsers do not provide the file list until the drop event.\n if (dataTransfer.files?.length > 0) {\n dataTransfer.files.forEach(file => {\n files.push(file);\n });\n }\n return {\n type: 'files',\n files,\n };\n }\n\n /**\n * Generate a completion export data from the cm element.\n *\n * @param {Object} state the current state.\n * @param {Object} cminfo the course module state data.\n * @returns {Object}\n */\n cmCompletion(state, cminfo) {\n const data = {\n statename: '',\n state: 'NaN',\n };\n if (cminfo.completionstate !== undefined) {\n data.state = cminfo.completionstate;\n data.hasstate = true;\n let statename = this.COMPLETIONS[cminfo.completionstate] ?? 'NaN';\n if (cminfo.isoverallcomplete !== undefined && cminfo.isoverallcomplete === true) {\n statename = 'complete';\n }\n data[`is${statename}`] = true;\n }\n return data;\n }\n\n /**\n * Return a sorted list of all sections and cms items in the state.\n *\n * @param {Object} state the current state.\n * @returns {Array} all sections and cms items in the state.\n */\n allItemsArray(state) {\n const items = [];\n const sectionlist = state.course.sectionlist ?? [];\n // Add sections.\n sectionlist.forEach(sectionid => {\n const sectioninfo = state.section.get(sectionid);\n items.push({type: 'section', id: sectioninfo.id, url: sectioninfo.sectionurl});\n // Add cms.\n const cmlist = sectioninfo.cmlist ?? [];\n cmlist.forEach(cmid => {\n const cminfo = state.cm.get(cmid);\n items.push({type: 'cm', id: cminfo.id, url: cminfo.url});\n });\n });\n return items;\n }\n\n /**\n * Check is some activities of a list can be stealth.\n *\n * @param {Object} state the current state.\n * @param {Number[]} cmIds the module ids to check\n * @returns {Boolean} if any of the activities can be stealth.\n */\n canUseStealth(state, cmIds) {\n return cmIds.some(cmId => {\n const cminfo = state.cm.get(cmId);\n return cminfo?.allowstealth ?? false;\n });\n }\n}\n"],"names":["constructor","reactive","COMPLETIONS","course","state","data","sections","editmode","this","isEditing","highlighted","listedSectionIds","forEach","sectionid","sectioninfo","section","get","push","hassections","length","sectionlist","filter","component","cms","cmlist","cmid","cminfo","cm","hascms","isactive","hasdelegatedsection","delegatesectionid","cmDraggableData","nextcmid","currentindex","indexOf","id","undefined","type","name","sectionDraggableData","number","fileDraggableData","dataTransfer","files","file","cmCompletion","statename","completionstate","hasstate","isoverallcomplete","allItemsArray","items","url","sectionurl","canUseStealth","cmIds","some","cmId","allowstealth"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"exporter.min.js","sources":["../../../src/local/courseeditor/exporter.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 * Module to export parts of the state and transform them to be used in templates\n * and as draggable data.\n *\n * @module core_courseformat/local/courseeditor/exporter\n * @class core_courseformat/local/courseeditor/exporter\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class {\n\n /**\n * Class constructor.\n *\n * @param {CourseEditor} reactive the course editor object\n */\n constructor(reactive) {\n this.reactive = reactive;\n\n // Completions states are defined in lib/completionlib.php. There are 4 different completion\n // state values, however, the course index uses the same state for complete and complete_pass.\n // This is the reason why completed appears twice in the array.\n this.COMPLETIONS = ['incomplete', 'complete', 'complete', 'fail'];\n }\n\n /**\n * Generate the course export data from the state.\n *\n * @param {Object} state the current state.\n * @returns {Object}\n */\n course(state) {\n // Collect section information from the state.\n const data = {\n sections: [],\n editmode: this.reactive.isEditing,\n highlighted: state.course.highlighted ?? '',\n };\n const sectionlist = this.listedSectionIds(state);\n sectionlist.forEach(sectionid => {\n const sectioninfo = state.section.get(sectionid) ?? {};\n const section = this.section(state, sectioninfo);\n data.sections.push(section);\n });\n data.hassections = (data.sections.length != 0);\n\n return data;\n }\n\n /**\n * Get the IDs of the sections that are listed as regular sections.\n * @param {Object} state the current state.\n * @returns {Number[]} the list of section ids that are listed.\n */\n listedSectionIds(state) {\n const fullSectionList = state.course.sectionlist ?? [];\n return fullSectionList.filter(sectionid => {\n const sectioninfo = state.section.get(sectionid) ?? {};\n // Delegated sections (controlled by a component) are not listed in course.\n return sectioninfo.component === null;\n });\n }\n\n /**\n * Generate a section export data from the state.\n *\n * @param {Object} state the current state.\n * @param {Object} sectioninfo the section state data.\n * @returns {Object}\n */\n section(state, sectioninfo) {\n const section = {\n ...sectioninfo,\n highlighted: state.course.highlighted ?? '',\n cms: [],\n };\n const cmlist = sectioninfo.cmlist ?? [];\n cmlist.forEach(cmid => {\n const cminfo = state.cm.get(cmid);\n const cm = this.cm(state, cminfo);\n section.cms.push(cm);\n });\n section.hascms = (section.cms.length != 0);\n\n return section;\n }\n\n /**\n * Generate a cm export data from the state.\n *\n * @param {Object} state the current state.\n * @param {Object} cminfo the course module state data.\n * @returns {Object}\n */\n cm(state, cminfo) {\n const cm = {\n ...cminfo,\n isactive: false,\n sectioninfo: false, // Init to false to prevent mustache recursion loops.\n };\n if (cminfo.hasdelegatedsection) {\n const sectioninfo = state.section.get(cminfo.delegatesectionid);\n cm.sectioninfo = this.section(state, sectioninfo);\n }\n return cm;\n }\n\n /**\n * Generate a dragable cm data structure.\n *\n * This method is used by any draggable course module element to generate drop data\n * for its reactive/dragdrop instance.\n *\n * @param {*} state the state object\n * @param {*} cmid the cours emodule id\n * @returns {Object|null}\n */\n cmDraggableData(state, cmid) {\n const cminfo = state.cm.get(cmid);\n if (!cminfo) {\n return null;\n }\n\n // Drop an activity over the next activity is the same as doing anything.\n let nextcmid;\n const section = state.section.get(cminfo.sectionid);\n const currentindex = section?.cmlist.indexOf(cminfo.id);\n if (currentindex !== undefined) {\n nextcmid = section?.cmlist[currentindex + 1];\n }\n\n return {\n type: 'cm',\n id: cminfo.id,\n name: cminfo.name,\n sectionid: cminfo.sectionid,\n hasdelegatedsection: cminfo.hasdelegatedsection,\n nextcmid,\n };\n }\n\n /**\n * Generate a dragable cm data structure.\n *\n * This method is used by any draggable section element to generate drop data\n * for its reactive/dragdrop instance.\n *\n * @param {*} state the state object\n * @param {*} sectionid the cours section id\n * @returns {Object|null}\n */\n sectionDraggableData(state, sectionid) {\n const sectioninfo = state.section.get(sectionid);\n if (!sectioninfo) {\n return null;\n }\n return {\n type: 'section',\n id: sectioninfo.id,\n name: sectioninfo.name,\n number: sectioninfo.number,\n };\n }\n\n /**\n * Generate a file draggable structure.\n *\n * This method is used when files are dragged on the browser.\n *\n * @param {*} state the state object\n * @param {*} dataTransfer the current data tranfer data\n * @returns {Object|null}\n */\n fileDraggableData(state, dataTransfer) {\n const files = [];\n // Browsers do not provide the file list until the drop event.\n if (dataTransfer.files?.length > 0) {\n dataTransfer.files.forEach(file => {\n files.push(file);\n });\n }\n return {\n type: 'files',\n files,\n };\n }\n\n /**\n * Generate a completion export data from the cm element.\n *\n * @param {Object} state the current state.\n * @param {Object} cminfo the course module state data.\n * @returns {Object}\n */\n cmCompletion(state, cminfo) {\n const data = {\n statename: '',\n state: 'NaN',\n };\n if (cminfo.completionstate !== undefined) {\n data.state = cminfo.completionstate;\n data.hasstate = true;\n let statename = this.COMPLETIONS[cminfo.completionstate] ?? 'NaN';\n if (cminfo.isoverallcomplete !== undefined && cminfo.isoverallcomplete === true) {\n statename = 'complete';\n }\n data[`is${statename}`] = true;\n }\n return data;\n }\n\n /**\n * Return a sorted list of all sections and cms items in the state.\n *\n * @param {Object} state the current state.\n * @returns {Array} all sections and cms items in the state.\n */\n allItemsArray(state) {\n const items = [];\n const sectionlist = state.course.sectionlist ?? [];\n // Add sections.\n sectionlist.forEach(sectionid => {\n const sectioninfo = state.section.get(sectionid);\n items.push({type: 'section', id: sectioninfo.id, url: sectioninfo.sectionurl});\n // Add cms.\n const cmlist = sectioninfo.cmlist ?? [];\n cmlist.forEach(cmid => {\n const cminfo = state.cm.get(cmid);\n items.push({type: 'cm', id: cminfo.id, url: cminfo.url});\n });\n });\n return items;\n }\n\n /**\n * Check is some activities of a list can be stealth.\n *\n * @param {Object} state the current state.\n * @param {Number[]} cmIds the module ids to check\n * @returns {Boolean} if any of the activities can be stealth.\n */\n canUseStealth(state, cmIds) {\n return cmIds.some(cmId => {\n const cminfo = state.cm.get(cmId);\n return cminfo?.allowstealth ?? false;\n });\n }\n}\n"],"names":["constructor","reactive","COMPLETIONS","course","state","data","sections","editmode","this","isEditing","highlighted","listedSectionIds","forEach","sectionid","sectioninfo","section","get","push","hassections","length","sectionlist","filter","component","cms","cmlist","cmid","cminfo","cm","hascms","isactive","hasdelegatedsection","delegatesectionid","cmDraggableData","nextcmid","currentindex","indexOf","id","undefined","type","name","sectionDraggableData","number","fileDraggableData","dataTransfer","files","file","cmCompletion","statename","completionstate","hasstate","isoverallcomplete","allItemsArray","items","url","sectionurl","canUseStealth","cmIds","some","cmId","allowstealth"],"mappings":"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"} \ No newline at end of file diff --git a/course/format/amd/build/local/courseeditor/fileuploader.min.js b/course/format/amd/build/local/courseeditor/fileuploader.min.js index cbf6ba3d164cd..4396849599e9c 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"}];window.console.log(allStrings);const 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;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;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 window.console.log(allStrings);\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","window","console","log","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 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":"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,mBA4OQN,SAAUC,eAC7BkD,QAAUxD,cAAcyD,IAAIpD,UAC3BmD,UACDA,QAAU,IAAIE,KAElBF,QAAQG,IAAIrD,WACZN,cAAc4D,IAAIvD,SAAUmD,SAC5BK,uBAlPIC,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,OAC5C6C,KAAKG,SAASK,KAAK,IACZnG,QACHoG,SAAUF,UACVG,+BAAyBV,KAAKE,UAC9BS,MAAOL,QAEXF,WAAaA,YAAcG,cAE1BH,YAAcJ,KAAKG,SAAS1B,OAAS,EAAG,OACnCmC,YAAcZ,KAAKG,SAASU,MAClCD,YAAYH,UAAW,EACvBT,KAAKG,SAASK,KAAKI,oBAEhBZ,KAYXF,uBAAuBH,MAAOZ,oBACpB+B,WAAY,uBAASnB,MAAMoB,kBAC1B,IAAIC,SAAQ,CAACC,QAASC,UACzBvB,MAAMwB,UAAUC,GACZC,sBAAYC,MACZzF,cAEUyE,MAAQQ,UAAUS,cAAc,iBAAiBZ,MACvD9E,MAAM2F,iBACN7B,MAAM8B,UACD1C,aAAauB,QACdY,OAAO,4BAEXD,QAAQlC,aAAauB,WAI7BX,MAAMwB,UAAUC,GACZC,sBAAYK,QACZ,KACIT,QAAQ,YAYxBrB,yBAAyBT,oBACd,IAAI6B,SAAQ,CAACC,QAASC,qCACTS,OAAOxC,aAAayC,MAAMjC,QACtCA,MAAMkC,kBAAiB,GAEvBlC,MAAMwB,UAAUC,GAAGC,sBAAYS,cAAc,KACzCb,QAAQtB,eAGuBoC,IAA/B5C,YAAYO,gBACZC,MAAMqC,kBAAkB7C,YAAYO,gBAExCC,MAAMsC,UAEPC,OAAM,KACLhB,iDA0BVzD,sBAAuB,oBACzB,WACU0E,UAAYvI,cAClBA,cAAgB,IAAIC,IACpBsI,UAAU9B,SAAQ,CAAC+B,WAAYnI,kBACrB6D,cAAe,iCAAgB7D,UAChC6D,cAGLA,aAAauE,SAAS,eAAgB,IAAID,kBAzZhC,WAkdhBE,gBAAkBC,eAAetI,SAAUC,UAAWC,WAAYC,SAAUoI,oBAC1EnI,QACJX,kBAAoB+I,gCAAeC,yBAE/BF,eAAe7D,aAAavE,UAC5BC,cAAgBmI,eAAeG,eAAevI,UAChD,MAAOwC,mBACLlD,YAAYkJ,SAASxI,SAASkE,KAAM1B,MAAMiG,aAIzCxI,qBAGCyI,cAAgB,IAAI/I,aAAaE,SAAUC,UAAWC,WAAYC,SAAUC,SAClFX,YAAYqJ,WAAW3I,SAASkE,KAAMwE,cAActH,sDAarB+G,eAAetI,SAAUC,UAAWC,WAAY6I,aAEzER,oCA3E8BvI,kBACF8H,IAA9BpI,gBAAgBM,iBACTN,gBAAgBM,gBAErBuI,eAAiB,IAAI5E,eAAe3D,uBACpCuI,eAAeS,eACrBtJ,gBAAgBM,UAAYuI,eACrB7I,gBAAgBM,UAoEMiJ,CAAyBjJ,+BA7D1BA,yDACb,OAAXH,oBAIEqJ,sEADe,iCAAgBlJ,UACHoD,IAAI,8CAAjB+F,kBAA4BD,oEAAgB,IAEjErJ,OAAS,SACHuJ,WAAa,CACf,CAACC,IAAK,cAAeC,UAAW,aAAcC,MAAO,CAACnI,KAAM8H,eAC5D,CAACG,IAAK,UAAWC,UAAW,cAC5B,CAACD,IAAK,YAAaC,UAAW,cAC9B,CAACD,IAAK,gBAAiBC,UAAW,eAGhCE,oBAAsB,mBAAWJ,YACvCA,WAAWhD,SAAQ,MAAQC,aAAPgD,IAACA,UACjBxJ,OAAOwJ,KAAOG,cAAcnD,UA6C1BoD,CAAiBzJ,cAClB,IAAIqG,MAAQ,EAAGA,MAAQ0C,MAAMvE,OAAQ6B,QAAS,OACzClG,SAAW4I,MAAM1C,aACjBgC,gBAAgBrI,SAAUC,UAAWC,WAAYC,SAAUoI"} \ No newline at end of file diff --git a/course/format/amd/src/local/content/actions.js b/course/format/amd/src/local/content/actions.js index eb75eabe0747b..db069cf6bd31f 100644 --- a/course/format/amd/src/local/content/actions.js +++ b/course/format/amd/src/local/content/actions.js @@ -40,6 +40,7 @@ import Pending from 'core/pending'; import ContentTree from 'core_courseformat/local/courseeditor/contenttree'; // The jQuery module is only used for interacting with Boostrap 4. It can we removed when MDL-71979 is integrated. import jQuery from 'jquery'; +import Notification from "core/notification"; // Load global strings. prefetchStrings('core', ['movecoursesection', 'movecoursemodule', 'confirm', 'delete']); @@ -82,11 +83,15 @@ export default class extends BaseComponent { ACTIONMENUTOGGLER: `[data-toggle="dropdown"]`, // Availability modal selectors. OPTIONSRADIO: `[type='radio']`, + COURSEADDSECTION: `#course-addsection`, + MAXSECTIONSWARNING: `[data-region='max-sections-warning']`, + ADDSECTIONREGION: `[data-region='section-addsection']`, }; // Component css classes. this.classes = { - DISABLED: `text-body`, + DISABLED: `disabled`, ITALIC: `font-italic`, + DISPLAYNONE: `d-none`, }; } @@ -357,16 +362,15 @@ export default class extends BaseComponent { // Open the cm section node if possible (Bootstrap 4 uses jQuery to interact with collapsibles). // All jQuery in this code can be replaced when MDL-71979 is integrated. cmIds.forEach(cmId => { - const currentElement = modalBody.querySelector(`${this.selectors.CMLINK}[data-id='${cmId}']`); - const sectionnode = currentElement.closest(this.selectors.SECTIONNODE); - const toggler = jQuery(sectionnode).find(this.selectors.MODALTOGGLER); - let collapsibleId = toggler.data('target') ?? toggler.attr('href'); - if (collapsibleId) { - // We cannot be sure we have # in the id element name. - collapsibleId = collapsibleId.replace('#', ''); - const expandNode = modalBody.querySelector(`#${collapsibleId}`); - jQuery(expandNode).collapse('show'); + const cmInfo = this.reactive.get('cm', cmId); + let selector; + if (!cmInfo.hasdelegatedsection) { + selector = `${this.selectors.CMLINK}[data-id='${cmId}']`; + } else { + selector = `${this.selectors.SECTIONLINK}[data-id='${cmInfo.sectionid}']`; } + const currentElement = modalBody.querySelector(selector); + this._expandCmMoveModalParentSections(modalBody, currentElement); }); modalBody.addEventListener('click', (event) => { @@ -381,6 +385,7 @@ export default class extends BaseComponent { let targetSectionId; let targetCmId; + let droppedCmIds = [...cmIds]; if (target.dataset.for == 'cm') { const dropData = exporter.cmDraggableData(this.reactive.state, target.dataset.id); targetSectionId = dropData.sectionid; @@ -390,13 +395,54 @@ export default class extends BaseComponent { targetSectionId = target.dataset.id; targetCmId = section?.cmlist[0]; } - this.reactive.dispatch('cmMove', cmIds, targetSectionId, targetCmId); + const section = this.reactive.get('section', targetSectionId); + if (section.component) { + // Remove cmIds which are not allowed to be moved to this delegated section (mostly + // all other delegated cm). + droppedCmIds = droppedCmIds.filter(cmId => { + const cmInfo = this.reactive.get('cm', cmId); + return !cmInfo.hasdelegatedsection; + }); + } + if (droppedCmIds.length === 0) { + return; // No cm to move. + } + this.reactive.dispatch('cmMove', droppedCmIds, targetSectionId, targetCmId); this._destroyModal(modal, editTools); }); pendingModalReady.resolve(); } + /** + * Expand all the modal tree branches that contains the element. + * + * Bootstrap 4 uses jQuery to interact with collapsibles. + * All jQuery in this code can be replaced when MDL-71979 is integrated. + * + * @private + * @param {HTMLElement} modalBody the modal body element + * @param {HTMLElement} element the element to display + */ + _expandCmMoveModalParentSections(modalBody, element) { + const sectionnode = element.closest(this.selectors.SECTIONNODE); + if (!sectionnode) { + return; + } + + const toggler = jQuery(sectionnode).find(this.selectors.MODALTOGGLER); + let collapsibleId = toggler.data('target') ?? toggler.attr('href'); + if (collapsibleId) { + // We cannot be sure we have # in the id element name. + collapsibleId = collapsibleId.replace('#', ''); + const expandNode = modalBody.querySelector(`#${collapsibleId}`); + jQuery(expandNode).collapse('show'); + } + + // Section are a tree structure, we need to expand all the parents. + this._expandCmMoveModalParentSections(modalBody, sectionnode.parentElement); + } + /** * Handle a create section request. * @@ -678,12 +724,28 @@ export default class extends BaseComponent { * @param {boolean} locked the new locked value. */ _setAddSectionLocked(locked) { - const targets = this.getElements(this.selectors.ADDSECTION); + const targets = this.getElements(this.selectors.ADDSECTIONREGION); targets.forEach(element => { element.classList.toggle(this.classes.DISABLED, locked); - element.classList.toggle(this.classes.ITALIC, locked); - this.setElementLocked(element, locked); + const addSectionElement = element.querySelector(this.selectors.ADDSECTION); + addSectionElement.classList.toggle(this.classes.DISABLED, locked); + this.setElementLocked(addSectionElement, locked); + // We tweak the element to show a tooltip as a title attribute. + if (locked) { + getString('sectionaddmax', 'core_courseformat') + .then((text) => addSectionElement.setAttribute('title', text)) + .catch(Notification.exception); + addSectionElement.style.pointerEvents = null; // Unlocks the pointer events. + addSectionElement.style.userSelect = null; // Unlocks the pointer events. + } else { + addSectionElement.setAttribute('title', addSectionElement.dataset.addSections); + } }); + const courseAddSection = this.getElement(this.selectors.COURSEADDSECTION); + const addSection = courseAddSection.querySelector(this.selectors.ADDSECTION); + addSection.classList.toggle(this.classes.DISPLAYNONE, locked); + const noMoreSections = courseAddSection.querySelector(this.selectors.MAXSECTIONSWARNING); + noMoreSections.classList.toggle(this.classes.DISPLAYNONE, !locked); } /** diff --git a/course/format/amd/src/local/content/section.js b/course/format/amd/src/local/content/section.js index 135e55f355e41..4f0d9c894ee7f 100644 --- a/course/format/amd/src/local/content/section.js +++ b/course/format/amd/src/local/content/section.js @@ -37,6 +37,7 @@ export default class extends DndSection { this.name = 'content_section'; // Default query selectors. this.selectors = { + ACTIONMENU: '.section-actions', SECTION_ITEM: `[data-for='section_title']`, CM: `[data-for="cmitem"]`, SECTIONINFO: `[data-for="sectioninfo"]`, @@ -119,7 +120,7 @@ export default class extends DndSection { */ validateDropData(dropdata) { // If the format uses one section per page sections dropping in the content is ignored. - if (dropdata?.type === 'section' && this.reactive.sectionReturn !== null) { + if (dropdata?.type === 'section' && this.reactive.sectionReturn !== null) { return false; } return super.validateDropData(dropdata); @@ -136,7 +137,24 @@ export default class extends DndSection { if (!cms || cms.length === 0) { return null; } - return cms[cms.length - 1]; + const lastCm = cms[cms.length - 1]; + // If it is a delegated section return the last item overall. + if (this.section.component !== null) { + return lastCm; + } + // If it is a regular section and the last item overall has a parent cm, return the parent instead. + const parentSection = lastCm.parentNode.closest(this.selectors.CM); + return parentSection ?? lastCm; + } + + /** + * Get a fallback element when there is no CM in the section. + * + * @returns {element|null} the las course module element of the section. + */ + getLastCmFallback() { + // The sectioninfo is always present, even when the section is empty. + return this.getElement(this.selectors.SECTIONINFO); } /** @@ -208,6 +226,8 @@ export default class extends DndSection { const icon = affectedAction.querySelector(this.selectors.ICON); if (affectedAction.dataset?.swapicon && icon) { const newIcon = affectedAction.dataset.swapicon; + affectedAction.dataset.swapicon = affectedAction.dataset.icon; + affectedAction.dataset.icon = newIcon; if (newIcon) { const pixHtml = await Templates.renderPix(newIcon, 'core'); Templates.replaceNode(icon, pixHtml, ''); @@ -222,10 +242,6 @@ export default class extends DndSection { * @returns The action menu element. */ _getActionMenu(selector) { - if (this.getElement('.section_action_menu')) { - return this.getElement(selector); - } - - return document.querySelector(selector); + return document.querySelector(`${this.selectors.ACTIONMENU}[data-sectionid='${this.id}'] ${selector}`); } } diff --git a/course/format/amd/src/local/content/section/cmitem.js b/course/format/amd/src/local/content/section/cmitem.js index 4670612b58e04..7cb7c97005429 100644 --- a/course/format/amd/src/local/content/section/cmitem.js +++ b/course/format/amd/src/local/content/section/cmitem.js @@ -40,7 +40,7 @@ export default class extends DndCmItem { BULKCHECKBOX: `[data-bulkcheckbox]`, CARD: `[data-region='activity-card']`, DRAGICON: `.editing_move`, - INPLACEEDITABLE: `[data-inplaceeditablelink]`, + INPLACEEDITABLE: `[data-itemtype="activityname"] > [data-inplaceeditablelink]`, }; // Most classes will be loaded later by DndCmItem. this.classes = { diff --git a/course/format/amd/src/local/courseeditor/dndsection.js b/course/format/amd/src/local/courseeditor/dndsection.js index ad0aaf8078ed0..a6d8b3f74c636 100644 --- a/course/format/amd/src/local/courseeditor/dndsection.js +++ b/course/format/amd/src/local/courseeditor/dndsection.js @@ -84,6 +84,17 @@ export default class extends BaseComponent { return null; } + /** + * Get a fallback element when there is no CM in the section. + * + * This is used to show the correct dropzone position. + * + * @returns {element|null} the las course module element of the section. + */ + getLastCmFallback() { + return null; + } + // Drag and drop methods. /** @@ -154,7 +165,11 @@ export default class extends BaseComponent { }); } if (dropdata.type == 'cm') { - this.getLastCm()?.classList.add(this.classes.DROPDOWN); + const lastCm = this.getLastCm(); + lastCm?.classList.add(this.classes.DROPDOWN); + if (!lastCm) { + this.getLastCmFallback()?.classList.add(this.classes.DROPDOWN); + } } if (dropdata.type == 'section') { this.element.classList.remove(this.classes.DROPUP); @@ -167,6 +182,7 @@ export default class extends BaseComponent { */ hideDropZone() { this.getLastCm()?.classList.remove(this.classes.DROPDOWN); + this.getLastCmFallback()?.classList.remove(this.classes.DROPDOWN); this.element.classList.remove(this.classes.DROPUP); this.element.classList.remove(this.classes.DROPDOWN); this.removeOverlay(); diff --git a/course/format/amd/src/local/courseeditor/exporter.js b/course/format/amd/src/local/courseeditor/exporter.js index e7846229f6bbb..cd8dbc3fa1a21 100644 --- a/course/format/amd/src/local/courseeditor/exporter.js +++ b/course/format/amd/src/local/courseeditor/exporter.js @@ -111,6 +111,7 @@ export default class { const cm = { ...cminfo, isactive: false, + sectioninfo: false, // Init to false to prevent mustache recursion loops. }; if (cminfo.hasdelegatedsection) { const sectioninfo = state.section.get(cminfo.delegatesectionid); diff --git a/course/format/amd/src/local/courseeditor/fileuploader.js b/course/format/amd/src/local/courseeditor/fileuploader.js index 9103a44bf3265..3b67d35005d5f 100644 --- a/course/format/amd/src/local/courseeditor/fileuploader.js +++ b/course/format/amd/src/local/courseeditor/fileuploader.js @@ -492,7 +492,7 @@ async function loadErrorStrings(courseId) { {key: 'dndupload', component: 'core_error'}, {key: 'dndunkownfile', component: 'core_error'}, ]; - window.console.log(allStrings); + const loadedStrings = await getStrings(allStrings); allStrings.forEach(({key}, index) => { errors[key] = loadedStrings[index]; diff --git a/course/format/classes/base.php b/course/format/classes/base.php index b0f8bc35ce9a8..e6e90134966ee 100644 --- a/course/format/classes/base.php +++ b/course/format/classes/base.php @@ -2121,4 +2121,17 @@ public function get_required_jsfiles(): array { public function can_sections_be_removed_from_navigation(): bool { return false; } + + /** + * Determines whether the course module should display the activity editor options. + * + * @param cm_info $cm The activity module. + * @return bool True if the activity editor options are displayed, false otherwise. + */ + public function show_activity_editor_options(cm_info $cm): bool { + if ($cm->get_delegated_section_info() && component_callback_exists('mod_' . $cm->modname, 'cm_info_view')) { + return false; + } + return true; + } } diff --git a/course/format/classes/output/local/content/addsection.php b/course/format/classes/output/local/content/addsection.php index b78cdf76ac1fa..342a3ca302b4c 100644 --- a/course/format/classes/output/local/content/addsection.php +++ b/course/format/classes/output/local/content/addsection.php @@ -153,11 +153,11 @@ protected function get_add_section_data(\renderer_base $output, int $lastsection if ($singlesection) { $params['sectionreturn'] = $singlesection; } - $data->addsections = (object) [ 'url' => new moodle_url('/course/changenumsections.php', $params), 'title' => $addstring, 'newsection' => $maxsections - $lastsection, + 'canaddsection' => $lastsection < $maxsections, ]; return $data; } diff --git a/course/format/classes/output/local/content/basecontrolmenu.php b/course/format/classes/output/local/content/basecontrolmenu.php new file mode 100644 index 0000000000000..062edd12be02a --- /dev/null +++ b/course/format/classes/output/local/content/basecontrolmenu.php @@ -0,0 +1,163 @@ +. + +namespace core_courseformat\output\local\content; + +use action_menu; +use action_menu_link_secondary; +use core\output\named_templatable; +use core_courseformat\base as course_format; +use core_courseformat\output\local\courseformat_named_templatable; +use moodle_url; +use pix_icon; +use renderable; +use section_info; +use cm_info; +use stdClass; + +/** + * Base class to render course element controls. + * + * @package core_courseformat + * @copyright 2024 Amaia Anabitarte + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class basecontrolmenu implements named_templatable, renderable { + + use courseformat_named_templatable; + + /** @var course_format the course format class */ + protected $format; + + /** @var section_info the course section class */ + protected $section; + + /** @var cm_info the course module class */ + protected $mod; + + /** @var string the menu ID */ + protected $menuid; + + /** @var action_menu the action menu */ + protected $menu; + + /** + * Constructor. + * + * @param course_format $format the course format + * @param section_info $section the section info + * @param cm_info|null $mod the module info + * @param string $menuid the ID value for the menu + */ + public function __construct(course_format $format, section_info $section, ?cm_info $mod = null, string $menuid = '') { + $this->format = $format; + $this->section = $section; + $this->mod = $mod; + $this->menuid = $menuid; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param \renderer_base $output typically, the renderer that's calling this function + * @return null|array data context for a mustache template + */ + public function export_for_template(\renderer_base $output): ?stdClass { + $menu = $this->get_action_menu($output); + if (empty($menu)) { + return new stdClass(); + } + + $data = (object)[ + 'menu' => $output->render($menu), + 'hasmenu' => true, + 'id' => $this->menuid, + ]; + + return $data; + } + + /** + * Generate the action menu element. + * + * @param \renderer_base $output typically, the renderer that's calling this function + * @return action_menu|null the action menu or null if no action menu is available + */ + public function get_action_menu(\renderer_base $output): ?action_menu { + + if (!empty($this->menu)) { + return $this->menu; + } + + $this->menu = $this->get_default_action_menu($output); + return $this->menu; + } + + /** + * Generate the default action menu. + * + * This method is public in case some block needs to modify the menu before output it. + * + * @param \renderer_base $output typically, the renderer that's calling this function + * @return action_menu|null the action menu + */ + public function get_default_action_menu(\renderer_base $output): ?action_menu { + return null; + } + + /** + * Format control array into an action_menu. + * + * @param \renderer_base $output typically, the renderer that's calling this function + * @return action_menu|null the action menu + */ + protected function format_controls(array $controls): ?action_menu { + if (empty($controls)) { + return null; + } + + $menu = new action_menu(); + $menu->set_kebab_trigger(get_string('edit')); + $menu->attributes['class'] .= ' section-actions'; + $menu->attributes['data-sectionid'] = $this->section->id; + foreach ($controls as $value) { + $url = empty($value['url']) ? '' : $value['url']; + $icon = empty($value['icon']) ? '' : $value['icon']; + $name = empty($value['name']) ? '' : $value['name']; + $attr = empty($value['attr']) ? [] : $value['attr']; + $class = empty($value['pixattr']['class']) ? '' : $value['pixattr']['class']; + $al = new action_menu_link_secondary( + new moodle_url($url), + new pix_icon($icon, '', null, ['class' => "smallicon " . $class]), + $name, + $attr + ); + $menu->add($al); + } + return $menu; + } + + /** + * Generate the edit control items of a section. + * + * This method must remain public until the final deprecation of section_edit_control_items. + * + * @return array of edit control items + */ + public function section_control_items() { + return []; + } +} diff --git a/course/format/classes/output/local/content/cm.php b/course/format/classes/output/local/content/cm.php index 76173d173b891..a803423553709 100644 --- a/course/format/classes/output/local/content/cm.php +++ b/course/format/classes/output/local/content/cm.php @@ -123,6 +123,7 @@ public function export_for_template(renderer_base $output): stdClass { 'cmid' => $mod->id, 'editing' => $PAGE->user_is_editing(), 'sectionnum' => $this->section->section, + 'cmbulk' => !$mod->get_delegated_section_info(), ]; // Add partial data segments. @@ -185,7 +186,7 @@ protected function add_availability_data(stdClass &$data, renderer_base $output) $this->displayoptions ); $modavailability = $availability->export_for_template($output); - $data->modavailability = $modavailability; + $data->modavailability = $modavailability ?? false; return $availability->has_availability($output); } @@ -319,7 +320,8 @@ protected function add_editor_data(stdClass &$data, renderer_base $output): bool $this->mod, $this->displayoptions ); - $data->controlmenu = $controlmenu->export_for_template($output); + $data->controlmenu = $controlmenu->export_for_template($output) ?? false; + if (!$this->format->supports_components()) { // Add the legacy YUI move link. $data->moveicon = course_get_cm_move($this->mod, $returnsection); @@ -350,8 +352,8 @@ protected function add_groupmode_data(stdClass &$data, renderer_base $output): b protected function add_visibility_data(stdClass &$data, renderer_base $output): bool { $visibility = new $this->visibilityclass($this->format, $this->section, $this->mod); $templatedata = $visibility->export_for_template($output); + $data->visibility = $templatedata ?? false; if ($templatedata) { - $data->visibility = $templatedata; return true; } return false; diff --git a/course/format/classes/output/local/content/cm/availability.php b/course/format/classes/output/local/content/cm/availability.php index 1b409d14d9368..02ce915cdbd18 100644 --- a/course/format/classes/output/local/content/cm/availability.php +++ b/course/format/classes/output/local/content/cm/availability.php @@ -75,6 +75,19 @@ public function __construct(course_format $format, section_info $section, cm_inf $this->hasavailabilityname = 'hasmodavailability'; } + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param \renderer_base $output typically, the renderer that's calling this function + * @return stdClass|null data context for a mustache template + */ + public function export_for_template(\renderer_base $output): ?stdClass { + if (!$this->format->show_activity_editor_options($this->mod)) { + return null; + } + return parent::export_for_template($output); + } + /** * Get the availability data to be used as the context for a mustache template. * diff --git a/course/format/classes/output/local/content/cm/completion.php b/course/format/classes/output/local/content/cm/completion.php index 9c69dde409121..fa462ca71fdd9 100644 --- a/course/format/classes/output/local/content/cm/completion.php +++ b/course/format/classes/output/local/content/cm/completion.php @@ -56,11 +56,15 @@ public function __construct( * Export this data so it can be used as the context for a mustache template. * * @param \renderer_base $output typically, the renderer that's calling this function - * @return stdClass data context for a mustache template + * @return stdClass|null data context for a mustache template */ public function export_for_template(\renderer_base $output): ?stdClass { global $USER; + if (!$this->format->show_activity_editor_options($this->mod)) { + return null; + } + $course = $this->mod->get_course(); $showcompletionconditions = $course->showcompletionconditions == COMPLETION_SHOW_CONDITIONS; diff --git a/course/format/classes/output/local/content/cm/controlmenu.php b/course/format/classes/output/local/content/cm/controlmenu.php index f71a793837176..7e4a8e47f84e9 100644 --- a/course/format/classes/output/local/content/cm/controlmenu.php +++ b/course/format/classes/output/local/content/cm/controlmenu.php @@ -27,10 +27,9 @@ use action_menu; use action_menu_link; use cm_info; -use core\output\named_templatable; use core_courseformat\base as course_format; +use core_courseformat\output\local\content\basecontrolmenu; use core_courseformat\output\local\courseformat_named_templatable; -use renderable; use section_info; use stdClass; @@ -41,21 +40,7 @@ * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class controlmenu implements named_templatable, renderable { - - use courseformat_named_templatable; - - /** @var course_format the course format */ - protected $format; - - /** @var section_info the section object */ - private $section; - - /** @var action_menu the activity aciton menu */ - protected $menu; - - /** @var cm_info the course module instance */ - protected $mod; +class controlmenu extends basecontrolmenu { /** @var array optional display options */ protected $displayoptions; @@ -69,9 +54,7 @@ class controlmenu implements named_templatable, renderable { * @param array $displayoptions optional extra display options */ public function __construct(course_format $format, section_info $section, cm_info $mod, array $displayoptions = []) { - $this->format = $format; - $this->section = $section; - $this->mod = $mod; + parent::__construct($format, $section, $mod, $mod->id); $this->displayoptions = $displayoptions; } @@ -79,12 +62,16 @@ public function __construct(course_format $format, section_info $section, cm_inf * Export this data so it can be used as the context for a mustache template. * * @param \renderer_base $output typically, the renderer that's calling this function - * @return stdClass data context for a mustache template + * @return stdClass|null data context for a mustache template */ - public function export_for_template(\renderer_base $output): stdClass { + public function export_for_template(\renderer_base $output): ?stdClass { $mod = $this->mod; + if (!$this->format->show_activity_editor_options($this->mod)) { + return null; + } + $menu = $this->get_action_menu($output); if (empty($menu)) { @@ -94,7 +81,7 @@ public function export_for_template(\renderer_base $output): stdClass { $data = (object)[ 'menu' => $menu->export_for_template($output), 'hasmenu' => true, - 'id' => $mod->id, + 'id' => $this->menuid, ]; // After icons. @@ -120,6 +107,14 @@ public function get_action_menu(\renderer_base $output): ?action_menu { $mod = $this->mod; + // In case module is delegating a section, we should return delegated section action menu. + if ($delegated = $mod->get_delegated_section_info()) { + $controlmenuclass = $this->format->get_output_classname('content\\cm\\delegatedcontrolmenu'); + $controlmenu = new $controlmenuclass($this->format, $delegated, $mod); + + return $controlmenu->get_action_menu($output); + } + $controls = $this->cm_control_items(); if (empty($controls)) { diff --git a/course/format/classes/output/local/content/cm/delegatedcontrolmenu.php b/course/format/classes/output/local/content/cm/delegatedcontrolmenu.php new file mode 100644 index 0000000000000..4a0a0e32ec584 --- /dev/null +++ b/course/format/classes/output/local/content/cm/delegatedcontrolmenu.php @@ -0,0 +1,170 @@ +. + +namespace core_courseformat\output\local\content\cm; + +use action_menu; +use context_course; +use core_courseformat\base as course_format; +use core_courseformat\output\local\content\basecontrolmenu; +use moodle_url; +use section_info; +use cm_info; + +/** + * Base class to render delegated section controls. + * + * @package core_courseformat + * @copyright 2024 Amaia Anabitarte + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class delegatedcontrolmenu extends basecontrolmenu { + + /** + * Constructor. + * + * @param course_format $format the course format + * @param section_info $section the section info + * @param cm_info $mod the module info + */ + public function __construct(course_format $format, section_info $section, cm_info $mod) { + parent::__construct($format, $section, $mod, $section->id); + } + + /** + * Generate the default delegated section action menu. + * + * This method is public in case some block needs to modify the menu before output it. + * + * @param \renderer_base $output typically, the renderer that's calling this function + * @return action_menu|null the action menu + */ + public function get_default_action_menu(\renderer_base $output): ?action_menu { + $controls = $this->delegated_control_items(); + return $this->format_controls($controls); + } + + /** + * Generate the edit control items of a section. + * + * It is not clear this kind of controls are still available in 4.0 so, for now, this + * method is almost a clone of the previous section_control_items from the course/renderer.php. + * + * This method must remain public until the final deprecation of section_edit_control_items. + * + * @return array of edit control items + */ + public function delegated_control_items() { + global $USER; + + $format = $this->format; + $section = $this->section; + $cm = $this->mod; + $course = $format->get_course(); + $sectionreturn = !is_null($format->get_sectionid()) ? $format->get_sectionnum() : null; + $user = $USER; + + $usecomponents = $format->supports_components(); + $coursecontext = context_course::instance($course->id); + + $baseurl = course_get_url($course, $sectionreturn); + $baseurl->param('sesskey', sesskey()); + + $cmbaseurl = new moodle_url('/course/mod.php'); + $cmbaseurl->param('sesskey', sesskey()); + + $hasmanageactivities = has_capability('moodle/course:manageactivities', $coursecontext); + $isheadersection = $format->get_sectionid() == $section->id; + + $controls = []; + + // Only show the view link if we are not already in the section view page. + if (!$isheadersection) { + $controls['view'] = [ + 'url' => new moodle_url('/course/section.php', ['id' => $section->id]), + 'icon' => 'i/viewsection', + 'name' => get_string('view'), + 'pixattr' => ['class' => ''], + 'attr' => ['class' => 'view'], + ]; + } + + if (has_capability('moodle/course:update', $coursecontext, $user)) { + $params = ['id' => $section->id]; + $params['sr'] = $section->section; + if (get_string_manager()->string_exists('editsection', 'format_'.$format->get_format())) { + $streditsection = get_string('editsection', 'format_'.$format->get_format()); + } else { + $streditsection = get_string('editsection'); + } + + // Edit settings goes to section settings form. + $controls['edit'] = [ + 'url' => new moodle_url('/course/editsection.php', $params), + 'icon' => 'i/settings', + 'name' => $streditsection, + 'pixattr' => ['class' => ''], + 'attr' => ['class' => 'edit'], + ]; + } + + // Delete deletes the module. + // Only show the view link if we are not already in the section view page. + if (!$isheadersection && $hasmanageactivities) { + $url = clone($cmbaseurl); + $url->param('delete', $cm->id); + $url->param('sr', $cm->sectionnum); + + $controls['delete'] = [ + 'url' => $url, + 'icon' => 't/delete', + 'name' => get_string('delete'), + 'pixattr' => ['class' => ''], + 'attr' => [ + 'class' => 'editing_delete text-danger', + 'data-action' => ($usecomponents) ? 'cmDelete' : 'delete', + 'data-sectionreturn' => $sectionreturn, + 'data-id' => $cm->id, + ], + ]; + } + + // Add section page permalink. + if ( + has_any_capability([ + 'moodle/course:movesections', + 'moodle/course:update', + 'moodle/course:sectionvisibility', + ], $coursecontext) + ) { + $sectionlink = new moodle_url( + '/course/section.php', + ['id' => $section->id] + ); + $controls['permalink'] = [ + 'url' => $sectionlink, + 'icon' => 'i/link', + 'name' => get_string('sectionlink', 'course'), + 'pixattr' => ['class' => ''], + 'attr' => [ + 'data-action' => 'permalink', + ], + ]; + } + + return $controls; + } +} diff --git a/course/format/classes/output/local/content/cm/visibility.php b/course/format/classes/output/local/content/cm/visibility.php index 8a6cbc330f6af..2af03a27bd43c 100644 --- a/course/format/classes/output/local/content/cm/visibility.php +++ b/course/format/classes/output/local/content/cm/visibility.php @@ -78,6 +78,9 @@ public function export_for_template(\renderer_base $output): ?stdClass { if (!$this->show_visibility()) { return null; } + if (!$this->format->show_activity_editor_options($this->mod)) { + return null; + } $format = $this->format; // In rare legacy cases, the section could be stealth (orphaned) but they are not editable. if (!$format->show_editor() diff --git a/course/format/classes/output/local/content/delegatedsection.php b/course/format/classes/output/local/content/delegatedsection.php new file mode 100644 index 0000000000000..027104a78ac27 --- /dev/null +++ b/course/format/classes/output/local/content/delegatedsection.php @@ -0,0 +1,35 @@ +. + +/** + * Contains the default delegated section course format output class. + * + * @package core_courseformat + * @copyright 2024 Mikel Martín + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_courseformat\output\local\content; + +/** + * Base class to render a delegated section. + * + * @package core_courseformat + * @copyright 2024 Mikel Martín + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class delegatedsection extends section { +} diff --git a/course/format/classes/output/local/content/section.php b/course/format/classes/output/local/content/section.php index 6e5136a83ad05..2c0f615968c31 100644 --- a/course/format/classes/output/local/content/section.php +++ b/course/format/classes/output/local/content/section.php @@ -152,7 +152,7 @@ public function export_for_template(renderer_base $output): stdClass { 'highlightedlabel' => $format->get_section_highlighted_name(), 'sitehome' => $course->id == SITEID, 'editing' => $PAGE->user_is_editing(), - 'displayonesection' => ($course->id != SITEID && !is_null($format->get_sectionid())), + 'displayonesection' => ($course->id != SITEID && $format->get_sectionid() == $section->id), ]; $haspartials = []; diff --git a/course/format/classes/output/local/content/section/availability.php b/course/format/classes/output/local/content/section/availability.php index eb64d15966232..91ebe64c5c3e1 100644 --- a/course/format/classes/output/local/content/section/availability.php +++ b/course/format/classes/output/local/content/section/availability.php @@ -77,9 +77,9 @@ public function __construct(course_format $format, section_info $section) { * Export this data so it can be used as the context for a mustache template. * * @param \renderer_base $output typically, the renderer that's calling this function - * @return stdClass data context for a mustache template + * @return stdClass|null data context for a mustache template */ - public function export_for_template(\renderer_base $output): stdClass { + public function export_for_template(\renderer_base $output): ?stdClass { $this->build_export_data($output); return $this->data; } diff --git a/course/format/classes/output/local/content/section/controlmenu.php b/course/format/classes/output/local/content/section/controlmenu.php index ab545516ecbc4..7b18968d5fad0 100644 --- a/course/format/classes/output/local/content/section/controlmenu.php +++ b/course/format/classes/output/local/content/section/controlmenu.php @@ -14,27 +14,14 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Contains the default section controls output class. - * - * @package core_courseformat - * @copyright 2020 Ferran Recio - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace core_courseformat\output\local\content\section; use action_menu; -use action_menu_link_secondary; use context_course; -use core\output\named_templatable; use core_courseformat\base as course_format; -use core_courseformat\output\local\courseformat_named_templatable; +use core_courseformat\output\local\content\basecontrolmenu; use moodle_url; -use pix_icon; -use renderable; use section_info; -use stdClass; /** * Base class to render section controls. @@ -43,15 +30,7 @@ * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class controlmenu implements named_templatable, renderable { - - use courseformat_named_templatable; - - /** @var course_format the course format class */ - protected $format; - - /** @var section_info the course section class */ - protected $section; +class controlmenu extends basecontrolmenu { /** * Constructor. @@ -60,40 +39,23 @@ class controlmenu implements named_templatable, renderable { * @param section_info $section the section info */ public function __construct(course_format $format, section_info $section) { - $this->format = $format; - $this->section = $section; - } - - /** - * Export this data so it can be used as the context for a mustache template. - * - * @param \renderer_base $output typically, the renderer that's calling this function - * @return array data context for a mustache template - */ - public function export_for_template(\renderer_base $output): stdClass { - $menu = $this->get_action_menu($output); - if (empty($menu)) { - return new stdClass(); - } - - $data = (object)[ - 'menu' => $output->render($menu), - 'hasmenu' => true, - 'id' => $this->section->id, - ]; - - return $data; + parent::__construct($format, $section, null, $section->id); } /** * Generate the action menu element depending on the section. * - * Sections controlled by a plugin will delegate the control menu to the plugin. + * Sections controlled by a plugin will delegate the control menu to the delegated section class. * * @param \renderer_base $output typically, the renderer that's calling this function - * @return action_menu|null the activity action menu or null if no action menu is available + * @return action_menu|null the section action menu or null if no action menu is available */ public function get_action_menu(\renderer_base $output): ?action_menu { + + if (!empty($this->menu)) { + return $this->menu; + } + $sectiondelegate = $this->section->get_component_instance(); if ($sectiondelegate) { return $sectiondelegate->get_section_action_menu($this->format, $this, $output); @@ -107,34 +69,11 @@ public function get_action_menu(\renderer_base $output): ?action_menu { * This method is public in case some block needs to modify the menu before output it. * * @param \renderer_base $output typically, the renderer that's calling this function - * @return action_menu|null the activity action menu + * @return action_menu|null the section action menu */ public function get_default_action_menu(\renderer_base $output): ?action_menu { $controls = $this->section_control_items(); - if (empty($controls)) { - return null; - } - - // Convert control array into an action_menu. - $menu = new action_menu(); - $menu->set_kebab_trigger(get_string('edit')); - $menu->attributes['class'] .= ' section-actions'; - $menu->attributes['data-sectionid'] = $this->section->id; - foreach ($controls as $value) { - $url = empty($value['url']) ? '' : $value['url']; - $icon = empty($value['icon']) ? '' : $value['icon']; - $name = empty($value['name']) ? '' : $value['name']; - $attr = empty($value['attr']) ? [] : $value['attr']; - $class = empty($value['pixattr']['class']) ? '' : $value['pixattr']['class']; - $al = new action_menu_link_secondary( - new moodle_url($url), - new pix_icon($icon, '', null, ['class' => "smallicon " . $class]), - $name, - $attr - ); - $menu->add($al); - } - return $menu; + return $this->format_controls($controls); } /** @@ -222,9 +161,9 @@ public function section_control_items() { $strshowfromothers = get_string('showfromothers', 'format_' . $course->format); if ($section->visible) { // Show the hide/show eye. $url->param('hide', $section->section); - $controls['visiblity'] = [ + $controls['visibility'] = [ 'url' => $url, - 'icon' => 'i/hide', + 'icon' => 'i/show', 'name' => $strhidefromothers, 'pixattr' => ['class' => ''], 'attr' => [ @@ -232,15 +171,16 @@ public function section_control_items() { 'data-sectionreturn' => $sectionreturn, 'data-action' => ($usecomponents) ? 'sectionHide' : 'hide', 'data-id' => $section->id, + 'data-icon' => 'i/show', 'data-swapname' => $strshowfromothers, - 'data-swapicon' => 'i/show', + 'data-swapicon' => 'i/hide', ], ]; } else { $url->param('show', $section->section); - $controls['visiblity'] = [ + $controls['visibility'] = [ 'url' => $url, - 'icon' => 'i/show', + 'icon' => 'i/hide', 'name' => $strshowfromothers, 'pixattr' => ['class' => ''], 'attr' => [ @@ -248,8 +188,9 @@ public function section_control_items() { 'data-sectionreturn' => $sectionreturn, 'data-action' => ($usecomponents) ? 'sectionShow' : 'show', 'data-id' => $section->id, + 'data-icon' => 'i/hide', 'data-swapname' => $strhidefromothers, - 'data-swapicon' => 'i/hide', + 'data-swapicon' => 'i/show', ], ]; } diff --git a/course/format/classes/output/local/content/section/header.php b/course/format/classes/output/local/content/section/header.php index 7c938342936f2..d325efe9a278f 100644 --- a/course/format/classes/output/local/content/section/header.php +++ b/course/format/classes/output/local/content/section/header.php @@ -82,7 +82,7 @@ public function export_for_template(\renderer_base $output): stdClass { $data->title = $output->section_title_without_link($section, $course); $data->sitehome = true; } else { - if (is_null($format->get_sectionid())) { + if (is_null($format->get_sectionid()) || $format->get_sectionid() != $section->id) { // All sections are displayed. if (!$data->editing) { $data->title = $output->section_title($section, $course); @@ -114,10 +114,13 @@ public function export_for_template(\renderer_base $output): stdClass { $data->name = get_section_name($course, $section); $data->selecttext = $format->get_format_string('selectsection', $data->name); - if (!$format->get_sectionnum()) { + if (!$format->get_sectionnum() && !$section->is_delegated()) { $data->sectionbulk = true; } + // Delegated sections in main course page need to have h4 tag, h3 otherwise. + $data->headinglevel = ($section->is_delegated() && is_null($format->get_sectionid())) ? 4 : 3; + return $data; } } diff --git a/course/format/classes/output/local/content/sectionselector.php b/course/format/classes/output/local/content/sectionselector.php index 5694dc5e49c79..a78ff80baa539 100644 --- a/course/format/classes/output/local/content/sectionselector.php +++ b/course/format/classes/output/local/content/sectionselector.php @@ -85,7 +85,7 @@ public function export_for_template(\renderer_base $output): stdClass { $numsections = $format->get_last_section_number(); while ($section <= $numsections) { $thissection = $modinfo->get_section_info($section); - $url = course_get_url($course, $section); + $url = course_get_url($course, $section, ['navigation' => true]); if ($thissection->uservisible && $url && $section != $data->currentsection) { $sectionmenu[$url->out(false)] = get_section_name($course, $section); } diff --git a/course/format/classes/sectiondelegatemodule.php b/course/format/classes/sectiondelegatemodule.php index 61e5a2c736398..c91ba739812c0 100644 --- a/course/format/classes/sectiondelegatemodule.php +++ b/course/format/classes/sectiondelegatemodule.php @@ -16,7 +16,13 @@ namespace core_courseformat; +use action_menu; use cm_info; +use core_courseformat\base as course_format; +use core_courseformat\formatactions; +use core_courseformat\output\local\content\section\controlmenu; +use core_courseformat\stateupdates; +use renderer_base; use section_info; use stdClass; @@ -110,4 +116,56 @@ public function get_course(): stdClass { private function get_module_name(): string { return \core_component::normalize_component($this->sectioninfo->component)[1]; } + + /** + * Sync the section renaming with the activity name. + * + * @param section_info $section + * @param string|null $newname + * @return string|null + */ + public function preprocess_section_name(section_info $section, ?string $newname): ?string { + $cm = get_coursemodule_from_instance($this->get_module_name(), $section->itemid); + if (!$cm) { + return $newname; + } + if (empty($newname) || $newname === $cm->name) { + return $cm->name; + } + formatactions::cm($section->course)->rename($cm->id, $newname); + return $newname; + } + + /** + * Allow delegate plugin to modify the available section menu. + * + * @param course_format $format The course format instance. + * @param controlmenu $controlmenu The control menu instance. + * @param renderer_base $output The renderer instance. + * @return action_menu|null The new action menu with the list of edit control items or null if no action menu is available. + */ + public function get_section_action_menu( + course_format $format, + controlmenu $controlmenu, + renderer_base $output, + ): ?action_menu { + $controlmenuclass = $format->get_output_classname('content\\cm\\controlmenu'); + $controlmenu = new $controlmenuclass( + $format, + $this->sectioninfo, + $this->cm, + ); + return $controlmenu->get_action_menu($output); + } + + /** + * Add extra state updates when put or create a section. + * + * @param section_info $section the affected section. + * @param stateupdates $updates the state updates object to notify the UI. + */ + public function put_section_state_extra_updates(section_info $section, stateupdates $updates): void { + $cm = get_coursemodule_from_instance($this->get_module_name(), $section->itemid); + $updates->add_cm_put($cm->id); + } } diff --git a/course/format/templates/local/content/addsection.mustache b/course/format/templates/local/content/addsection.mustache index ffd8064037d49..09c967979a930 100644 --- a/course/format/templates/local/content/addsection.mustache +++ b/course/format/templates/local/content/addsection.mustache @@ -51,18 +51,24 @@
    {{/decrease}} {{#addsections}} - - - {{#pix}} t/add, core {{/pix}} - {{title}} - - + + {{#pix}} t/add, core {{/pix}} + {{title}} + +
    +
    + {{#pix}}t/block, moodle{{/pix}} +
    +
    + {{#str}}maxsectionaddmessage, core_courseformat{{/str}} +
    +
    {{/addsections}}
    {{/showaddsection}} diff --git a/course/format/templates/local/content/cm.mustache b/course/format/templates/local/content/cm.mustache index 8e2102b6ac637..c07b78cd21298 100644 --- a/course/format/templates/local/content/cm.mustache +++ b/course/format/templates/local/content/cm.mustache @@ -65,9 +65,11 @@
    - {{$ core_courseformat/local/content/cm/bulkselect }} - {{> core_courseformat/local/content/cm/bulkselect }} - {{/ core_courseformat/local/content/cm/bulkselect }} + {{#cmbulk}} + {{$ core_courseformat/local/content/cm/bulkselect }} + {{> core_courseformat/local/content/cm/bulkselect }} + {{/ core_courseformat/local/content/cm/bulkselect }} + {{/cmbulk}} {{! Place the actual content of the activity-item in a separate template to make it easier for other formats to add additional content to the activity wrapper. diff --git a/course/format/templates/local/content/cm/activity.mustache b/course/format/templates/local/content/cm/activity.mustache index 8bd20c4b92d88..8ca5b9397064a 100644 --- a/course/format/templates/local/content/cm/activity.mustache +++ b/course/format/templates/local/content/cm/activity.mustache @@ -139,7 +139,7 @@ {{! Description }} {{#altcontent}} -
    +
    {{{altcontent}}}
    {{/altcontent}} diff --git a/course/format/templates/local/content/delegatedsection.mustache b/course/format/templates/local/content/delegatedsection.mustache new file mode 100644 index 0000000000000..3cf4b264a4a04 --- /dev/null +++ b/course/format/templates/local/content/delegatedsection.mustache @@ -0,0 +1,93 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_courseformat/local/content/delegatedsection + + Displays a course section. + + Note: This template is a wrapper around the section/content template to allow course formats and theme designers to + modify parts of the wrapper without having to copy/paste the entire template. + + Example context (json): + { + "num": 3, + "id": 35, + "controlmenu": "[tools menu]", + "header": { + "name": "Section title", + "title": "Section title", + "url": "#", + "ishidden": true, + "headinglevel": 3 + }, + "cmlist": { + "cms": [ + { + "cmitem": { + "cmformat": { + "cmname": "Assign example", + "hasname": "true" + }, + "id": 4, + "anchor": "activity-4", + "module": "assign", + "extraclasses": "" + } + } + ], + "hascms": true + }, + "ishidden": false, + "iscurrent": true, + "currentlink": "This topic", + "availability": { + "info": "Hidden from students", + "hasavailability": true + }, + "summary": { + "summarytext": "Summary text!" + }, + "controlmenu": { + "menu": "Edit", + "hasmenu": true + }, + "cmcontrols": "[Add an activity or resource]", + "iscoursedisplaymultipage": true, + "sectionreturnnum": 0, + "contentcollapsed": false, + "sitehome": false, + "highlightedlabel" : "Highlighted" + } +}} +
      + +
    diff --git a/course/format/templates/local/content/movecm.mustache b/course/format/templates/local/content/movecm.mustache index 44ab885155884..36ba47f332081 100644 --- a/course/format/templates/local/content/movecm.mustache +++ b/course/format/templates/local/content/movecm.mustache @@ -34,17 +34,34 @@ { "name": "Glossary of characters", "id": "10", - "url": "#" + "url": "#", + "sectioninfo": false }, { "name": "World Cinema forum", "id": "11", - "url": "#" + "url": "#", + "sectioninfo": { + "title": "World Cinema forum", + "id": "11", + "number": "5", + "sectionurl": "#", + "hascms": true, + "cms": [ + { + "name": "Announcements", + "id": "12", + "url": "#", + "sectioninfo": false + } + ] + } }, { "name": "Announcements", "id": "12", - "url": "#" + "url": "#", + "sectioninfo": false } ] }, @@ -58,17 +75,20 @@ { "name": "Resources", "id": "13", - "url": "#" + "url": "#", + "sectioninfo": false }, { "name": "Studying City of God by Stephen Smith Bergman-Messerschmidt", "id": "14", - "url": "#" + "url": "#", + "sectioninfo": false }, { "name": "Film education study guide", "id": "15", - "url": "#" + "url": "#", + "sectioninfo": false } ] } @@ -77,61 +97,10 @@ }}

    {{information}}:

    -