From a15c17cc0e398a470d051bc428352022b4c622c8 Mon Sep 17 00:00:00 2001 From: Hieu Nguyen Dang Date: Fri, 10 May 2024 12:27:27 +0700 Subject: [PATCH 001/178] MDL-80489 qtype_ddtos: Help Text for Choices section --- question/type/ddwtos/edit_ddwtos_form.php | 6 ++++++ question/type/ddwtos/lang/en/qtype_ddwtos.php | 2 ++ question/type/ddwtos/tests/behat/edit.feature | 1 + 3 files changed, 9 insertions(+) diff --git a/question/type/ddwtos/edit_ddwtos_form.php b/question/type/ddwtos/edit_ddwtos_form.php index f61c2a42ef4c6..357e8c0a977ec 100644 --- a/question/type/ddwtos/edit_ddwtos_form.php +++ b/question/type/ddwtos/edit_ddwtos_form.php @@ -66,4 +66,10 @@ protected function extra_slot_validation(array $slots, array $choices): ?string } return null; } + + protected function definition_inner($mform): void { + parent::definition_inner($mform); + $mform->insertElementBefore($mform->createElement('static', 'previewarea', '', + get_string('choicesacceptedtext', 'qtype_ddwtos')), 'shuffleanswers'); + } } diff --git a/question/type/ddwtos/lang/en/qtype_ddwtos.php b/question/type/ddwtos/lang/en/qtype_ddwtos.php index ed3d40e14b9a7..6ab1e157265f0 100644 --- a/question/type/ddwtos/lang/en/qtype_ddwtos.php +++ b/question/type/ddwtos/lang/en/qtype_ddwtos.php @@ -27,6 +27,8 @@ $string['blank'] = 'blank'; $string['blanknumber'] = 'Blank {$a}'; $string['correctansweris'] = 'The correct answer is: {$a}'; +$string['choicesacceptedtext'] = 'Write the answer to be dragged into each gap. You can include extra answers to increase difficulty.
+Accepted text formatting: <sub>, <sup>, <b>, <i>, <em>, <strong>. TeX is also accepted, using $$ at the start and at the end.'; $string['errorlimitedchoice'] = 'Choice [[{$a}]] has been used more than once without being set to "Unlimited". Please recheck this question.'; $string['infinite'] = 'Unlimited'; $string['pleaseputananswerineachbox'] = 'Please put an answer in each box.'; diff --git a/question/type/ddwtos/tests/behat/edit.feature b/question/type/ddwtos/tests/behat/edit.feature index fd531d49d93e2..4e9b9f0da7e20 100644 --- a/question/type/ddwtos/tests/behat/edit.feature +++ b/question/type/ddwtos/tests/behat/edit.feature @@ -27,6 +27,7 @@ Feature: Test editing a drag and drop into text questions And I should see "Choice [[1]]" And I should see "Choice [[2]]" And I should see "Choice [[3]]" + And I should see "Write the answer to be dragged into each gap. You can include extra answers to increase difficulty." in the "Choices" "fieldset" And I set the following fields to these values: | Question name | Edited question name | And I press "id_submitbutton" From 55571fb06cbb25fdbd6b16ae33204ac06c04781d Mon Sep 17 00:00:00 2001 From: Eric Merrill Date: Thu, 13 Jun 2024 18:35:07 -0400 Subject: [PATCH 002/178] MDL-82192 scorm: Correct field names for AICC ExitAu --- mod/scorm/aicc.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mod/scorm/aicc.php b/mod/scorm/aicc.php index d101a8eca8df3..028bf0b6d1763 100644 --- a/mod/scorm/aicc.php +++ b/mod/scorm/aicc.php @@ -413,10 +413,10 @@ } else { $track = new stdClass(); $track->scoid = $sco->id; - $track->element = scorm_get_elementid('cmi.core.total_time'); + $track->elementid = scorm_get_elementid('cmi.core.total_time'); $track->value = $scormsession->sessiontime; $atobject = scorm_get_attempt($aiccuser->id, $scormsession->scormid, $attempt); - $track->attempt = $atobject->id; + $track->attemptid = $atobject->id; $track->timemodified = time(); $id = $DB->insert_record('scorm_scoes_value', $track); } From 4fbf34b73d3fbdd0af6a4ffb7e96ca7ec992166d Mon Sep 17 00:00:00 2001 From: Meirza Date: Tue, 9 Jan 2024 11:34:55 +0100 Subject: [PATCH 003/178] MDL-75864 cache: Change key prefix in cache_cron_task Currently, the cache_cron_task is set to look for 'sess_' as a key prefix, which is not used in any code. As a result, sessions that have exceeded the timeout are never deleted. The proposed patch involves changing the prefix to the LASTACCESS key prefix and fixing the return value of get_many in the Redis cache so that it can return the required keys. Co-authored-by: Renaud Lemaire --- cache/classes/helper.php | 25 +++++++++++++++++-------- cache/stores/redis/lib.php | 2 +- 2 files changed, 18 insertions(+), 9 deletions(-) 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; From 27a6dbeeb6e4cc027596887c0176487deaeaf1ab Mon Sep 17 00:00:00 2001 From: Michael Milette Date: Mon, 27 Jan 2020 23:42:35 -0500 Subject: [PATCH 004/178] MDL-67554 oauth2: New filtered language tags for additional parameters. This adds support for new language tags in OAuth2's "Additional Parameters Included in a Login Request" field. Available tags include: Tags Example value ----------------------------- {lang} fr {LANG} FR {language} fr_ca {LANGUAGE} FR_CA {lan-guage} fr-ca {LAN-GUAGE} FR-CA --- lib/classes/oauth2/client.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/classes/oauth2/client.php b/lib/classes/oauth2/client.php index 905a751ce4161..6369636355f55 100644 --- a/lib/classes/oauth2/client.php +++ b/lib/classes/oauth2/client.php @@ -126,6 +126,20 @@ public function get_additional_login_parameters() { return []; } $result = []; + + // Replace the language tag if it appears in the string. + $lang = current_language(); + $tags = ["{lang}", "{LANG}", "{language}", "{LANGUAGE}", '{lan-guage}', '{LAN-GUAGE}']; + $langcode = [ + strtolower(substr($lang, 0, 2)), + strtoupper(substr($lang, 0, 2)), + strtolower($lang), + strtoupper($lang), + str_replace('_', '-', strtolower($lang)), + str_replace('_', '-', strtoupper($lang)), + ]; + $params = str_replace($tags, $langcode, $params); + parse_str($params, $result); return $result; } From 4101dc6dab2fac534ea2becff31081833f0edfc2 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 25 Jun 2024 21:30:30 +0800 Subject: [PATCH 005/178] MDL-82287 core: Remove long-deprecated methods --- lib/deprecatedlib.php | 2597 +---------------------------------------- 1 file changed, 12 insertions(+), 2585 deletions(-) diff --git a/lib/deprecatedlib.php b/lib/deprecatedlib.php index bff47b7e1e33d..8f694eabf3285 100644 --- a/lib/deprecatedlib.php +++ b/lib/deprecatedlib.php @@ -32,20 +32,6 @@ /* === Functions that needs to be kept longer in deprecated lib than normal time period === */ -/** - * @deprecated since 2.7 use new events instead - */ -function add_to_log() { - throw new coding_exception('add_to_log() has been removed, please rewrite your code to the new events API'); -} - -/** - * @deprecated since 2.6 - */ -function events_trigger() { - throw new coding_exception('events_trigger() has been deprecated along with all Events 1 API in favour of Events 2 API.'); -} - /** * List all core subsystems and their location * @@ -249,381 +235,23 @@ function get_context_instance($contextlevel, $instance = 0, $strictness = IGNORE /* === End of long term deprecated api list === */ /** - * @deprecated since 2.7 - use new file picker instead - */ -function clam_log_upload() { - throw new coding_exception('clam_log_upload() can not be used any more, please use file picker instead'); -} - -/** - * @deprecated since 2.7 - use new file picker instead - */ -function clam_log_infected() { - throw new coding_exception('clam_log_infected() can not be used any more, please use file picker instead'); -} - -/** - * @deprecated since 2.7 - use new file picker instead - */ -function clam_change_log() { - throw new coding_exception('clam_change_log() can not be used any more, please use file picker instead'); -} - -/** - * @deprecated since 2.7 - infected files are now deleted in file picker - */ -function clam_replace_infected_file() { - throw new coding_exception('clam_replace_infected_file() can not be used any more, please use file picker instead'); -} - -/** - * @deprecated since 2.7 - */ -function clam_handle_infected_file() { - throw new coding_exception('clam_handle_infected_file() can not be used any more, please use file picker instead'); -} - -/** - * @deprecated since 2.7 - */ -function clam_scan_moodle_file() { - throw new coding_exception('clam_scan_moodle_file() can not be used any more, please use file picker instead'); -} - - -/** - * @deprecated since 2.7 PHP 5.4.x should be always compatible. - */ -function password_compat_not_supported() { - throw new coding_exception('Do not use password_compat_not_supported() - bcrypt is now always available'); -} - -/** - * @deprecated since 2.6 - */ -function session_get_instance() { - throw new coding_exception('session_get_instance() is removed, use \core\session\manager instead'); -} - -/** - * @deprecated since 2.6 - */ -function session_is_legacy() { - throw new coding_exception('session_is_legacy() is removed, do not use any more'); -} - -/** - * @deprecated since 2.6 - */ -function session_kill_all() { - throw new coding_exception('session_kill_all() is removed, use \core\session\manager::kill_all_sessions() instead'); -} - -/** - * @deprecated since 2.6 - */ -function session_touch() { - throw new coding_exception('session_touch() is removed, use \core\session\manager::touch_session() instead'); -} - -/** - * @deprecated since 2.6 - */ -function session_kill() { - throw new coding_exception('session_kill() is removed, use \core\session\manager::kill_session() instead'); -} - -/** - * @deprecated since 2.6 - */ -function session_kill_user() { - throw new coding_exception('session_kill_user() is removed, use \core\session\manager::kill_user_sessions() instead'); -} - -/** - * @deprecated since 2.6 - */ -function session_set_user() { - throw new coding_exception('session_set_user() is removed, use \core\session\manager::set_user() instead'); -} - -/** - * @deprecated since 2.6 - */ -function session_is_loggedinas() { - throw new coding_exception('session_is_loggedinas() is removed, use \core\session\manager::is_loggedinas() instead'); -} - -/** - * @deprecated since 2.6 - */ -function session_get_realuser() { - throw new coding_exception('session_get_realuser() is removed, use \core\session\manager::get_realuser() instead'); -} - -/** - * @deprecated since 2.6 - */ -function session_loginas() { - throw new coding_exception('session_loginas() is removed, use \core\session\manager::loginas() instead'); -} - -/** - * @deprecated since 2.6 - */ -function js_minify() { - throw new coding_exception('js_minify() is removed, use core_minify::js_files() or core_minify::js() instead.'); -} - -/** - * @deprecated since 2.6 - */ -function css_minify_css() { - throw new coding_exception('css_minify_css() is removed, use core_minify::css_files() or core_minify::css() instead.'); -} - -// === Deprecated before 2.6.0 === - -/** - * @deprecated - */ -function check_gd_version() { - throw new coding_exception('check_gd_version() is removed, GD extension is always available now'); -} - -/** - * @deprecated - */ -function update_login_count() { - throw new coding_exception('update_login_count() is removed, all calls need to be removed'); -} - -/** - * @deprecated - */ -function reset_login_count() { - throw new coding_exception('reset_login_count() is removed, all calls need to be removed'); -} - -/** - * @deprecated - */ -function update_log_display_entry() { - - throw new coding_exception('The update_log_display_entry() is removed, please use db/log.php description file instead.'); -} - -/** - * @deprecated use the text formatting in a standard way instead (https://moodledev.io/docs/apis/subsystems/output#output-functions) - * this was abused mostly for embedding of attachments - */ -function filter_text() { - throw new coding_exception('filter_text() can not be used anymore, use format_text(), format_string() etc instead.'); -} - -/** - * @deprecated Loginhttps is no longer supported - */ -function httpsrequired() { - throw new coding_exception('httpsrequired() can not be used any more. Loginhttps is no longer supported.'); -} - -/** - * @deprecated since 3.1 - replacement legacy file API methods can be found on the moodle_url class, for example: - * The moodle_url::make_legacyfile_url() method can be used to generate a legacy course file url. To generate - * course module file.php url the moodle_url::make_file_url() should be used. - */ -function get_file_url() { - throw new coding_exception('get_file_url() can not be used anymore. Please use ' . - 'moodle_url factory methods instead.'); -} - -/** - * @deprecated use get_enrolled_users($context) instead. - */ -function get_course_participants() { - throw new coding_exception('get_course_participants() can not be used any more, use get_enrolled_users() instead.'); -} - -/** - * @deprecated use is_enrolled($context, $userid) instead. - */ -function is_course_participant() { - throw new coding_exception('is_course_participant() can not be used any more, use is_enrolled() instead.'); -} - -/** - * @deprecated - */ -function get_recent_enrolments() { - throw new coding_exception('get_recent_enrolments() is removed as it returned inaccurate results.'); -} - -/** - * @deprecated use clean_param($string, PARAM_FILE) instead. - */ -function detect_munged_arguments() { - throw new coding_exception('detect_munged_arguments() can not be used any more, please use clean_param(,PARAM_FILE) instead.'); -} - - -/** - * @deprecated since 2.0 MDL-15919 - */ -function unzip_file() { - throw new coding_exception(__FUNCTION__ . '() is deprecated. ' - . 'Please use the application/zip file_packer implementation instead.'); -} - -/** - * @deprecated since 2.0 MDL-15919 - */ -function zip_files() { - throw new coding_exception(__FUNCTION__ . '() is deprecated. ' - . 'Please use the application/zip file_packer implementation instead.'); -} - -/** - * @deprecated use groups_get_all_groups() instead. - */ -function mygroupid() { - throw new coding_exception('mygroupid() can not be used any more, please use groups_get_all_groups() instead.'); -} - -/** - * @deprecated since Moodle 2.0 MDL-14617 - please do not use this function any more. - */ -function groupmode() { - throw new coding_exception('groupmode() can not be used any more, please use groups_get_* instead.'); -} - -/** - * @deprecated Since year 2006 - please do not use this function any more. - */ -function set_current_group() { - throw new coding_exception('set_current_group() can not be used anymore, please use $SESSION->currentgroup[$courseid] instead'); -} - -/** - * @deprecated Since year 2006 - please do not use this function any more. - */ -function get_current_group() { - throw new coding_exception('get_current_group() can not be used any more, please use groups_get_* instead'); -} - -/** - * @deprecated Since Moodle 2.8 - */ -function groups_filter_users_by_course_module_visible() { - throw new coding_exception('groups_filter_users_by_course_module_visible() is removed. ' . - 'Replace with a call to \core_availability\info_module::filter_user_list(), ' . - 'which does basically the same thing but includes other restrictions such ' . - 'as profile restrictions.'); -} - -/** - * @deprecated Since Moodle 2.8 - */ -function groups_course_module_visible() { - throw new coding_exception('groups_course_module_visible() is removed, use $cm->uservisible to decide whether the current - user can ' . 'access an activity.', DEBUG_DEVELOPER); -} - -/** - * @deprecated since 2.0 - */ -function error() { - throw new coding_exception('notlocalisederrormessage', 'error', $link, $message, 'error() is a removed, please call - throw new \moodle_exception() instead of error()'); -} - - -/** - * @deprecated use $PAGE->theme->name instead. - */ -function current_theme() { - throw new coding_exception('current_theme() can not be used any more, please use $PAGE->theme->name instead'); -} - -/** - * @deprecated - */ -function formerr() { - throw new coding_exception('formerr() is removed. Please change your code to use $OUTPUT->error_text($string).'); -} - -/** - * @deprecated use $OUTPUT->skip_link_target() in instead. - */ -function skip_main_destination() { - throw new coding_exception('skip_main_destination() can not be used any more, please use $OUTPUT->skip_link_target() instead.'); -} - -/** - * @deprecated use $OUTPUT->container() instead. - */ -function print_container() { - throw new coding_exception('print_container() can not be used any more. Please use $OUTPUT->container() instead.'); -} - -/** - * @deprecated use $OUTPUT->container_start() instead. - */ -function print_container_start() { - throw new coding_exception('print_container_start() can not be used any more. Please use $OUTPUT->container_start() instead.'); -} - -/** - * @deprecated use $OUTPUT->container_end() instead. - */ -function print_container_end() { - throw new coding_exception('print_container_end() can not be used any more. Please use $OUTPUT->container_end() instead.'); -} - -/** - * @deprecated since Moodle 2.0 MDL-19077 - use $OUTPUT->notification instead. - */ -function notify() { - throw new coding_exception('notify() is removed, please use $OUTPUT->notification() instead'); -} - -/** - * @deprecated use $OUTPUT->continue_button() instead. - */ -function print_continue() { - throw new coding_exception('print_continue() can not be used any more. Please use $OUTPUT->continue_button() instead.'); -} - -/** - * @deprecated use $PAGE methods instead. - */ -function print_header() { - - throw new coding_exception('print_header() can not be used any more. Please use $PAGE methods instead.'); -} - -/** - * @deprecated use $PAGE methods instead. - */ -function print_header_simple() { - - throw new coding_exception('print_header_simple() can not be used any more. Please use $PAGE methods instead.'); -} - -/** - * @deprecated use $OUTPUT->block() instead. + * @deprecated since 2.5 - do not use, the textrotate.js will work it out automatically */ -function print_side_block() { - throw new coding_exception('print_side_block() can not be used any more, please use $OUTPUT->block() instead.'); +function can_use_rotated_text() { + debugging('can_use_rotated_text() is removed. JS feature detection is used automatically.'); } /** - * @deprecated since Moodle 3.6 + * Returns system context or null if can not be created yet. + * + * @see context_system::instance() + * @deprecated since 2.2 + * @param bool $cache use caching + * @return context system context (null if context table not created yet) */ -function print_textarea() { - throw new coding_exception( - 'print_textarea() has been removed. Please use $OUTPUT->print_textarea() instead.' - ); +function get_system_context($cache = true) { + debugging('get_system_context() is deprecated, please use context_system::instance() instead.', DEBUG_DEVELOPER); + return context_system::instance(0, IGNORE_MISSING, $cache); } /** @@ -683,2207 +311,6 @@ function print_arrow($direction='up', $strsort=null, $return=false) { } } -/** - * @deprecated since Moodle 2.0 - */ -function choose_from_menu() { - throw new coding_exception('choose_from_menu() is removed. Please change your code to use html_writer::select().'); -} - -/** - * @deprecated use $OUTPUT->help_icon_scale($courseid, $scale) instead. - */ -function print_scale_menu_helpbutton() { - throw new coding_exception('print_scale_menu_helpbutton() can not be used any more. '. - 'Please use $OUTPUT->help_icon_scale($courseid, $scale) instead.'); -} - -/** - * @deprecated use html_writer::checkbox() instead. - */ -function print_checkbox() { - throw new coding_exception('print_checkbox() can not be used any more. Please use html_writer::checkbox() instead.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function update_module_button() { - throw new coding_exception('update_module_button() can not be used anymore. Activity modules should ' . - 'not add the edit module button, the link is already available in the Administration block. Themes ' . - 'can choose to display the link in the buttons row consistently for all module types.'); -} - -/** - * @deprecated use $OUTPUT->navbar() instead - */ -function print_navigation () { - throw new coding_exception('print_navigation() can not be used any more, please update use $OUTPUT->navbar() instead.'); -} - -/** - * @deprecated Please use $PAGE->navabar methods instead. - */ -function build_navigation() { - throw new coding_exception('build_navigation() can not be used any more, please use $PAGE->navbar methods instead.'); -} - -/** - * @deprecated not relevant with global navigation in Moodle 2.x+ - */ -function navmenu() { - throw new coding_exception('navmenu() can not be used any more, it is no longer relevant with global navigation.'); -} - -/// CALENDAR MANAGEMENT //////////////////////////////////////////////////////////////// - - -/** - * @deprecated please use calendar_event::create() instead. - */ -function add_event() { - throw new coding_exception('add_event() can not be used any more, please use calendar_event::create() instead.'); -} - -/** - * @deprecated please calendar_event->update() instead. - */ -function update_event() { - throw new coding_exception('update_event() is removed, please use calendar_event->update() instead.'); -} - -/** - * @deprecated please use calendar_event->delete() instead. - */ -function delete_event() { - throw new coding_exception('delete_event() can not be used any more, please use '. - 'calendar_event->delete() instead.'); -} - -/** - * @deprecated please use calendar_event->toggle_visibility(false) instead. - */ -function hide_event() { - throw new coding_exception('hide_event() can not be used any more, please use '. - 'calendar_event->toggle_visibility(false) instead.'); -} - -/** - * @deprecated please use calendar_event->toggle_visibility(true) instead. - */ -function show_event() { - throw new coding_exception('show_event() can not be used any more, please use '. - 'calendar_event->toggle_visibility(true) instead.'); -} - -/** - * @deprecated since Moodle 2.2 use core_text::xxxx() instead. - */ -function textlib_get_instance() { - throw new coding_exception('textlib_get_instance() can not be used any more, please use '. - 'core_text::functioname() instead.'); -} - -/** - * @deprecated since 2.4 - */ -function get_generic_section_name() { - throw new coding_exception('get_generic_section_name() is deprecated. Please use appropriate functionality ' - .'from class core_courseformat\\base'); -} - -/** - * @deprecated since 2.4 - */ -function get_all_sections() { - throw new coding_exception('get_all_sections() is removed. See phpdocs for this function'); -} - -/** - * @deprecated since 2.4 - */ -function add_mod_to_section() { - throw new coding_exception('Function add_mod_to_section() is removed, please use course_add_cm_to_section()'); -} - -/** - * @deprecated since 2.4 - */ -function get_all_mods() { - throw new coding_exception('Function get_all_mods() is removed. Use get_fast_modinfo() and get_module_types_names() instead. See phpdocs for details'); -} - -/** - * @deprecated since 2.4 - */ -function get_course_section() { - throw new coding_exception('Function get_course_section() is removed. Please use course_create_sections_if_missing() and get_fast_modinfo() instead.'); -} - -/** - * @deprecated since 2.4 - */ -function format_weeks_get_section_dates() { - throw new coding_exception('Function format_weeks_get_section_dates() is removed. It is not recommended to'. - ' use it outside of format_weeks plugin'); -} - -/** - * @deprecated since 2.5 - */ -function get_print_section_cm_text() { - throw new coding_exception('Function get_print_section_cm_text() is removed. Please use '. - 'cm_info::get_formatted_content() and cm_info::get_formatted_name()'); -} - -/** - * @deprecated since 2.5 - */ -function print_section_add_menus() { - throw new coding_exception('Function print_section_add_menus() is removed. Please use course renderer '. - 'function course_section_add_cm_control()'); -} - -/** - * @deprecated since 2.5. Please use: - */ -function make_editing_buttons() { - throw new coding_exception('Function make_editing_buttons() is removed, please see PHPdocs in '. - 'lib/deprecatedlib.php on how to replace it'); -} - -/** - * @deprecated since 2.5 - */ -function print_section() { - throw new coding_exception( - 'Function print_section() is removed.' . - ' Please use core_courseformat\\output\\local\\content\\section' . - ' to render a course section instead.' - ); -} - -/** - * @deprecated since 2.5 - */ -function print_overview() { - throw new coding_exception('Function print_overview() is removed. Use block course_overview to display this information'); -} - -/** - * @deprecated since 2.5 - */ -function print_recent_activity() { - throw new coding_exception('Function print_recent_activity() is removed. It is not recommended to'. - ' use it outside of block_recent_activity'); -} - -/** - * @deprecated since 2.5 - */ -function delete_course_module() { - throw new coding_exception('Function delete_course_module() is removed. Please use course_delete_module() instead.'); -} - -/** - * @deprecated since 2.5 - */ -function update_category_button() { - throw new coding_exception('Function update_category_button() is removed. Pages to view '. - 'and edit courses are now separate and no longer depend on editing mode.'); -} - -/** - * @deprecated since 2.5 - */ -function make_categories_list() { - throw new coding_exception('Global function make_categories_list() is removed. Please use '. - 'core_course_category::make_categories_list() and core_course_category::get_parents()'); -} - -/** - * @deprecated since 2.5 - */ -function category_delete_move() { - throw new coding_exception('Function category_delete_move() is removed. Please use ' . - 'core_course_category::delete_move() instead.'); -} - -/** - * @deprecated since 2.5 - */ -function category_delete_full() { - throw new coding_exception('Function category_delete_full() is removed. Please use ' . - 'core_course_category::delete_full() instead.'); -} - -/** - * @deprecated since 2.5 - */ -function move_category() { - throw new coding_exception('Function move_category() is removed. Please use core_course_category::change_parent() instead.'); -} - -/** - * @deprecated since 2.5 - */ -function course_category_hide() { - throw new coding_exception('Function course_category_hide() is removed. Please use core_course_category::hide() instead.'); -} - -/** - * @deprecated since 2.5 - */ -function course_category_show() { - throw new coding_exception('Function course_category_show() is removed. Please use core_course_category::show() instead.'); -} - -/** - * @deprecated since 2.5. Please use core_course_category::get($catid, IGNORE_MISSING) or - * core_course_category::get($catid, MUST_EXIST). - */ -function get_course_category() { - throw new coding_exception('Function get_course_category() is removed. Please use core_course_category::get(), ' . - 'see phpdocs for more details'); -} - -/** - * @deprecated since 2.5 - */ -function create_course_category() { - throw new coding_exception('Function create_course_category() is removed. Please use core_course_category::create(), ' . - 'see phpdocs for more details'); -} - -/** - * @deprecated since 2.5. Please use core_course_category::get() and core_course_category::get_children() - */ -function get_all_subcategories() { - throw new coding_exception('Function get_all_subcategories() is removed. Please use appropriate methods() '. - 'of core_course_category class. See phpdocs for more details'); -} - -/** - * @deprecated since 2.5. Please use core_course_category::get($parentid)->get_children(). - */ -function get_child_categories() { - throw new coding_exception('Function get_child_categories() is removed. Use core_course_category::get_children() or see ' . - 'phpdocs for more details.'); -} - -/** - * @deprecated since 2.5 - */ -function get_categories() { - throw new coding_exception('Function get_categories() is removed. Please use ' . - 'appropriate functions from class core_course_category'); -} - -/** -* @deprecated since 2.5 -*/ -function print_course_search() { - throw new coding_exception('Function print_course_search() is removed, please use course renderer'); -} - -/** - * @deprecated since 2.5 - */ -function print_my_moodle() { - throw new coding_exception('Function print_my_moodle() is removed, please use course renderer ' . - 'function frontpage_my_courses()'); -} - -/** - * @deprecated since 2.5 - */ -function print_remote_course() { - throw new coding_exception('Function print_remote_course() is removed, please use course renderer'); -} - -/** - * @deprecated since 2.5 - */ -function print_remote_host() { - throw new coding_exception('Function print_remote_host() is removed, please use course renderer'); -} - -/** - * @deprecated since 2.5 - */ -function print_whole_category_list() { - throw new coding_exception('Function print_whole_category_list() is removed, please use course renderer'); -} - -/** - * @deprecated since 2.5 - */ -function print_category_info() { - throw new coding_exception('Function print_category_info() is removed, please use course renderer'); -} - -/** - * @deprecated since 2.5 - */ -function get_course_category_tree() { - throw new coding_exception('Function get_course_category_tree() is removed, please use course ' . - 'renderer or core_course_category class, see function phpdocs for more info'); -} - -/** - * @deprecated since 2.5 - */ -function print_courses() { - throw new coding_exception('Function print_courses() is removed, please use course renderer'); -} - -/** - * @deprecated since 2.5 - */ -function print_course() { - throw new coding_exception('Function print_course() is removed, please use course renderer'); -} - -/** - * @deprecated since 2.5 - */ -function get_category_courses_array() { - throw new coding_exception('Function get_category_courses_array() is removed, please use methods of ' . - 'core_course_category class'); -} - -/** - * @deprecated since 2.5 - */ -function get_category_courses_array_recursively() { - throw new coding_exception('Function get_category_courses_array_recursively() is removed, please use ' . - 'methods of core_course_category class', DEBUG_DEVELOPER); -} - -/** - * @deprecated since Moodle 2.5 MDL-27814 - please do not use this function any more. - */ -function blog_get_context_url() { - throw new coding_exception('Function blog_get_context_url() is removed, getting params from context is not reliable for blogs.'); -} - -/** - * @deprecated since 2.5 - */ -function get_courses_wmanagers() { - throw new coding_exception('Function get_courses_wmanagers() is removed, please use ' . - 'core_course_category::get_courses()'); -} - -/** - * @deprecated since 2.5 - */ -function convert_tree_to_html() { - throw new coding_exception('Function convert_tree_to_html() is removed. Consider using class tabtree and core_renderer::render_tabtree()'); -} - -/** - * @deprecated since 2.5 - */ -function convert_tabrows_to_tree() { - throw new coding_exception('Function convert_tabrows_to_tree() is removed. Consider using class tabtree'); -} - -/** - * @deprecated since 2.5 - do not use, the textrotate.js will work it out automatically - */ -function can_use_rotated_text() { - debugging('can_use_rotated_text() is removed. JS feature detection is used automatically.'); -} - -/** - * @deprecated since Moodle 2.2 MDL-35009 - please do not use this function any more. - */ -function get_context_instance_by_id() { - throw new coding_exception('get_context_instance_by_id() is now removed, please use context::instance_by_id($id) instead.'); -} - -/** - * Returns system context or null if can not be created yet. - * - * @see context_system::instance() - * @deprecated since 2.2 - * @param bool $cache use caching - * @return context system context (null if context table not created yet) - */ -function get_system_context($cache = true) { - debugging('get_system_context() is deprecated, please use context_system::instance() instead.', DEBUG_DEVELOPER); - return context_system::instance(0, IGNORE_MISSING, $cache); -} - -/** - * @deprecated since 2.2, use $context->get_parent_context_ids() instead - */ -function get_parent_contexts() { - throw new coding_exception('get_parent_contexts() is removed, please use $context->get_parent_context_ids() instead.'); -} - -/** - * @deprecated since Moodle 2.2 - */ -function get_parent_contextid() { - throw new coding_exception('get_parent_contextid() is removed, please use $context->get_parent_context() instead.'); -} - -/** - * @deprecated since 2.2 - */ -function get_child_contexts() { - throw new coding_exception('get_child_contexts() is removed, please use $context->get_child_contexts() instead.'); -} - -/** - * @deprecated since 2.2 - */ -function create_contexts() { - throw new coding_exception('create_contexts() is removed, please use context_helper::create_instances() instead.'); -} - -/** - * @deprecated since 2.2 - */ -function cleanup_contexts() { - throw new coding_exception('cleanup_contexts() is removed, please use context_helper::cleanup_instances() instead.'); -} - -/** - * @deprecated since 2.2 - */ -function build_context_path() { - throw new coding_exception('build_context_path() is removed, please use context_helper::build_all_paths() instead.'); -} - -/** - * @deprecated since 2.2 - */ -function rebuild_contexts() { - throw new coding_exception('rebuild_contexts() is removed, please use $context->reset_paths(true) instead.'); -} - -/** - * @deprecated since Moodle 2.2 - */ -function preload_course_contexts() { - throw new coding_exception('preload_course_contexts() is removed, please use context_helper::preload_course() instead.'); -} - -/** - * @deprecated since Moodle 2.2 - */ -function context_moved() { - throw new coding_exception('context_moved() is removed, please use context::update_moved() instead.'); -} - -/** - * @deprecated since 2.2 - */ -function fetch_context_capabilities() { - throw new coding_exception('fetch_context_capabilities() is removed, please use $context->get_capabilities() instead.'); -} - -/** - * @deprecated since 2.2 - */ -function context_instance_preload() { - throw new coding_exception('context_instance_preload() is removed, please use context_helper::preload_from_record() instead.'); -} - -/** - * @deprecated since 2.2 - */ -function get_contextlevel_name() { - throw new coding_exception('get_contextlevel_name() is removed, please use context_helper::get_level_name() instead.'); -} - -/** - * @deprecated since 2.2 - */ -function print_context_name() { - throw new coding_exception('print_context_name() is removed, please use $context->get_context_name() instead.'); -} - -/** - * @deprecated since 2.2, use $context->mark_dirty() instead - */ -function mark_context_dirty() { - throw new coding_exception('mark_context_dirty() is removed, please use $context->mark_dirty() instead.'); -} - -/** - * @deprecated since Moodle 2.2 - */ -function delete_context() { - throw new coding_exception('delete_context() is removed, please use context_helper::delete_instance() ' . - 'or $context->delete_content() instead.'); -} - -/** - * @deprecated since 2.2 - */ -function get_context_url() { - throw new coding_exception('get_context_url() is removed, please use $context->get_url() instead.'); -} - -/** - * @deprecated since 2.2 - */ -function get_course_context() { - throw new coding_exception('get_course_context() is removed, please use $context->get_course_context(true) instead.'); -} - -/** - * @deprecated since 2.2 - */ -function get_user_courses_bycap() { - throw new coding_exception('get_user_courses_bycap() is removed, please use enrol_get_users_courses() instead.'); -} - -/** - * @deprecated since Moodle 2.2 - */ -function get_role_context_caps() { - throw new coding_exception('get_role_context_caps() is removed, it is really slow. Don\'t use it.'); -} - -/** - * @deprecated since 2.2 - */ -function get_courseid_from_context() { - throw new coding_exception('get_courseid_from_context() is removed, please use $context->get_course_context(false) instead.'); -} - -/** - * @deprecated since 2.2 - */ -function context_instance_preload_sql() { - throw new coding_exception('context_instance_preload_sql() is removed, please use context_helper::get_preload_record_columns_sql() instead.'); -} - -/** - * @deprecated since 2.2 - */ -function get_related_contexts_string() { - throw new coding_exception('get_related_contexts_string() is removed, please use $context->get_parent_context_ids(true) instead.'); -} - -/** - * @deprecated since 2.6 - */ -function get_plugin_list_with_file() { - throw new coding_exception('get_plugin_list_with_file() is removed, please use core_component::get_plugin_list_with_file() instead.'); -} - -/** - * @deprecated since 2.6 - */ -function check_browser_operating_system() { - throw new coding_exception('check_browser_operating_system is removed, please update your code to use core_useragent instead.'); -} - -/** - * @deprecated since 2.6 - */ -function check_browser_version() { - throw new coding_exception('check_browser_version is removed, please update your code to use core_useragent instead.'); -} - -/** - * @deprecated since 2.6 - */ -function get_device_type() { - throw new coding_exception('get_device_type is removed, please update your code to use core_useragent instead.'); -} - -/** - * @deprecated since 2.6 - */ -function get_device_type_list() { - throw new coding_exception('get_device_type_list is removed, please update your code to use core_useragent instead.'); -} - -/** - * @deprecated since 2.6 - */ -function get_selected_theme_for_device_type() { - throw new coding_exception('get_selected_theme_for_device_type is removed, please update your code to use core_useragent instead.'); -} - -/** - * @deprecated since 2.6 - */ -function get_device_cfg_var_name() { - throw new coding_exception('get_device_cfg_var_name is removed, please update your code to use core_useragent instead.'); -} - -/** - * @deprecated since 2.6 - */ -function set_user_device_type() { - throw new coding_exception('set_user_device_type is removed, please update your code to use core_useragent instead.'); -} - -/** - * @deprecated since 2.6 - */ -function get_user_device_type() { - throw new coding_exception('get_user_device_type is removed, please update your code to use core_useragent instead.'); -} - -/** - * @deprecated since 2.6 - */ -function get_browser_version_classes() { - throw new coding_exception('get_browser_version_classes is removed, please update your code to use core_useragent instead.'); -} - -/** - * @deprecated since Moodle 2.6 - */ -function generate_email_supportuser() { - throw new coding_exception('generate_email_supportuser is removed, please use core_user::get_support_user'); -} - -/** - * @deprecated since Moodle 2.6 - */ -function badges_get_issued_badge_info() { - throw new coding_exception('Function badges_get_issued_badge_info() is removed. Please use core_badges_assertion class and methods to generate badge assertion.'); -} - -/** - * @deprecated since 2.6 - */ -function can_use_html_editor() { - throw new coding_exception('can_use_html_editor is removed, please update your code to assume it returns true.'); -} - - -/** - * @deprecated since Moodle 2.7, use {@link user_count_login_failures()} instead. - */ -function count_login_failures() { - throw new coding_exception('count_login_failures() can not be used any more, please use user_count_login_failures().'); -} - -/** - * @deprecated since 2.7 MDL-33099/MDL-44088 - please do not use this function any more. - */ -function ajaxenabled() { - throw new coding_exception('ajaxenabled() can not be used anymore. Update your code to work with JS at all times.'); -} - -/** - * @deprecated Since Moodle 2.7 MDL-44070 - */ -function coursemodule_visible_for_user() { - throw new coding_exception('coursemodule_visible_for_user() can not be used any more, - please use \core_availability\info_module::is_user_visible()'); -} - -/** - * @deprecated since Moodle 2.8 MDL-36014, MDL-35618 this functionality is removed - */ -function enrol_cohort_get_cohorts() { - throw new coding_exception('Function enrol_cohort_get_cohorts() is removed, use '. - 'cohort_get_available_cohorts() instead'); -} - -/** - * @deprecated since Moodle 2.8 MDL-36014 please use cohort_can_view_cohort() - */ -function enrol_cohort_can_view_cohort() { - throw new coding_exception('Function enrol_cohort_can_view_cohort() is removed, use cohort_can_view_cohort() instead'); -} - -/** - * @deprecated since Moodle 2.8 MDL-36014 use cohort_get_available_cohorts() instead - */ -function cohort_get_visible_list() { - throw new coding_exception('Function cohort_get_visible_list() is removed. Please use function cohort_get_available_cohorts() ". - "that correctly checks capabilities.'); -} - -/** - * @deprecated since Moodle 2.8 MDL-35618 this functionality is removed - */ -function enrol_cohort_enrol_all_users() { - throw new coding_exception('enrol_cohort_enrol_all_users() is removed. This functionality is moved to enrol_manual.'); -} - -/** - * @deprecated since Moodle 2.8 MDL-35618 this functionality is removed - */ -function enrol_cohort_search_cohorts() { - throw new coding_exception('enrol_cohort_search_cohorts() is removed. This functionality is moved to enrol_manual.'); -} - -/* === Apis deprecated in since Moodle 2.9 === */ - -/** - * @deprecated since Moodle 2.9 MDL-49371 - please do not use this function any more. - */ -function message_current_user_is_involved() { - throw new coding_exception('message_current_user_is_involved() can not be used any more.'); -} - -/** - * @deprecated since Moodle 2.9 MDL-45898 - please do not use this function any more. - */ -function profile_display_badges() { - throw new coding_exception('profile_display_badges() can not be used any more.'); -} - -/** - * @deprecated since Moodle 2.9 MDL-45774 - Please do not use this function any more. - */ -function useredit_shared_definition_preferences() { - throw new coding_exception('useredit_shared_definition_preferences() can not be used any more.'); -} - - -/** - * @deprecated since Moodle 2.9 - */ -function calendar_normalize_tz() { - throw new coding_exception('calendar_normalize_tz() can not be used any more, please use core_date::normalise_timezone() instead.'); -} - -/** - * @deprecated since Moodle 2.9 - */ -function get_user_timezone_offset() { - throw new coding_exception('get_user_timezone_offset() can not be used any more, please use standard PHP DateTimeZone class instead'); - -} - -/** - * @deprecated since Moodle 2.9 - */ -function get_timezone_offset() { - throw new coding_exception('get_timezone_offset() can not be used any more, please use standard PHP DateTimeZone class instead'); -} - -/** - * @deprecated since Moodle 2.9 - */ -function get_list_of_timezones() { - throw new coding_exception('get_list_of_timezones() can not be used any more, please use core_date::get_list_of_timezones() instead'); -} - -/** - * @deprecated since Moodle 2.9 - */ -function update_timezone_records() { - throw new coding_exception('update_timezone_records() can not be used any more, please use standard PHP DateTime class instead'); -} - -/** - * @deprecated since Moodle 2.9 - */ -function calculate_user_dst_table() { - throw new coding_exception('calculate_user_dst_table() can not be used any more, please use standard PHP DateTime class instead'); -} - -/** - * @deprecated since Moodle 2.9 - */ -function dst_changes_for_year() { - throw new coding_exception('dst_changes_for_year() can not be used any more, please use standard DateTime class instead'); -} - -/** - * @deprecated since Moodle 2.9 - */ -function get_timezone_record() { - throw new coding_exception('get_timezone_record() can not be used any more, please use standard PHP DateTime class instead'); -} - -/* === Apis deprecated since Moodle 3.0 === */ -/** - * @deprecated since Moodle 3.0 MDL-49360 - please do not use this function any more. - */ -function get_referer() { - throw new coding_exception('get_referer() can not be used any more. Please use get_local_referer() instead.'); -} - -/** - * @deprecated since Moodle 3.0 use \core_useragent::is_web_crawler instead. - */ -function is_web_crawler() { - throw new coding_exception('is_web_crawler() can not be used any more. Please use core_useragent::is_web_crawler() instead.'); -} - -/** - * @deprecated since Moodle 3.0 MDL-50287 - please do not use this function any more. - */ -function completion_cron() { - throw new coding_exception('completion_cron() can not be used any more. Functionality has been moved to scheduled tasks.'); -} - -/** - * @deprecated since 3.0 - */ -function coursetag_get_tags() { - throw new coding_exception('Function coursetag_get_tags() can not be used any more. ' . - 'Userid is no longer used for tagging courses.'); -} - -/** - * @deprecated since 3.0 - */ -function coursetag_get_all_tags() { - throw new coding_exception('Function coursetag_get_all_tags() can not be used any more. Userid is no ' . - 'longer used for tagging courses.'); -} - -/** - * @deprecated since 3.0 - */ -function coursetag_get_jscript() { - throw new coding_exception('Function coursetag_get_jscript() can not be used any more and is obsolete.'); -} - -/** - * @deprecated since 3.0 - */ -function coursetag_get_jscript_links() { - throw new coding_exception('Function coursetag_get_jscript_links() can not be used any more and is obsolete.'); -} - -/** - * @deprecated since 3.0 - */ -function coursetag_get_records() { - throw new coding_exception('Function coursetag_get_records() can not be used any more. ' . - 'Userid is no longer used for tagging courses.'); -} - -/** - * @deprecated since 3.0 - */ -function coursetag_store_keywords() { - throw new coding_exception('Function coursetag_store_keywords() can not be used any more. ' . - 'Userid is no longer used for tagging courses.'); -} - -/** - * @deprecated since 3.0 - */ -function coursetag_delete_keyword() { - throw new coding_exception('Function coursetag_delete_keyword() can not be used any more. ' . - 'Userid is no longer used for tagging courses.'); -} - -/** - * @deprecated since 3.0 - */ -function coursetag_get_tagged_courses() { - throw new coding_exception('Function coursetag_get_tagged_courses() can not be used any more. ' . - 'Userid is no longer used for tagging courses.'); -} - -/** - * @deprecated since 3.0 - */ -function coursetag_delete_course_tags() { - throw new coding_exception('Function coursetag_delete_course_tags() is deprecated. ' . - 'Use core_tag_tag::remove_all_item_tags().'); -} - -/** - * @deprecated since 3.1. Use core_tag_tag::get($tagid)->update() instead - */ -function tag_type_set() { - throw new coding_exception('tag_type_set() can not be used anymore. Please use ' . - 'core_tag_tag::get($tagid)->update().'); -} - -/** - * @deprecated since 3.1. Use core_tag_tag::get($tagid)->update() instead - */ -function tag_description_set() { - throw new coding_exception('tag_description_set() can not be used anymore. Please use ' . - 'core_tag_tag::get($tagid)->update().'); -} - -/** - * @deprecated since 3.1. Use core_tag_tag::get_item_tags() instead - */ -function tag_get_tags() { - throw new coding_exception('tag_get_tags() can not be used anymore. Please use ' . - 'core_tag_tag::get_item_tags().'); -} - -/** - * @deprecated since 3.1 - */ -function tag_get_tags_array() { - throw new coding_exception('tag_get_tags_array() can not be used anymore. Please use ' . - 'core_tag_tag::get_item_tags_array().'); -} - -/** - * @deprecated since 3.1. Use core_tag_tag::get_item_tags_array() or $OUTPUT->tag_list(core_tag_tag::get_item_tags()) - */ -function tag_get_tags_csv() { - throw new coding_exception('tag_get_tags_csv() can not be used anymore. Please use ' . - 'core_tag_tag::get_item_tags_array() or $OUTPUT->tag_list(core_tag_tag::get_item_tags()).'); -} - -/** - * @deprecated since 3.1. Use core_tag_tag::get_item_tags() instead - */ -function tag_get_tags_ids() { - throw new coding_exception('tag_get_tags_ids() can not be used anymore. Please consider using ' . - 'core_tag_tag::get_item_tags() or similar methods.'); -} - -/** - * @deprecated since 3.1. Use core_tag_tag::get_by_name() or core_tag_tag::get_by_name_bulk() - */ -function tag_get_id() { - throw new coding_exception('tag_get_id() can not be used anymore. Please use ' . - 'core_tag_tag::get_by_name() or core_tag_tag::get_by_name_bulk()'); -} - -/** - * @deprecated since 3.1. Use core_tag_tag::get($tagid)->update() instead - */ -function tag_rename() { - throw new coding_exception('tag_rename() can not be used anymore. Please use ' . - 'core_tag_tag::get($tagid)->update()'); -} - -/** - * @deprecated since 3.1. Use core_tag_tag::remove_item_tag() instead - */ -function tag_delete_instance() { - throw new coding_exception('tag_delete_instance() can not be used anymore. Please use ' . - 'core_tag_tag::remove_item_tag()'); -} - -/** - * @deprecated since 3.1. Use core_tag_tag::get_by_name()->get_tagged_items() instead - */ -function tag_find_records() { - throw new coding_exception('tag_find_records() can not be used anymore. Please use ' . - 'core_tag_tag::get_by_name()->get_tagged_items()'); -} - -/** - * @deprecated since 3.1 - */ -function tag_add() { - throw new coding_exception('tag_add() can not be used anymore. You can use ' . - 'core_tag_tag::create_if_missing(), however it should not be necessary since tags are ' . - 'created automatically when assigned to items'); -} - -/** - * @deprecated since 3.1. Use core_tag_tag::set_item_tags() or core_tag_tag::add_item_tag() instead - */ -function tag_assign() { - throw new coding_exception('tag_assign() can not be used anymore. Please use ' . - 'core_tag_tag::set_item_tags() or core_tag_tag::add_item_tag() instead. Tag instance ' . - 'ordering should not be set manually'); -} - -/** - * @deprecated since 3.1. Use core_tag_tag::get($tagid)->count_tagged_items() instead - */ -function tag_record_count() { - throw new coding_exception('tag_record_count() can not be used anymore. Please use ' . - 'core_tag_tag::get($tagid)->count_tagged_items().'); -} - -/** - * @deprecated since 3.1. Use core_tag_tag::get($tagid)->is_item_tagged_with() instead - */ -function tag_record_tagged_with() { - throw new coding_exception('tag_record_tagged_with() can not be used anymore. Please use ' . - 'core_tag_tag::get($tagid)->is_item_tagged_with().'); -} - -/** - * @deprecated since 3.1. Use core_tag_tag::get($tagid)->flag() instead - */ -function tag_set_flag() { - throw new coding_exception('tag_set_flag() can not be used anymore. Please use ' . - 'core_tag_tag::get($tagid)->flag()'); -} - -/** - * @deprecated since 3.1. Use core_tag_tag::get($tagid)->reset_flag() instead - */ -function tag_unset_flag() { - throw new coding_exception('tag_unset_flag() can not be used anymore. Please use ' . - 'core_tag_tag::get($tagid)->reset_flag()'); -} - -/** - * @deprecated since 3.1 - */ -function tag_print_cloud() { - throw new coding_exception('tag_print_cloud() can not be used anymore. Please use ' . - 'core_tag_collection::get_tag_cloud(), templateable core_tag\output\tagcloud and ' . - 'template core_tag/tagcloud.'); -} - -/** - * @deprecated since 3.0 - */ -function tag_autocomplete() { - throw new coding_exception('tag_autocomplete() can not be used anymore. New form ' . - 'element "tags" does proper autocomplete.'); -} - -/** - * @deprecated since 3.1 - */ -function tag_print_description_box() { - throw new coding_exception('tag_print_description_box() can not be used anymore. ' . - 'See core_tag_renderer for similar code'); -} - -/** - * @deprecated since 3.1 - */ -function tag_print_management_box() { - throw new coding_exception('tag_print_management_box() can not be used anymore. ' . - 'See core_tag_renderer for similar code'); -} - -/** - * @deprecated since 3.1 - */ -function tag_print_search_box() { - throw new coding_exception('tag_print_search_box() can not be used anymore. ' . - 'See core_tag_renderer for similar code'); -} - -/** - * @deprecated since 3.1 - */ -function tag_print_search_results() { - throw new coding_exception('tag_print_search_results() can not be used anymore. ' . - 'In /tag/search.php the search results are printed using the core_tag/tagcloud template.'); -} - -/** - * @deprecated since 3.1 - */ -function tag_print_tagged_users_table() { - throw new coding_exception('tag_print_tagged_users_table() can not be used anymore. ' . - 'See core_user_renderer for similar code'); -} - -/** - * @deprecated since 3.1 - */ -function tag_print_user_box() { - throw new coding_exception('tag_print_user_box() can not be used anymore. ' . - 'See core_user_renderer for similar code'); -} - -/** - * @deprecated since 3.1 - */ -function tag_print_user_list() { - throw new coding_exception('tag_print_user_list() can not be used anymore. ' . - 'See core_user_renderer for similar code'); -} - -/** - * @deprecated since 3.1 - */ -function tag_display_name() { - throw new coding_exception('tag_display_name() can not be used anymore. Please use ' . - 'core_tag_tag::make_display_name().'); - -} - -/** - * @deprecated since 3.1 - */ -function tag_normalize() { - throw new coding_exception('tag_normalize() can not be used anymore. Please use ' . - 'core_tag_tag::normalize().'); -} - -/** - * @deprecated since 3.1 - */ -function tag_get_related_tags_csv() { - throw new coding_exception('tag_get_related_tags_csv() can not be used anymore. Please ' . - 'consider looping through array or using $OUTPUT->tag_list(core_tag_tag::get_item_tags()).'); -} - -/** - * @deprecated since 3.1 - */ -function tag_set() { - throw new coding_exception('tag_set() can not be used anymore. Please use ' . - 'core_tag_tag::set_item_tags().'); -} - -/** - * @deprecated since 3.1 - */ -function tag_set_add() { - throw new coding_exception('tag_set_add() can not be used anymore. Please use ' . - 'core_tag_tag::add_item_tag().'); -} - -/** - * @deprecated since 3.1 - */ -function tag_set_delete() { - throw new coding_exception('tag_set_delete() can not be used anymore. Please use ' . - 'core_tag_tag::remove_item_tag().'); -} - -/** - * @deprecated since 3.1 - */ -function tag_get() { - throw new coding_exception('tag_get() can not be used anymore. Please use ' . - 'core_tag_tag::get() or core_tag_tag::get_by_name().'); -} - -/** - * @deprecated since 3.1 - */ -function tag_get_related_tags() { - throw new coding_exception('tag_get_related_tags() can not be used anymore. Please use ' . - 'core_tag_tag::get_correlated_tags(), core_tag_tag::get_related_tags() or ' . - 'core_tag_tag::get_manual_related_tags().'); -} - -/** - * @deprecated since 3.1 - */ -function tag_delete() { - throw new coding_exception('tag_delete() can not be used anymore. Please use ' . - 'core_tag_tag::delete_tags().'); -} - -/** - * @deprecated since 3.1 - */ -function tag_delete_instances() { - throw new coding_exception('tag_delete_instances() can not be used anymore. Please use ' . - 'core_tag_tag::delete_instances().'); -} - -/** - * @deprecated since 3.1 - */ -function tag_cleanup() { - throw new coding_exception('tag_cleanup() can not be used anymore. Please use ' . - '\core\task\tag_cron_task::cleanup().'); -} - -/** - * @deprecated since 3.1 - */ -function tag_bulk_delete_instances() { - throw new coding_exception('tag_bulk_delete_instances() can not be used anymore. Please use ' . - '\core\task\tag_cron_task::bulk_delete_instances().'); - -} - -/** - * @deprecated since 3.1 - */ -function tag_compute_correlations() { - throw new coding_exception('tag_compute_correlations() can not be used anymore. Please use ' . - 'use \core\task\tag_cron_task::compute_correlations().'); -} - -/** - * @deprecated since 3.1 - */ -function tag_process_computed_correlation() { - throw new coding_exception('tag_process_computed_correlation() can not be used anymore. Please use ' . - 'use \core\task\tag_cron_task::process_computed_correlation().'); -} - -/** - * @deprecated since 3.1 - */ -function tag_cron() { - throw new coding_exception('tag_cron() can not be used anymore. Please use ' . - 'use \core\task\tag_cron_task::execute().'); -} - -/** - * @deprecated since 3.1 - */ -function tag_find_tags() { - throw new coding_exception('tag_find_tags() can not be used anymore.'); -} - -/** - * @deprecated since 3.1 - */ -function tag_get_name() { - throw new coding_exception('tag_get_name() can not be used anymore.'); -} - -/** - * @deprecated since 3.1 - */ -function tag_get_correlated() { - throw new coding_exception('tag_get_correlated() can not be used anymore. Please use ' . - 'use core_tag_tag::get_correlated_tags().'); - -} - -/** - * @deprecated since 3.1 - */ -function tag_cloud_sort() { - throw new coding_exception('tag_cloud_sort() can not be used anymore. Similar method can ' . - 'be found in core_tag_collection::cloud_sort().'); -} - -/** - * @deprecated since Moodle 3.1 - */ -function events_load_def() { - throw new coding_exception('events_load_def() has been deprecated along with all Events 1 API in favour of Events 2 API.'); - -} - -/** - * @deprecated since Moodle 3.1 - */ -function events_queue_handler() { - throw new coding_exception('events_queue_handler() has been deprecated along with all Events 1 API in favour of Events 2 API.'); -} - -/** - * @deprecated since Moodle 3.1 - */ -function events_dispatch() { - throw new coding_exception('events_dispatch() has been deprecated along with all Events 1 API in favour of Events 2 API.'); -} - -/** - * @deprecated since Moodle 3.1 - */ -function events_process_queued_handler() { - throw new coding_exception( - 'events_process_queued_handler() has been deprecated along with all Events 1 API in favour of Events 2 API.' - ); -} - -/** - * @deprecated since Moodle 3.1 - */ -function events_update_definition() { - throw new coding_exception( - 'events_update_definition has been deprecated along with all Events 1 API in favour of Events 2 API.' - ); -} - -/** - * @deprecated since Moodle 3.1 - */ -function events_cron() { - throw new coding_exception('events_cron() has been deprecated along with all Events 1 API in favour of Events 2 API.'); -} - -/** - * @deprecated since Moodle 3.1 - */ -function events_trigger_legacy() { - throw new coding_exception('events_trigger_legacy() has been deprecated along with all Events 1 API in favour of Events 2 API.'); -} - -/** - * @deprecated since Moodle 3.1 - */ -function events_is_registered() { - throw new coding_exception('events_is_registered() has been deprecated along with all Events 1 API in favour of Events 2 API.'); -} - -/** - * @deprecated since Moodle 3.1 - */ -function events_pending_count() { - throw new coding_exception('events_pending_count() has been deprecated along with all Events 1 API in favour of Events 2 API.'); -} - -/** - * @deprecated since Moodle 3.0 - this is a part of clamav plugin now. - */ -function clam_message_admins() { - throw new coding_exception('clam_message_admins() can not be used anymore. Please use ' . - 'message_admins() method of \antivirus_clamav\scanner class.'); -} - -/** - * @deprecated since Moodle 3.0 - this is a part of clamav plugin now. - */ -function get_clam_error_code() { - throw new coding_exception('get_clam_error_code() can not be used anymore. Please use ' . - 'get_clam_error_code() method of \antivirus_clamav\scanner class.'); -} - -/** - * @deprecated since 3.1 - */ -function course_get_cm_rename_action() { - throw new coding_exception('course_get_cm_rename_action() can not be used anymore. Please use ' . - 'inplace_editable https://docs.moodle.org/dev/Inplace_editable.'); - -} - -/** - * @deprecated since Moodle 3.1 - */ -function course_scale_used() { - throw new coding_exception('course_scale_used() can not be used anymore. Plugins can ' . - 'implement _scale_used_anywhere, all implementations of _scale_used are now ignored'); -} - -/** - * @deprecated since Moodle 3.1 - */ -function site_scale_used() { - throw new coding_exception('site_scale_used() can not be used anymore. Plugins can implement ' . - '_scale_used_anywhere, all implementations of _scale_used are now ignored'); -} - -/** - * @deprecated since Moodle 3.1. Use external_api::external_function_info(). - */ -function external_function_info() { - throw new coding_exception('external_function_info() can not be used any'. - 'more. Please use external_api::external_function_info() instead.'); -} - -/** - * @deprecated since Moodle 3.2 - * @see csv_import_reader::load_csv_content() - */ -function get_records_csv() { - throw new coding_exception('get_records_csv() can not be used anymore. Please use ' . - 'lib/csvlib.class.php csv_import_reader() instead.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function put_records_csv() { - throw new coding_exception(__FUNCTION__ . '() has been removed, please use \core\dataformat::download_data() instead'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function css_is_colour() { - throw new coding_exception('css_is_colour() can not be used anymore.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function css_is_width() { - throw new coding_exception('css_is_width() can not be used anymore.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function css_sort_by_count() { - throw new coding_exception('css_sort_by_count() can not be used anymore.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function message_get_course_contexts() { - throw new coding_exception('message_get_course_contexts() can not be used anymore.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function message_remove_url_params() { - throw new coding_exception('message_remove_url_params() can not be used anymore.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function message_count_messages() { - throw new coding_exception('message_count_messages() can not be used anymore.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function message_count_blocked_users() { - throw new coding_exception('message_count_blocked_users() can not be used anymore. Please use ' . - '\core_message\api::count_blocked_users() instead.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function message_contact_link() { - throw new coding_exception('message_contact_link() can not be used anymore.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function message_get_recent_notifications() { - throw new coding_exception('message_get_recent_notifications() can not be used anymore.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function message_history_link() { - throw new coding_exception('message_history_link() can not be used anymore.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function message_search() { - throw new coding_exception('message_search() can not be used anymore.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function message_shorten_message() { - throw new coding_exception('message_shorten_message() can not be used anymore.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function message_get_fragment() { - throw new coding_exception('message_get_fragment() can not be used anymore.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function message_get_history() { - throw new coding_exception('message_get_history() can not be used anymore.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function message_get_contact_add_remove_link() { - throw new coding_exception('message_get_contact_add_remove_link() can not be used anymore.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function message_get_contact_block_link() { - throw new coding_exception('message_get_contact_block_link() can not be used anymore.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function message_mark_messages_read() { - throw new coding_exception('message_mark_messages_read() can not be used anymore. Please use ' . - '\core_message\api::mark_all_messages_as_read() instead.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function message_can_post_message() { - throw new coding_exception('message_can_post_message() can not be used anymore. Please use ' . - '\core_message\api::can_send_message() instead.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function message_is_user_non_contact_blocked() { - throw new coding_exception('message_is_user_non_contact_blocked() can not be used anymore. Please use ' . - '\core_message\api::is_user_non_contact_blocked() instead.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function message_is_user_blocked() { - throw new coding_exception('message_is_user_blocked() can not be used anymore. Please use ' . - '\core_message\api::is_user_blocked() instead.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function print_log() { - throw new coding_exception('print_log() can not be used anymore. Please use the ' . - 'report_log framework instead.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function print_mnet_log() { - throw new coding_exception('print_mnet_log() can not be used anymore. Please use the ' . - 'report_log framework instead.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function print_log_csv() { - throw new coding_exception('print_log_csv() can not be used anymore. Please use the ' . - 'report_log framework instead.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function print_log_xls() { - throw new coding_exception('print_log_xls() can not be used anymore. Please use the ' . - 'report_log framework instead.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function print_log_ods() { - throw new coding_exception('print_log_ods() can not be used anymore. Please use the ' . - 'report_log framework instead.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function build_logs_array() { - throw new coding_exception('build_logs_array() can not be used anymore. Please use the ' . - 'report_log framework instead.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function get_logs_usercourse() { - throw new coding_exception('get_logs_usercourse() can not be used anymore. Please use the ' . - 'report_log framework instead.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function get_logs_userday() { - throw new coding_exception('get_logs_userday() can not be used anymore. Please use the ' . - 'report_log framework instead.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function get_logs() { - throw new coding_exception('get_logs() can not be used anymore. Please use the ' . - 'report_log framework instead.'); -} - -/** - * @deprecated since Moodle 3.2 - */ -function prevent_form_autofill_password() { - throw new coding_exception('prevent_form_autofill_password() can not be used anymore.'); -} - -/** - * @deprecated since Moodle 3.3 MDL-57370 - */ -function message_get_recent_conversations($userorid, $limitfrom = 0, $limitto = 100) { - throw new coding_exception('message_get_recent_conversations() can not be used any more. ' . - 'Please use \core_message\api::get_conversations() instead.', DEBUG_DEVELOPER); -} - -/** - * @deprecated since Moodle 3.2 - */ -function calendar_preferences_button() { - throw new coding_exception('calendar_preferences_button() can not be used anymore. The calendar ' . - 'preferences are now linked to the user preferences page.'); -} - -/** - * @deprecated since 3.3 - */ -function calendar_wday_name() { - throw new coding_exception('Function calendar_wday_name() is removed and no longer used in core.'); -} - -/** - * @deprecated since 3.3 - */ -function calendar_get_block_upcoming() { - throw new coding_exception('Function calendar_get_block_upcoming() is removed,' . - 'Please see block_calendar_upcoming::get_content() for the correct API usage.'); -} - -/** - * @deprecated since 3.3 - */ -function calendar_print_month_selector() { - throw new coding_exception('Function calendar_print_month_selector() is removed and can no longer used in core.'); -} - -/** - * @deprecated since 3.3 - */ -function calendar_cron() { - throw new coding_exception('Function calendar_cron() is removed. Please use the core\task\calendar_cron_task instead.'); -} - -/** - * @deprecated since Moodle 3.4 and removed immediately. MDL-49398. - */ -function load_course_context() { - throw new coding_exception('load_course_context() is removed. Do not use private functions or data structures.'); -} - -/** - * @deprecated since Moodle 3.4 and removed immediately. MDL-49398. - */ -function load_role_access_by_context() { - throw new coding_exception('load_role_access_by_context() is removed. Do not use private functions or data structures.'); -} - -/** - * @deprecated since Moodle 3.4 and removed immediately. MDL-49398. - */ -function dedupe_user_access() { - throw new coding_exception('dedupe_user_access() is removed. Do not use private functions or data structures.'); -} - -/** - * @deprecated since Moodle 3.4. MDL-49398. - */ -function get_user_access_sitewide() { - throw new coding_exception('get_user_access_sitewide() is removed. Do not use private functions or data structures.'); -} - -/** - * @deprecated since Moodle 3.4. MDL-59333 - */ -function calendar_get_mini() { - throw new coding_exception('calendar_get_mini() has been removed. Please update your code to use calendar_get_view.'); -} - -/** - * @deprecated since Moodle 3.4. MDL-59333 - */ -function calendar_get_upcoming() { - throw new coding_exception('calendar_get_upcoming() has been removed. ' . - 'Please see block_calendar_upcoming::get_content() for the correct API usage.'); -} - -/** - * @deprecated since Moodle 3.4. MDL-50666 - */ -function allow_override() { - throw new coding_exception('allow_override() has been removed. Please update your code to use core_role_set_override_allowed.'); -} - -/** - * @deprecated since Moodle 3.4. MDL-50666 - */ -function allow_assign() { - throw new coding_exception('allow_assign() has been removed. Please update your code to use core_role_set_assign_allowed.'); -} - -/** - * @deprecated since Moodle 3.4. MDL-50666 - */ -function allow_switch() { - throw new coding_exception('allow_switch() has been removed. Please update your code to use core_role_set_switch_allowed.'); -} - -/** - * @deprecated since Moodle 3.5. MDL-61132 - */ -function question_add_tops() { - throw new coding_exception( - 'question_add_tops() has been removed. You may want to pass $top = true to get_categories_for_contexts().' - ); -} - -/** - * @deprecated since Moodle 3.5. MDL-61132 - */ -function question_is_only_toplevel_category_in_context() { - throw new coding_exception('question_is_only_toplevel_category_in_context() has been removed. ' - . 'Please update your code to use question_is_only_child_of_top_category_in_context() instead.'); -} - -/** - * @deprecated since Moodle 3.5 - */ -function message_move_userfrom_unread2read() { - throw new coding_exception('message_move_userfrom_unread2read() has been removed.'); -} - -/** - * @deprecated since Moodle 3.5 - */ -function message_get_blocked_users() { - throw new coding_exception( - 'message_get_blocked_users() has been removed, please use \core_message\api::get_blocked_users() instead.' - ); -} - -/** - * @deprecated since Moodle 3.5 - */ -function message_get_contacts() { - throw new coding_exception('message_get_contacts() has been removed.'); -} - -/** - * @deprecated since Moodle 3.5 - */ -function message_mark_message_read() { - throw new coding_exception('message_mark_message_read() has been removed, please use \core_message\api::mark_message_as_read() - or \core_message\api::mark_notification_as_read().'); -} - -/** - * @deprecated since Moodle 3.5 - */ -function message_can_delete_message() { - throw new coding_exception( - 'message_can_delete_message() has been removed, please use \core_message\api::can_delete_message() instead.' - ); -} - -/** - * @deprecated since Moodle 3.5 - */ -function message_delete_message() { - throw new coding_exception( - 'message_delete_message() has been removed, please use \core_message\api::delete_message() instead.' - ); -} - -/** - * @deprecated since 3.6 - */ -function calendar_get_all_allowed_types() { - throw new coding_exception( - 'calendar_get_all_allowed_types() has been removed. Please use calendar_get_allowed_types() instead.' - ); - -} - -/** - * @deprecated since Moodle 3.6. - */ -function groups_get_all_groups_for_courses() { - throw new coding_exception( - 'groups_get_all_groups_for_courses() has been removed and can not be used anymore.' - ); -} - -/** - * @deprecated since Moodle 3.6. Please use the Events 2 API. - */ -function events_get_cached() { - throw new coding_exception( - 'Events API using $handlers array has been removed in favour of Events 2 API, please use it instead.' - ); -} - -/** - * @deprecated since Moodle 3.6. Please use the Events 2 API. - */ -function events_uninstall() { - throw new coding_exception( - 'Events API using $handlers array has been removed in favour of Events 2 API, please use it instead.' - ); -} - -/** - * @deprecated since Moodle 3.6. Please use the Events 2 API. - */ -function events_cleanup() { - throw new coding_exception( - 'Events API using $handlers array has been removed in favour of Events 2 API, please use it instead.' - ); -} - -/** - * @deprecated since Moodle 3.6. Please use the Events 2 API. - */ -function events_dequeue() { - throw new coding_exception( - 'Events API using $handlers array has been removed in favour of Events 2 API, please use it instead.' - ); -} - -/** - * @deprecated since Moodle 3.6. Please use the Events 2 API. - */ -function events_get_handlers() { - throw new coding_exception( - 'Events API using $handlers array has been removed in favour of Events 2 API, please use it instead.' - ); -} - -/** - * @deprecated since Moodle 3.6. Please use the get_roles_used_in_context(). - */ -function get_roles_on_exact_context() { - throw new coding_exception( - 'get_roles_on_exact_context() has been removed, please use get_roles_used_in_context() instead.' - ); -} - -/** - * @deprecated since Moodle 3.6. Please use the get_roles_used_in_context(). - */ -function get_roles_with_assignment_on_context() { - throw new coding_exception( - 'get_roles_with_assignment_on_context() has been removed, please use get_roles_used_in_context() instead.' - ); -} - -/** - * @deprecated since Moodle 3.6 - */ -function message_add_contact() { - throw new coding_exception( - 'message_add_contact() has been removed. Please use \core_message\api::create_contact_request() instead. ' . - 'If you wish to block or unblock a user please use \core_message\api::is_blocked() and ' . - '\core_message\api::block_user() or \core_message\api::unblock_user() respectively.' - ); -} - -/** - * @deprecated since Moodle 3.6 - */ -function message_remove_contact() { - throw new coding_exception( - 'message_remove_contact() has been removed. Please use \core_message\api::remove_contact() instead.' - ); -} - -/** - * @deprecated since Moodle 3.6 - */ -function message_unblock_contact() { - throw new coding_exception( - 'message_unblock_contact() has been removed. Please use \core_message\api::unblock_user() instead.' - ); -} - -/** - * @deprecated since Moodle 3.6 - */ -function message_block_contact() { - throw new coding_exception( - 'message_block_contact() has been removed. Please use \core_message\api::is_blocked() and ' . - '\core_message\api::block_user() instead.' - ); -} - -/** - * @deprecated since Moodle 3.6 - */ -function message_get_contact() { - throw new coding_exception( - 'message_get_contact() has been removed. Please use \core_message\api::get_contact() instead.' - ); -} - -/** - * @deprecated since Moodle 3.7 - */ -function get_courses_page() { - throw new coding_exception( - 'Function get_courses_page() has been removed. Please use core_course_category::get_courses() ' . - 'or core_course_category::search_courses()' - ); -} - -/** - * @deprecated since Moodle 3.8 - */ -function report_insights_context_insights(\context $context) { - throw new coding_exception( - 'Function report_insights_context_insights() ' . - 'has been removed. Please use \core_analytics\manager::cached_models_with_insights instead' - ); -} - -/** - * @deprecated since 3.9 - */ -function get_module_metadata() { - throw new coding_exception( - 'get_module_metadata() has been removed. Please use \core_course\local\service\content_item_service instead.'); -} - -/** - * @deprecated since Moodle 3.9 MDL-63580. Please use the \core\task\manager::run_from_cli($task). - */ -function cron_run_single_task() { - throw new coding_exception( - 'cron_run_single_task() has been removed. Please use \\core\task\manager::run_from_cli() instead.' - ); -} - -/** - * @deprecated since Moodle 3.9 MDL-52846. Please use new task API. - */ -function cron_execute_plugin_type() { - throw new coding_exception( - 'cron_execute_plugin_type() has been removed. Please, use the Task API instead: ' . - 'https://moodledev.io/docs/apis/subsystems/task.' - ); -} - -/** - * @deprecated since Moodle 3.9 MDL-52846. Please use new task API. - */ -function cron_bc_hack_plugin_functions() { - throw new coding_exception( - 'cron_bc_hack_plugin_functions() has been removed. Please, use the Task API instead: ' . - 'https://moodledev.io/docs/apis/subsystems/task.' - ); -} - -/** - * @deprecated since Moodle 3.9 MDL-68612 - See \core_user\table\participants_search for an improved way to fetch participants. - */ -function user_get_participants_sql() { - $deprecatedtext = __FUNCTION__ . '() has been removed. ' . - 'Please use \core\table\participants_search::class with table filtersets instead.'; - throw new coding_exception($deprecatedtext); -} - -/** - * @deprecated since Moodle 3.9 MDL-68612 - See \core_user\table\participants_search for an improved way to fetch participants. - */ -function user_get_total_participants() { - $deprecatedtext = __FUNCTION__ . '() has been removed. ' . - 'Please use \core\table\participants_search::class with table filtersets instead.'; - throw new coding_exception($deprecatedtext); -} - -/** - * @deprecated since Moodle 3.9 MDL-68612 - See \core_user\table\participants_search for an improved way to fetch participants. - */ -function user_get_participants() { - $deprecatedtext = __FUNCTION__ . '() has been removed. ' . - 'Please use \core\table\participants_search::class with table filtersets instead.'; - throw new coding_exception($deprecatedtext); -} - -/** - * @deprecated Since Moodle 3.9. MDL-65835 - */ -function plagiarism_save_form_elements() { - throw new coding_exception( - 'Function plagiarism_save_form_elements() has been removed. ' . - 'Please use {plugin name}_coursemodule_edit_post_actions() instead.' - ); -} - -/** - * @deprecated Since Moodle 3.9. MDL-65835 - */ -function plagiarism_get_form_elements_module() { - throw new coding_exception( - 'Function plagiarism_get_form_elements_module() has been removed. ' . - 'Please use {plugin name}_coursemodule_standard_elements() instead.' - ); -} - -/** - * @deprecated Since Moodle 3.9 - MDL-68500 please use {@see \core\dataformat::download_data} - */ -function download_as_dataformat() { - throw new coding_exception(__FUNCTION__ . '() has been removed, please use \core\dataformat::download_data() instead'); -} - -/** - * @deprecated since Moodle 3.10 - */ -function make_categories_options() { - throw new coding_exception(__FUNCTION__ . '() has been removed. ' . - 'Please use \core_course_category::make_categories_list() instead.'); -} - -/** - * @deprecated since 3.10 - */ -function message_count_unread_messages() { - throw new coding_exception('message_count_unread_messages has been removed.'); -} - -/** - * @deprecated since 3.10 - */ -function serialise_tool_proxy() { - throw new coding_exception('serialise_tool_proxy has been removed.'); -} - -/** - * @deprecated Since Moodle 3.11. - */ -function badges_check_backpack_accessibility() { - throw new coding_exception('badges_check_backpack_accessibility() can not be used any more, it was only used for OBv1.0'); -} - -/** - * @deprecated Since Moodle 3.11. - */ -function badges_setup_backpack_js() { - throw new coding_exception('badges_setup_backpack_js() can not be used any more, it was only used for OBv1.0'); -} - -/** - * @deprecated Since Moodle 3.11. - */ -function badges_local_backpack_js() { - throw new coding_exception('badges_local_backpack_js() can not be used any more, it was only used for OBv1.0'); -} - -/** - * @deprecated since Moodle 3.11 MDL-45242 - */ -function get_extra_user_fields() { - throw new coding_exception('get_extra_user_fields() has been removed. Please use the \core_user\fields API instead.'); -} - -/** - * @deprecated since Moodle 3.11 MDL-45242 - */ -function get_extra_user_fields_sql() { - throw new coding_exception('get_extra_user_fields_sql() has been removed. Please use the \core_user\fields API instead.'); -} - -/** - * @deprecated since Moodle 3.11 MDL-45242 - */ -function get_user_field_name() { - throw new coding_exception('get_user_field_name() has been removed. Please use \core_user\fields::get_display_name() instead'); -} - -/** - * @deprecated since Moodle 3.11 MDL-45242 - */ -function get_all_user_name_fields() { - throw new coding_exception('get_all_user_name_fields() is deprecated. Please use the \core_user\fields API instead'); -} - -/** - * @deprecated since Moodle 3.11 MDL-71051 - */ -function profile_display_fields() { - throw new coding_exception(__FUNCTION__ . '() has been removed.'); -} - -/** - * @deprecated since Moodle 3.11 MDL-71051 - */ -function profile_edit_category() { - throw new coding_exception(__FUNCTION__ . '() has been removed.'); -} - -/** - * @deprecated since Moodle 3.11 MDL-71051 - */ -function profile_edit_field() { - throw new coding_exception(__FUNCTION__ . '() has been removed.'); -} - -/** - * @deprecated since Moodle 4.0 MDL-71953 - */ -function calendar_process_subscription_row() { - throw new coding_exception(__FUNCTION__ . '() has been removed.'); -} - -/** - * @deprecated since Moodle 4.0 MDL-71953 - */ -function calendar_import_icalendar_events() { - throw new coding_exception(__FUNCTION__ . '() has been removed. Please use calendar_import_events_from_ical() instead.'); -} - -/** - * @deprecated since Moodle 4.0. Tabs navigation has been replaced with tertiary navigation. - */ -function grade_print_tabs() { - throw new coding_exception(__FUNCTION__ . '() has been removed.'); -} - -/** - * @deprecated since Moodle 4.0. Dropdown box navigation has been replaced with tertiary navigation. - */ -function print_grade_plugin_selector() { - throw new coding_exception(__FUNCTION__ . '() has been removed.'); -} - -/** - * @deprecated since Moodle 4.0. Please use {@link course_modinfo::purge_course_section_cache_by_id()} - * or {@link course_modinfo::purge_course_section_cache_by_number()} instead. - */ -function course_purge_section_cache() { - throw new coding_exception(__FUNCTION__ . '() has been removed. ' . - 'Please use course_modinfo::purge_course_section_cache_by_id() ' . - 'or course_modinfo::purge_course_section_cache_by_number() instead.'); -} - -/** - * @deprecated since Moodle 4.0. Please use {@link course_modinfo::purge_course_module_cache()} instead. - */ -function course_purge_module_cache() { - throw new coding_exception(__FUNCTION__ . '() has been removed. ' . - 'Please use course_modinfo::purge_course_module_cache() instead.'); -} - -/** - * @deprecated since Moodle 4.0. Please use {@link course_modinfo::get_array_of_activities()} instead. - */ -function get_array_of_activities() { - throw new coding_exception(__FUNCTION__ . '() has been removed. ' . - 'Please use course_modinfo::get_array_of_activities() instead.'); -} - -/** - * @deprecated since Moodle 4.1 - */ -#[\core\attribute\deprecated( - 'Please throw a new moodle_exception instance instead.', - since: '4.1', - mdl: 'MDL-71062', - final: true -)] -function print_error(): void { - \core\deprecation::emit_deprecation_if_present([__FUNCTION__]); -} - /** * Execute cron tasks * From 67f348a340d0fb962864d180468f2a4cec51601e Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Wed, 26 Jun 2024 10:13:23 +0700 Subject: [PATCH 006/178] MDL-78936 editor_tiny: Disable quickbars for small target The quickbars selection toolbar is not displayed correctly if the target element is too small. We will wait for it to be fixed in the future before we re-enable it in Moodle --- lib/editor/tiny/amd/build/editor.min.js | 2 +- lib/editor/tiny/amd/build/editor.min.js.map | 2 +- lib/editor/tiny/amd/src/editor.js | 13 ++++++++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/editor/tiny/amd/build/editor.min.js b/lib/editor/tiny/amd/build/editor.min.js index a9ed7703488a4..4978279163bb6 100644 --- a/lib/editor/tiny/amd/build/editor.min.js +++ b/lib/editor/tiny/amd/build/editor.min.js @@ -1,3 +1,3 @@ -define("editor_tiny/editor",["exports","jquery","core/pending","./defaults","./loader","./options","./utils"],(function(_exports,_jquery,_pending,_defaults,_loader,Options,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setupForTarget=_exports.setupForElementId=_exports.getInstanceForElementId=_exports.getInstanceForElement=_exports.getAllInstances=_exports.configureDefaultEditor=void 0,_jquery=_interopRequireDefault(_jquery),_pending=_interopRequireDefault(_pending),Options=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}(Options);var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};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}}const instanceMap=new Map;let defaultOptions={};const importPluginList=async pluginList=>{const pluginHandlers=await Promise.all(pluginList.map((pluginPath=>-1===pluginPath.indexOf("/")?Promise.resolve(pluginPath):"function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require([pluginPath],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require(pluginPath)):Promise.resolve(_systemImportTransformerGlobalIdentifier[pluginPath])))),pluginNames=pluginHandlers.map((pluginConfig=>"string"==typeof pluginConfig?pluginConfig:Array.isArray(pluginConfig)?pluginConfig[0]:null)).filter((value=>value));return{pluginNames:pluginNames,pluginConfig:pluginHandlers.map((pluginConfig=>Array.isArray(pluginConfig)?pluginConfig[1]:null)).filter((value=>value))}};_exports.getAllInstances=()=>new Map(instanceMap.entries());_exports.getInstanceForElementId=elementId=>getInstanceForElement(document.getElementById(elementId));const getInstanceForElement=element=>{const instance=instanceMap.get(element);if(!instance||!instance.removed)return instance;instanceMap.delete(element)};_exports.getInstanceForElement=getInstanceForElement;_exports.setupForElementId=_ref=>{let{elementId:elementId,options:options}=_ref;const target=document.getElementById(elementId);setTimeout((()=>setupForTarget(target,options)),1)};(async()=>{const lang=document.querySelector("html").lang,[tinyMCE,langData]=await Promise.all([(0,_loader.getTinyMCE)(),(language=lang,fetch("".concat(M.cfg.wwwroot,"/lib/editor/tiny/lang.php/").concat(M.cfg.langrev,"/").concat(language)).then((response=>response.json())))]);var language;tinyMCE.addI18n(lang,langData)})();const getPlugins=function(){let{plugins:plugins=null}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return plugins||(defaultOptions.plugins?defaultOptions.plugins:{})},getStandardConfig=(target,tinyMCE,options,plugins)=>{const lang=document.querySelector("html").lang,config=Object.assign({},(0,_defaults.getDefaultConfiguration)(),{base_url:_loader.baseUrl,target:target,min_height:175,height:target.clientHeight||"auto",language:lang,content_css:[options.css],convert_urls:!1,a11y_advanced_options:!0,extended_valid_elements:"script[*],p[*],i[*]",xss_sanitization:!1,quickbars_insert_toolbar:"",block_formats:"Paragraph=p;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6;Preformatted=pre",plugins:[...plugins],skin:"oxide",help_accessibility:!1,promotion:!1,branding:options.branding,table_header_type:"sectionCells",entity_encoding:"raw",ui_mode:"split",browser_spellcheck:!0,setup:editor=>{Options.register(editor,options),editor.on("PreInit",(function(){this.contentWindow=this.iframeElement.contentWindow})),editor.on("init",(function(){(0,_utils.removeSubmenuItem)(editor,"align","tiny:justify"),((editor,target)=>{let expectedEditingAreaHeight=0;expectedEditingAreaHeight=target.clientHeight?target.clientHeight:target.rows*(parseFloat(window.getComputedStyle(target).lineHeight)||22),editor.getContainer().querySelector(".tox-sidebar-wrap").clientHeight{const{pluginNames:pluginNames,pluginConfig:pluginConfig}=pluginValues,instanceConfig=getStandardConfig(target,0,options,pluginNames);return instanceConfig.menu.file&&(instanceConfig.menu.file.items=""),instanceConfig.menu.format&&(instanceConfig.menu.format.items=instanceConfig.menu.format.items.replace(/forecolor ?/,"").replace(/backcolor ?/,"").replace(/fontfamily ?/,"").replace(/fontsize ?/,"").replace(/styles ?/,"").replaceAll(/\| *\|/g,"|")),instanceConfig.quickbars_selection_toolbar=instanceConfig.quickbars_selection_toolbar.replace("h2 h3","h3 h4 h5 h6"),pluginConfig.filter((pluginConfig=>"function"==typeof pluginConfig.configure)).forEach((pluginConfig=>{const pluginInstanceOverride=pluginConfig.configure(instanceConfig,options);Object.assign(instanceConfig,pluginInstanceOverride)})),Object.assign(instanceConfig,Options.getInitialPluginConfiguration(options)),instanceConfig},isModalMode=target=>!!target.closest('[data-region="modal"]'),setupForTarget=async function(target){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const instance=getInstanceForElement(target);if(instance)return Promise.resolve(instance);const pendingPromise=new _pending.default("editor_tiny/editor:setupForTarget"),plugins=getPlugins(options),[tinyMCE,pluginValues]=await Promise.all([(0,_loader.getTinyMCE)(),importPluginList(Object.keys(plugins))]);tinyMCE.get().filter((editor=>!editor.getElement().isConnected)).forEach((editor=>{editor.remove()}));const existingEditor=tinyMCE.EditorManager.get(target.id);if(existingEditor){if(existingEditor.getElement()===target)return pendingPromise.resolve(),Promise.resolve(existingEditor);throw pendingPromise.resolve(),new Error("TinyMCE instance already exists for different target with same ID")}const instanceConfig=getEditorConfiguration(target,0,options,pluginValues),[editor]=await tinyMCE.init(instanceConfig);return target.dataset.fieldtype="editor",instanceMap.set(target,editor),editor.on("remove",(_ref2=>{let{target:target}=_ref2;instanceMap.delete(target.targetElm),target.targetElm.dataset.fieldtype=null})),target.form&&(0,_jquery.default)(target.form).on("submit",(()=>{editor.save()})),editor.on("blur",(()=>{editor.save()})),editor.on("OpenWindow",(()=>{const modals=document.querySelectorAll('[data-region="modal"]');modals&&modals.forEach((modal=>{modal.classList.contains("hide")||modal.classList.add("hide")}))})),editor.on("CloseWindow",(()=>{if(isModalMode(target)){const modals=document.querySelectorAll('[data-region="modal"]');modals&&modals.forEach((modal=>{modal.classList.contains("hide")&&modal.classList.remove("hide")}))}})),pendingPromise.resolve(),editor};_exports.setupForTarget=setupForTarget;_exports.configureDefaultEditor=function(){let options=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};defaultOptions=options}})); +define("editor_tiny/editor",["exports","jquery","core/pending","./defaults","./loader","./options","./utils"],(function(_exports,_jquery,_pending,_defaults,_loader,Options,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setupForTarget=_exports.setupForElementId=_exports.getInstanceForElementId=_exports.getInstanceForElement=_exports.getAllInstances=_exports.configureDefaultEditor=void 0,_jquery=_interopRequireDefault(_jquery),_pending=_interopRequireDefault(_pending),Options=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}(Options);var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};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}}const instanceMap=new Map;let defaultOptions={};const importPluginList=async pluginList=>{const pluginHandlers=await Promise.all(pluginList.map((pluginPath=>-1===pluginPath.indexOf("/")?Promise.resolve(pluginPath):"function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require([pluginPath],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require(pluginPath)):Promise.resolve(_systemImportTransformerGlobalIdentifier[pluginPath])))),pluginNames=pluginHandlers.map((pluginConfig=>"string"==typeof pluginConfig?pluginConfig:Array.isArray(pluginConfig)?pluginConfig[0]:null)).filter((value=>value));return{pluginNames:pluginNames,pluginConfig:pluginHandlers.map((pluginConfig=>Array.isArray(pluginConfig)?pluginConfig[1]:null)).filter((value=>value))}};_exports.getAllInstances=()=>new Map(instanceMap.entries());_exports.getInstanceForElementId=elementId=>getInstanceForElement(document.getElementById(elementId));const getInstanceForElement=element=>{const instance=instanceMap.get(element);if(!instance||!instance.removed)return instance;instanceMap.delete(element)};_exports.getInstanceForElement=getInstanceForElement;_exports.setupForElementId=_ref=>{let{elementId:elementId,options:options}=_ref;const target=document.getElementById(elementId);setTimeout((()=>setupForTarget(target,options)),1)};(async()=>{const lang=document.querySelector("html").lang,[tinyMCE,langData]=await Promise.all([(0,_loader.getTinyMCE)(),(language=lang,fetch("".concat(M.cfg.wwwroot,"/lib/editor/tiny/lang.php/").concat(M.cfg.langrev,"/").concat(language)).then((response=>response.json())))]);var language;tinyMCE.addI18n(lang,langData)})();const getPlugins=function(){let{plugins:plugins=null}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return plugins||(defaultOptions.plugins?defaultOptions.plugins:{})},getStandardConfig=(target,tinyMCE,options,plugins)=>{const lang=document.querySelector("html").lang,config=Object.assign({},(0,_defaults.getDefaultConfiguration)(),{base_url:_loader.baseUrl,target:target,min_height:175,height:target.clientHeight||"auto",language:lang,content_css:[options.css],convert_urls:!1,a11y_advanced_options:!0,extended_valid_elements:"script[*],p[*],i[*]",xss_sanitization:!1,quickbars_insert_toolbar:"",quickbars_selection_toolbar:target.rows>5&&(0,_defaults.getDefaultQuickbarsSelectionToolbar)(),block_formats:"Paragraph=p;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6;Preformatted=pre",plugins:[...plugins],skin:"oxide",help_accessibility:!1,promotion:!1,branding:options.branding,table_header_type:"sectionCells",entity_encoding:"raw",ui_mode:"split",browser_spellcheck:!0,setup:editor=>{Options.register(editor,options),editor.on("PreInit",(function(){this.contentWindow=this.iframeElement.contentWindow})),editor.on("init",(function(){(0,_utils.removeSubmenuItem)(editor,"align","tiny:justify"),((editor,target)=>{let expectedEditingAreaHeight=0;expectedEditingAreaHeight=target.clientHeight?target.clientHeight:target.rows*(parseFloat(window.getComputedStyle(target).lineHeight)||22),editor.getContainer().querySelector(".tox-sidebar-wrap").clientHeight{const{pluginNames:pluginNames,pluginConfig:pluginConfig}=pluginValues,instanceConfig=getStandardConfig(target,0,options,pluginNames);return instanceConfig.menu.file&&(instanceConfig.menu.file.items=""),instanceConfig.menu.format&&(instanceConfig.menu.format.items=instanceConfig.menu.format.items.replace(/forecolor ?/,"").replace(/backcolor ?/,"").replace(/fontfamily ?/,"").replace(/fontsize ?/,"").replace(/styles ?/,"").replaceAll(/\| *\|/g,"|")),!1!==instanceConfig.quickbars_selection_toolbar&&(instanceConfig.quickbars_selection_toolbar=instanceConfig.quickbars_selection_toolbar.replace("h2 h3","h3 h4 h5 h6")),pluginConfig.filter((pluginConfig=>"function"==typeof pluginConfig.configure)).forEach((pluginConfig=>{const pluginInstanceOverride=pluginConfig.configure(instanceConfig,options);Object.assign(instanceConfig,pluginInstanceOverride)})),Object.assign(instanceConfig,Options.getInitialPluginConfiguration(options)),instanceConfig},isModalMode=target=>!!target.closest('[data-region="modal"]'),setupForTarget=async function(target){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const instance=getInstanceForElement(target);if(instance)return Promise.resolve(instance);const pendingPromise=new _pending.default("editor_tiny/editor:setupForTarget"),plugins=getPlugins(options),[tinyMCE,pluginValues]=await Promise.all([(0,_loader.getTinyMCE)(),importPluginList(Object.keys(plugins))]);tinyMCE.get().filter((editor=>!editor.getElement().isConnected)).forEach((editor=>{editor.remove()}));const existingEditor=tinyMCE.EditorManager.get(target.id);if(existingEditor){if(existingEditor.getElement()===target)return pendingPromise.resolve(),Promise.resolve(existingEditor);throw pendingPromise.resolve(),new Error("TinyMCE instance already exists for different target with same ID")}const instanceConfig=getEditorConfiguration(target,0,options,pluginValues),[editor]=await tinyMCE.init(instanceConfig);return target.dataset.fieldtype="editor",instanceMap.set(target,editor),editor.on("remove",(_ref2=>{let{target:target}=_ref2;instanceMap.delete(target.targetElm),target.targetElm.dataset.fieldtype=null})),target.form&&(0,_jquery.default)(target.form).on("submit",(()=>{editor.save()})),editor.on("blur",(()=>{editor.save()})),editor.on("OpenWindow",(()=>{const modals=document.querySelectorAll('[data-region="modal"]');modals&&modals.forEach((modal=>{modal.classList.contains("hide")||modal.classList.add("hide")}))})),editor.on("CloseWindow",(()=>{if(isModalMode(target)){const modals=document.querySelectorAll('[data-region="modal"]');modals&&modals.forEach((modal=>{modal.classList.contains("hide")&&modal.classList.remove("hide")}))}})),pendingPromise.resolve(),editor};_exports.setupForTarget=setupForTarget;_exports.configureDefaultEditor=function(){let options=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};defaultOptions=options}})); //# sourceMappingURL=editor.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/amd/build/editor.min.js.map b/lib/editor/tiny/amd/build/editor.min.js.map index 1fead5f87e187..7e7b26db133a1 100644 --- a/lib/editor/tiny/amd/build/editor.min.js.map +++ b/lib/editor/tiny/amd/build/editor.min.js.map @@ -1 +1 @@ -{"version":3,"file":"editor.min.js","sources":["../src/editor.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 * TinyMCE Editor Manager.\n *\n * @module editor_tiny/editor\n * @copyright 2022 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport jQuery from 'jquery';\nimport Pending from 'core/pending';\nimport {getDefaultConfiguration} from './defaults';\nimport {getTinyMCE, baseUrl} from './loader';\nimport * as Options from './options';\nimport {addToolbarButton, addToolbarButtons, addToolbarSection,\n removeToolbarButton, removeSubmenuItem, updateEditorState} from './utils';\n\n/**\n * Storage for the TinyMCE instances on the page.\n * @type {Map}\n */\nconst instanceMap = new Map();\n\n/**\n * The default editor configuration.\n * @type {Object}\n */\nlet defaultOptions = {};\n\n/**\n * Require the modules for the named set of TinyMCE plugins.\n *\n * @param {string[]} pluginList The list of plugins\n * @return {Promise[]} A matching set of Promises relating to the requested plugins\n */\nconst importPluginList = async(pluginList) => {\n // Fetch all of the plugins from the list of plugins.\n // If a plugin contains a '/' then it is assumed to be a Moodle AMD module to import.\n const pluginHandlers = await Promise.all(pluginList.map(pluginPath => {\n if (pluginPath.indexOf('/') === -1) {\n // A standard TinyMCE Plugin.\n return Promise.resolve(pluginPath);\n }\n\n return import(pluginPath);\n }));\n\n // Normalise the plugin data to a list of plugin names.\n // Two formats are supported:\n // - a string; and\n // - an array whose first element is the plugin name, and the second element is the plugin configuration.\n const pluginNames = pluginHandlers.map((pluginConfig) => {\n if (typeof pluginConfig === 'string') {\n return pluginConfig;\n }\n if (Array.isArray(pluginConfig)) {\n return pluginConfig[0];\n }\n return null;\n }).filter((value) => value);\n\n // Fetch the list of pluginConfig handlers.\n const pluginConfig = pluginHandlers.map((pluginConfig) => {\n if (Array.isArray(pluginConfig)) {\n return pluginConfig[1];\n }\n return null;\n }).filter((value) => value);\n\n return {\n pluginNames,\n pluginConfig,\n };\n};\n\n/**\n * Fetch the language data for the specified language.\n *\n * @param {string} language The language identifier\n * @returns {object}\n */\nconst fetchLanguage = (language) => fetch(\n `${M.cfg.wwwroot}/lib/editor/tiny/lang.php/${M.cfg.langrev}/${language}`\n).then(response => response.json());\n\n/**\n * Get a list of all Editors in a Map, keyed by the DOM Node that the Editor is associated with.\n *\n * @returns {Map}\n */\nexport const getAllInstances = () => new Map(instanceMap.entries());\n\n/**\n * Get the TinyMCE instance for the specified Node ID.\n *\n * @param {string} elementId\n * @returns {TinyMCE|undefined}\n */\nexport const getInstanceForElementId = elementId => getInstanceForElement(document.getElementById(elementId));\n\n/*\n * Get the TinyMCE instance for the specified HTMLElement.\n *\n * @param {HTMLElement} element\n * @returns {TinyMCE|undefined}\n */\nexport const getInstanceForElement = element => {\n const instance = instanceMap.get(element);\n if (instance && instance.removed) {\n instanceMap.delete(element);\n return undefined;\n }\n return instance;\n};\n\n/**\n * Set up TinyMCE for the selector at the specified HTML Node id.\n *\n * @param {object} config The configuration required to setup the editor\n * @param {string} config.elementId The HTML Node ID\n * @param {Object} config.options The editor plugin configuration\n */\nexport const setupForElementId = ({elementId, options}) => {\n const target = document.getElementById(elementId);\n // We will need to wrap the setupForTarget and editor.remove() calls in a setTimeout.\n // Because other events callbacks will still try to run on the removed instance.\n // This will cause an error on Firefox.\n // We need to make TinyMCE to remove itself outside the event loop.\n // @see https://github.com/tinymce/tinymce/issues/3129 for more details.\n setTimeout(() => {\n return setupForTarget(target, options);\n }, 1);\n};\n\n/**\n * Initialise the page with standard TinyMCE requirements.\n *\n * Currently this includes the language taken from the HTML lang property.\n */\nconst initialisePage = async() => {\n const lang = document.querySelector('html').lang;\n\n const [tinyMCE, langData] = await Promise.all([getTinyMCE(), fetchLanguage(lang)]);\n tinyMCE.addI18n(lang, langData);\n};\ninitialisePage();\n\n/**\n * Get the list of plugins to load for the specified configuration.\n *\n * If the specified configuration does not include a plugin configuration, then return the default configuration.\n *\n * @param {object} options\n * @param {array} [options.plugins=null] The plugin list\n * @returns {object}\n */\nconst getPlugins = ({plugins = null} = {}) => {\n if (plugins) {\n return plugins;\n }\n\n if (defaultOptions.plugins) {\n return defaultOptions.plugins;\n }\n\n return {};\n};\n\n/**\n * Adjust the editor size base on the target element.\n *\n * @param {TinyMCE} editor TinyMCE editor\n * @param {Node} target Target element\n */\nconst adjustEditorSize = (editor, target) => {\n let expectedEditingAreaHeight = 0;\n if (target.clientHeight) {\n expectedEditingAreaHeight = target.clientHeight;\n } else {\n // If the target element is hidden, we cannot get the lineHeight of the target element.\n // We don't have a proper way to retrieve the general lineHeight of the theme, so we use 22 here, it's equivalent to 1.5em.\n expectedEditingAreaHeight = target.rows * (parseFloat(window.getComputedStyle(target).lineHeight) || 22);\n }\n const currentEditingAreaHeight = editor.getContainer().querySelector('.tox-sidebar-wrap').clientHeight;\n if (currentEditingAreaHeight < expectedEditingAreaHeight) {\n // Change the height based on the target element's height.\n editor.getContainer().querySelector('.tox-sidebar-wrap').style.height = `${expectedEditingAreaHeight}px`;\n }\n};\n\n/**\n * Get the standard configuration for the specified options.\n *\n * @param {Node} target\n * @param {tinyMCE} tinyMCE\n * @param {object} options\n * @param {Array} plugins\n * @returns {object}\n */\nconst getStandardConfig = (target, tinyMCE, options, plugins) => {\n const lang = document.querySelector('html').lang;\n\n const config = Object.assign({}, getDefaultConfiguration(), {\n // eslint-disable-next-line camelcase\n base_url: baseUrl,\n\n // Set the editor target.\n // https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#target\n target,\n\n // https://www.tiny.cloud/docs/tinymce/6/customize-ui/#set-maximum-and-minimum-heights-and-widths\n // Set the minimum height to the smallest height that we can fit the Menu bar, Tool bar, Status bar and the text area.\n // eslint-disable-next-line camelcase\n min_height: 175,\n\n // Base the height on the size of the text area.\n // In some cases, E.g.: The target is an advanced element, it will be hidden. We cannot get the height at this time.\n // So set the height to auto, and adjust it later by adjustEditorSize().\n height: target.clientHeight || 'auto',\n\n // Set the language.\n // https://www.tiny.cloud/docs/tinymce/6/ui-localization/#language\n // eslint-disable-next-line camelcase\n language: lang,\n\n // Load the editor stylesheet into the editor iframe.\n // https://www.tiny.cloud/docs/tinymce/6/add-css-options/\n // eslint-disable-next-line camelcase\n content_css: [\n options.css,\n ],\n\n // Do not convert URLs to relative URLs.\n // https://www.tiny.cloud/docs/tinymce/6/url-handling/#convert_urls\n // eslint-disable-next-line camelcase\n convert_urls: false,\n\n // Enabled 'advanced' a11y options.\n // This includes allowing role=\"presentation\" from the image uploader.\n // https://www.tiny.cloud/docs/tinymce/6/accessibility/\n // eslint-disable-next-line camelcase\n a11y_advanced_options: true,\n\n // Add specific rules to the valid elements.\n // eslint-disable-next-line camelcase\n extended_valid_elements: 'script[*],p[*],i[*]',\n\n // Disable XSS Sanitisation.\n // We do this in PHP.\n // https://www.tiny.cloud/docs/tinymce/6/security/#turning-dompurify-off\n // Note: This feature has been backported from TinyMCE 6.4.0.\n // eslint-disable-next-line camelcase\n xss_sanitization: false,\n\n // Disable quickbars entirely.\n // The UI is not ideal and we'll wait for it to improve in future before we enable it in Moodle.\n // eslint-disable-next-line camelcase\n quickbars_insert_toolbar: '',\n\n // Override the standard block formats property (removing h1 & h2).\n // https://www.tiny.cloud/docs/tinymce/6/user-formatting-options/#block_formats\n // eslint-disable-next-line camelcase\n block_formats: 'Paragraph=p;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6;Preformatted=pre',\n\n // The list of plugins to include in the instance.\n // https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#plugins\n plugins: [\n ...plugins,\n ],\n\n // Skins\n skin: 'oxide',\n\n // Do not show the help link in the status bar.\n // https://www.tiny.cloud/docs/tinymce/latest/accessibility/#help_accessibility\n // eslint-disable-next-line camelcase\n help_accessibility: false,\n\n // Remove the \"Upgrade\" link for Tiny.\n // https://www.tiny.cloud/docs/tinymce/6/editor-premium-upgrade-promotion/\n promotion: false,\n\n // Allow the administrator to disable branding.\n // https://www.tiny.cloud/docs/tinymce/6/statusbar-configuration-options/#branding\n branding: options.branding,\n\n // Put th cells in a thead element.\n // https://www.tiny.cloud/docs/tinymce/6/table-options/#table_header_type\n // eslint-disable-next-line camelcase\n table_header_type: 'sectionCells',\n\n // Stored text in non-entity form.\n // https://www.tiny.cloud/docs/tinymce/6/content-filtering/#entity_encoding\n // eslint-disable-next-line camelcase\n entity_encoding: \"raw\",\n\n // Enable support for editors in scrollable containers.\n // https://www.tiny.cloud/docs/tinymce/6/ui-mode-configuration-options/#ui_mode\n // eslint-disable-next-line camelcase\n ui_mode: 'split',\n\n // Enable browser-supported spell checking.\n // https://www.tiny.cloud/docs/tinymce/latest/spelling/\n // eslint-disable-next-line camelcase\n browser_spellcheck: true,\n\n setup: (editor) => {\n Options.register(editor, options);\n\n editor.on('PreInit', function() {\n // Work around a bug in TinyMCE with Firefox.\n // When an editor is removed, and replaced with an identically attributed editor (same ID),\n // and the Firefox window is freshly opened (e.g. Behat, Private browsing), the wrong contentWindow\n // is assigned to the editor instance leading to an NS_ERROR_UNEXPECTED error in Firefox.\n // This is a workaround for that issue.\n this.contentWindow = this.iframeElement.contentWindow;\n });\n editor.on('init', function() {\n // Hide justify alignment sub-menu.\n removeSubmenuItem(editor, 'align', 'tiny:justify');\n // Adjust the editor size.\n adjustEditorSize(editor, target);\n });\n\n target.addEventListener('form:editorUpdated', function() {\n updateEditorState(editor, target);\n });\n\n target.dispatchEvent(new Event('form:editorUpdated'));\n },\n });\n\n config.toolbar = addToolbarSection(config.toolbar, 'content', 'formatting', true);\n config.toolbar = addToolbarButton(config.toolbar, 'content', 'link');\n\n // Add directionality plugins, always.\n config.toolbar = addToolbarSection(config.toolbar, 'directionality', 'alignment', true);\n config.toolbar = addToolbarButtons(config.toolbar, 'directionality', ['ltr', 'rtl']);\n\n // Remove the align justify button from the toolbar.\n config.toolbar = removeToolbarButton(config.toolbar, 'alignment', 'alignjustify');\n\n return config;\n};\n\n/**\n * Fetch the TinyMCE configuration for this editor instance.\n *\n * @param {HTMLElement} target\n * @param {TinyMCE} tinyMCE The TinyMCE API\n * @param {Object} options The editor plugin configuration\n * @param {object} pluginValues\n * @param {object} pluginValues.pluginConfig The list of plugin configuration\n * @param {object} pluginValues.pluginNames The list of plugins to load\n * @returns {object} The TinyMCE Configuration\n */\nconst getEditorConfiguration = (target, tinyMCE, options, pluginValues) => {\n const {\n pluginNames,\n pluginConfig,\n } = pluginValues;\n\n // Allow plugins to modify the configuration.\n // This seems a little strange, but we must double-process the config slightly.\n\n // First we fetch the standard configuration.\n const instanceConfig = getStandardConfig(target, tinyMCE, options, pluginNames);\n\n // Next we make any standard changes.\n // Here we remove the file menu, as it doesn't offer any useful functionality.\n // We only empty the items list so that a plugin may choose to add to it themselves later if they wish.\n if (instanceConfig.menu.file) {\n instanceConfig.menu.file.items = '';\n }\n\n // We disable the styles, backcolor, and forecolor plugins from the format menu.\n // These are not useful for Moodle and we don't want to encourage their use.\n if (instanceConfig.menu.format) {\n instanceConfig.menu.format.items = instanceConfig.menu.format.items\n // Remove forecolor and backcolor.\n .replace(/forecolor ?/, '')\n .replace(/backcolor ?/, '')\n\n // Remove fontfamily for now.\n .replace(/fontfamily ?/, '')\n\n // Remove fontsize for now.\n .replace(/fontsize ?/, '')\n\n // Remove styles - it just duplicates the format menu in a way which does not respect configuration\n .replace(/styles ?/, '')\n\n // Remove any duplicate separators.\n .replaceAll(/\\| *\\|/g, '|');\n }\n\n // eslint-disable-next-line camelcase\n instanceConfig.quickbars_selection_toolbar = instanceConfig.quickbars_selection_toolbar.replace('h2 h3', 'h3 h4 h5 h6');\n\n // Next we call the `configure` function for any plugin which defines it.\n // We pass the current instanceConfig in here, to allow them to make certain changes to the global configuration.\n // For example, to add themselves to any menu, toolbar, and so on.\n // Any plugin which wishes to have configuration options must register those options here.\n pluginConfig.filter((pluginConfig) => typeof pluginConfig.configure === 'function').forEach((pluginConfig) => {\n const pluginInstanceOverride = pluginConfig.configure(instanceConfig, options);\n Object.assign(instanceConfig, pluginInstanceOverride);\n });\n\n // Next we convert the plugin configuration into a format that TinyMCE understands.\n Object.assign(instanceConfig, Options.getInitialPluginConfiguration(options));\n\n return instanceConfig;\n};\n\n/**\n * Check if the target for TinyMCE is in a modal or not.\n *\n * @param {HTMLElement} target Target to check\n * @returns {boolean} True if the target is in a modal form.\n */\nconst isModalMode = (target) => {\n return !!target.closest('[data-region=\"modal\"]');\n};\n\n/**\n * Set up TinyMCE for the HTML Element.\n *\n * @param {HTMLElement} target\n * @param {Object} [options={}] The editor plugin configuration\n * @return {Promise} The TinyMCE instance\n */\nexport const setupForTarget = async(target, options = {}) => {\n const instance = getInstanceForElement(target);\n if (instance) {\n return Promise.resolve(instance);\n }\n\n // Register a new pending promise to ensure that Behat waits for the editor setup to complete before continuing.\n const pendingPromise = new Pending('editor_tiny/editor:setupForTarget');\n\n // Get the list of plugins.\n const plugins = getPlugins(options);\n\n // Fetch the tinyMCE API, and instantiate the plugins.\n const [tinyMCE, pluginValues] = await Promise.all([\n getTinyMCE(),\n importPluginList(Object.keys(plugins)),\n ]);\n\n // TinyMCE uses the element ID as a map key internally, even if the target has changed.\n // In the case where we have an editor in a modal form which has been detached from the DOM, but the editor not removed,\n // we need to manually destroy the editor.\n // We could theoretically do this with a Mutation Observer, but in some cases the Node may be moved,\n // or added back elsewhere in the DOM.\n\n // First remove any detached editors.\n tinyMCE.get().filter((editor) => !editor.getElement().isConnected).forEach((editor) => {\n editor.remove();\n });\n\n // Now check for any existing editor which shares the same ID.\n const existingEditor = tinyMCE.EditorManager.get(target.id);\n if (existingEditor) {\n if (existingEditor.getElement() === target) {\n pendingPromise.resolve();\n return Promise.resolve(existingEditor);\n } else {\n pendingPromise.resolve();\n throw new Error('TinyMCE instance already exists for different target with same ID');\n }\n }\n\n // Get the editor configuration for this editor.\n const instanceConfig = getEditorConfiguration(target, tinyMCE, options, pluginValues);\n\n // Initialise the editor instance for the given configuration.\n // At this point any plugin which has configuration options registered will have them applied for this instance.\n const [editor] = await tinyMCE.init(instanceConfig);\n\n // Update the textarea when the editor to set the field type for Behat.\n target.dataset.fieldtype = 'editor';\n\n // Store the editor instance in the instanceMap and register a listener on removal to remove it from the map.\n instanceMap.set(target, editor);\n editor.on('remove', ({target}) => {\n // Handle removal of the editor from the map on destruction.\n instanceMap.delete(target.targetElm);\n target.targetElm.dataset.fieldtype = null;\n });\n\n // If the editor is part of a form, also listen to the jQuery submit event.\n // The jQuery submit event will not trigger the native submit event, and therefore the content will not be saved.\n // We cannot rely on listening to the bubbled submit event on the document because other events on child nodes may\n // consume the data before it is saved.\n if (target.form) {\n jQuery(target.form).on('submit', () => {\n editor.save();\n });\n }\n\n // Save the editor content to the textarea when the editor is blurred.\n editor.on('blur', () => {\n editor.save();\n });\n\n // If the editor is in a modal, we need to hide the modal when window editor's window is opened.\n editor.on('OpenWindow', () => {\n const modals = document.querySelectorAll('[data-region=\"modal\"]');\n if (modals) {\n modals.forEach((modal) => {\n if (!modal.classList.contains('hide')) {\n modal.classList.add('hide');\n }\n });\n }\n });\n\n // If the editor's window is closed, we need to show the hidden modal back.\n editor.on('CloseWindow', () => {\n if (isModalMode(target)) {\n const modals = document.querySelectorAll('[data-region=\"modal\"]');\n if (modals) {\n modals.forEach((modal) => {\n if (modal.classList.contains('hide')) {\n modal.classList.remove('hide');\n }\n });\n }\n }\n });\n\n pendingPromise.resolve();\n return editor;\n};\n\n/**\n * Set the default editor configuration.\n *\n * This configuration is used when an editor is initialised without any configuration.\n *\n * @param {object} [options={}]\n */\nexport const configureDefaultEditor = (options = {}) => {\n defaultOptions = options;\n};\n"],"names":["instanceMap","Map","defaultOptions","importPluginList","async","pluginHandlers","Promise","all","pluginList","map","pluginPath","indexOf","resolve","pluginNames","pluginConfig","Array","isArray","filter","value","entries","elementId","getInstanceForElement","document","getElementById","element","instance","get","removed","delete","_ref","options","target","setTimeout","setupForTarget","lang","querySelector","tinyMCE","langData","language","fetch","M","cfg","wwwroot","langrev","then","response","json","addI18n","initialisePage","getPlugins","plugins","getStandardConfig","config","Object","assign","base_url","baseUrl","min_height","height","clientHeight","content_css","css","convert_urls","a11y_advanced_options","extended_valid_elements","xss_sanitization","quickbars_insert_toolbar","block_formats","skin","help_accessibility","promotion","branding","table_header_type","entity_encoding","ui_mode","browser_spellcheck","setup","editor","Options","register","on","contentWindow","this","iframeElement","expectedEditingAreaHeight","rows","parseFloat","window","getComputedStyle","lineHeight","getContainer","style","adjustEditorSize","addEventListener","dispatchEvent","Event","toolbar","getEditorConfiguration","pluginValues","instanceConfig","menu","file","items","format","replace","replaceAll","quickbars_selection_toolbar","configure","forEach","pluginInstanceOverride","getInitialPluginConfiguration","isModalMode","closest","pendingPromise","Pending","keys","getElement","isConnected","remove","existingEditor","EditorManager","id","Error","init","dataset","fieldtype","set","_ref2","targetElm","form","save","modals","querySelectorAll","modal","classList","contains","add"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"editor.min.js","sources":["../src/editor.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 * TinyMCE Editor Manager.\n *\n * @module editor_tiny/editor\n * @copyright 2022 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport jQuery from 'jquery';\nimport Pending from 'core/pending';\nimport {getDefaultConfiguration, getDefaultQuickbarsSelectionToolbar} from './defaults';\nimport {getTinyMCE, baseUrl} from './loader';\nimport * as Options from './options';\nimport {addToolbarButton, addToolbarButtons, addToolbarSection,\n removeToolbarButton, removeSubmenuItem, updateEditorState} from './utils';\n\n/**\n * Storage for the TinyMCE instances on the page.\n * @type {Map}\n */\nconst instanceMap = new Map();\n\n/**\n * The default editor configuration.\n * @type {Object}\n */\nlet defaultOptions = {};\n\n/**\n * Require the modules for the named set of TinyMCE plugins.\n *\n * @param {string[]} pluginList The list of plugins\n * @return {Promise[]} A matching set of Promises relating to the requested plugins\n */\nconst importPluginList = async(pluginList) => {\n // Fetch all of the plugins from the list of plugins.\n // If a plugin contains a '/' then it is assumed to be a Moodle AMD module to import.\n const pluginHandlers = await Promise.all(pluginList.map(pluginPath => {\n if (pluginPath.indexOf('/') === -1) {\n // A standard TinyMCE Plugin.\n return Promise.resolve(pluginPath);\n }\n\n return import(pluginPath);\n }));\n\n // Normalise the plugin data to a list of plugin names.\n // Two formats are supported:\n // - a string; and\n // - an array whose first element is the plugin name, and the second element is the plugin configuration.\n const pluginNames = pluginHandlers.map((pluginConfig) => {\n if (typeof pluginConfig === 'string') {\n return pluginConfig;\n }\n if (Array.isArray(pluginConfig)) {\n return pluginConfig[0];\n }\n return null;\n }).filter((value) => value);\n\n // Fetch the list of pluginConfig handlers.\n const pluginConfig = pluginHandlers.map((pluginConfig) => {\n if (Array.isArray(pluginConfig)) {\n return pluginConfig[1];\n }\n return null;\n }).filter((value) => value);\n\n return {\n pluginNames,\n pluginConfig,\n };\n};\n\n/**\n * Fetch the language data for the specified language.\n *\n * @param {string} language The language identifier\n * @returns {object}\n */\nconst fetchLanguage = (language) => fetch(\n `${M.cfg.wwwroot}/lib/editor/tiny/lang.php/${M.cfg.langrev}/${language}`\n).then(response => response.json());\n\n/**\n * Get a list of all Editors in a Map, keyed by the DOM Node that the Editor is associated with.\n *\n * @returns {Map}\n */\nexport const getAllInstances = () => new Map(instanceMap.entries());\n\n/**\n * Get the TinyMCE instance for the specified Node ID.\n *\n * @param {string} elementId\n * @returns {TinyMCE|undefined}\n */\nexport const getInstanceForElementId = elementId => getInstanceForElement(document.getElementById(elementId));\n\n/*\n * Get the TinyMCE instance for the specified HTMLElement.\n *\n * @param {HTMLElement} element\n * @returns {TinyMCE|undefined}\n */\nexport const getInstanceForElement = element => {\n const instance = instanceMap.get(element);\n if (instance && instance.removed) {\n instanceMap.delete(element);\n return undefined;\n }\n return instance;\n};\n\n/**\n * Set up TinyMCE for the selector at the specified HTML Node id.\n *\n * @param {object} config The configuration required to setup the editor\n * @param {string} config.elementId The HTML Node ID\n * @param {Object} config.options The editor plugin configuration\n */\nexport const setupForElementId = ({elementId, options}) => {\n const target = document.getElementById(elementId);\n // We will need to wrap the setupForTarget and editor.remove() calls in a setTimeout.\n // Because other events callbacks will still try to run on the removed instance.\n // This will cause an error on Firefox.\n // We need to make TinyMCE to remove itself outside the event loop.\n // @see https://github.com/tinymce/tinymce/issues/3129 for more details.\n setTimeout(() => {\n return setupForTarget(target, options);\n }, 1);\n};\n\n/**\n * Initialise the page with standard TinyMCE requirements.\n *\n * Currently this includes the language taken from the HTML lang property.\n */\nconst initialisePage = async() => {\n const lang = document.querySelector('html').lang;\n\n const [tinyMCE, langData] = await Promise.all([getTinyMCE(), fetchLanguage(lang)]);\n tinyMCE.addI18n(lang, langData);\n};\ninitialisePage();\n\n/**\n * Get the list of plugins to load for the specified configuration.\n *\n * If the specified configuration does not include a plugin configuration, then return the default configuration.\n *\n * @param {object} options\n * @param {array} [options.plugins=null] The plugin list\n * @returns {object}\n */\nconst getPlugins = ({plugins = null} = {}) => {\n if (plugins) {\n return plugins;\n }\n\n if (defaultOptions.plugins) {\n return defaultOptions.plugins;\n }\n\n return {};\n};\n\n/**\n * Adjust the editor size base on the target element.\n *\n * @param {TinyMCE} editor TinyMCE editor\n * @param {Node} target Target element\n */\nconst adjustEditorSize = (editor, target) => {\n let expectedEditingAreaHeight = 0;\n if (target.clientHeight) {\n expectedEditingAreaHeight = target.clientHeight;\n } else {\n // If the target element is hidden, we cannot get the lineHeight of the target element.\n // We don't have a proper way to retrieve the general lineHeight of the theme, so we use 22 here, it's equivalent to 1.5em.\n expectedEditingAreaHeight = target.rows * (parseFloat(window.getComputedStyle(target).lineHeight) || 22);\n }\n const currentEditingAreaHeight = editor.getContainer().querySelector('.tox-sidebar-wrap').clientHeight;\n if (currentEditingAreaHeight < expectedEditingAreaHeight) {\n // Change the height based on the target element's height.\n editor.getContainer().querySelector('.tox-sidebar-wrap').style.height = `${expectedEditingAreaHeight}px`;\n }\n};\n\n/**\n * Get the standard configuration for the specified options.\n *\n * @param {Node} target\n * @param {tinyMCE} tinyMCE\n * @param {object} options\n * @param {Array} plugins\n * @returns {object}\n */\nconst getStandardConfig = (target, tinyMCE, options, plugins) => {\n const lang = document.querySelector('html').lang;\n\n const config = Object.assign({}, getDefaultConfiguration(), {\n // eslint-disable-next-line camelcase\n base_url: baseUrl,\n\n // Set the editor target.\n // https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#target\n target,\n\n // https://www.tiny.cloud/docs/tinymce/6/customize-ui/#set-maximum-and-minimum-heights-and-widths\n // Set the minimum height to the smallest height that we can fit the Menu bar, Tool bar, Status bar and the text area.\n // eslint-disable-next-line camelcase\n min_height: 175,\n\n // Base the height on the size of the text area.\n // In some cases, E.g.: The target is an advanced element, it will be hidden. We cannot get the height at this time.\n // So set the height to auto, and adjust it later by adjustEditorSize().\n height: target.clientHeight || 'auto',\n\n // Set the language.\n // https://www.tiny.cloud/docs/tinymce/6/ui-localization/#language\n // eslint-disable-next-line camelcase\n language: lang,\n\n // Load the editor stylesheet into the editor iframe.\n // https://www.tiny.cloud/docs/tinymce/6/add-css-options/\n // eslint-disable-next-line camelcase\n content_css: [\n options.css,\n ],\n\n // Do not convert URLs to relative URLs.\n // https://www.tiny.cloud/docs/tinymce/6/url-handling/#convert_urls\n // eslint-disable-next-line camelcase\n convert_urls: false,\n\n // Enabled 'advanced' a11y options.\n // This includes allowing role=\"presentation\" from the image uploader.\n // https://www.tiny.cloud/docs/tinymce/6/accessibility/\n // eslint-disable-next-line camelcase\n a11y_advanced_options: true,\n\n // Add specific rules to the valid elements.\n // eslint-disable-next-line camelcase\n extended_valid_elements: 'script[*],p[*],i[*]',\n\n // Disable XSS Sanitisation.\n // We do this in PHP.\n // https://www.tiny.cloud/docs/tinymce/6/security/#turning-dompurify-off\n // Note: This feature has been backported from TinyMCE 6.4.0.\n // eslint-disable-next-line camelcase\n xss_sanitization: false,\n\n // Disable quickbars entirely.\n // The UI is not ideal and we'll wait for it to improve in future before we enable it in Moodle.\n // eslint-disable-next-line camelcase\n quickbars_insert_toolbar: '',\n\n // If the target element is too small, disable the quickbars selection toolbar.\n // The quickbars selection toolbar is not displayed correctly if the target element is too small.\n // See: https://github.com/tinymce/tinymce/issues/9693.\n quickbars_selection_toolbar: target.rows > 5 ? getDefaultQuickbarsSelectionToolbar() : false,\n\n // Override the standard block formats property (removing h1 & h2).\n // https://www.tiny.cloud/docs/tinymce/6/user-formatting-options/#block_formats\n // eslint-disable-next-line camelcase\n block_formats: 'Paragraph=p;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6;Preformatted=pre',\n\n // The list of plugins to include in the instance.\n // https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#plugins\n plugins: [\n ...plugins,\n ],\n\n // Skins\n skin: 'oxide',\n\n // Do not show the help link in the status bar.\n // https://www.tiny.cloud/docs/tinymce/latest/accessibility/#help_accessibility\n // eslint-disable-next-line camelcase\n help_accessibility: false,\n\n // Remove the \"Upgrade\" link for Tiny.\n // https://www.tiny.cloud/docs/tinymce/6/editor-premium-upgrade-promotion/\n promotion: false,\n\n // Allow the administrator to disable branding.\n // https://www.tiny.cloud/docs/tinymce/6/statusbar-configuration-options/#branding\n branding: options.branding,\n\n // Put th cells in a thead element.\n // https://www.tiny.cloud/docs/tinymce/6/table-options/#table_header_type\n // eslint-disable-next-line camelcase\n table_header_type: 'sectionCells',\n\n // Stored text in non-entity form.\n // https://www.tiny.cloud/docs/tinymce/6/content-filtering/#entity_encoding\n // eslint-disable-next-line camelcase\n entity_encoding: \"raw\",\n\n // Enable support for editors in scrollable containers.\n // https://www.tiny.cloud/docs/tinymce/6/ui-mode-configuration-options/#ui_mode\n // eslint-disable-next-line camelcase\n ui_mode: 'split',\n\n // Enable browser-supported spell checking.\n // https://www.tiny.cloud/docs/tinymce/latest/spelling/\n // eslint-disable-next-line camelcase\n browser_spellcheck: true,\n\n setup: (editor) => {\n Options.register(editor, options);\n\n editor.on('PreInit', function() {\n // Work around a bug in TinyMCE with Firefox.\n // When an editor is removed, and replaced with an identically attributed editor (same ID),\n // and the Firefox window is freshly opened (e.g. Behat, Private browsing), the wrong contentWindow\n // is assigned to the editor instance leading to an NS_ERROR_UNEXPECTED error in Firefox.\n // This is a workaround for that issue.\n this.contentWindow = this.iframeElement.contentWindow;\n });\n editor.on('init', function() {\n // Hide justify alignment sub-menu.\n removeSubmenuItem(editor, 'align', 'tiny:justify');\n // Adjust the editor size.\n adjustEditorSize(editor, target);\n });\n\n target.addEventListener('form:editorUpdated', function() {\n updateEditorState(editor, target);\n });\n\n target.dispatchEvent(new Event('form:editorUpdated'));\n },\n });\n\n config.toolbar = addToolbarSection(config.toolbar, 'content', 'formatting', true);\n config.toolbar = addToolbarButton(config.toolbar, 'content', 'link');\n\n // Add directionality plugins, always.\n config.toolbar = addToolbarSection(config.toolbar, 'directionality', 'alignment', true);\n config.toolbar = addToolbarButtons(config.toolbar, 'directionality', ['ltr', 'rtl']);\n\n // Remove the align justify button from the toolbar.\n config.toolbar = removeToolbarButton(config.toolbar, 'alignment', 'alignjustify');\n\n return config;\n};\n\n/**\n * Fetch the TinyMCE configuration for this editor instance.\n *\n * @param {HTMLElement} target\n * @param {TinyMCE} tinyMCE The TinyMCE API\n * @param {Object} options The editor plugin configuration\n * @param {object} pluginValues\n * @param {object} pluginValues.pluginConfig The list of plugin configuration\n * @param {object} pluginValues.pluginNames The list of plugins to load\n * @returns {object} The TinyMCE Configuration\n */\nconst getEditorConfiguration = (target, tinyMCE, options, pluginValues) => {\n const {\n pluginNames,\n pluginConfig,\n } = pluginValues;\n\n // Allow plugins to modify the configuration.\n // This seems a little strange, but we must double-process the config slightly.\n\n // First we fetch the standard configuration.\n const instanceConfig = getStandardConfig(target, tinyMCE, options, pluginNames);\n\n // Next we make any standard changes.\n // Here we remove the file menu, as it doesn't offer any useful functionality.\n // We only empty the items list so that a plugin may choose to add to it themselves later if they wish.\n if (instanceConfig.menu.file) {\n instanceConfig.menu.file.items = '';\n }\n\n // We disable the styles, backcolor, and forecolor plugins from the format menu.\n // These are not useful for Moodle and we don't want to encourage their use.\n if (instanceConfig.menu.format) {\n instanceConfig.menu.format.items = instanceConfig.menu.format.items\n // Remove forecolor and backcolor.\n .replace(/forecolor ?/, '')\n .replace(/backcolor ?/, '')\n\n // Remove fontfamily for now.\n .replace(/fontfamily ?/, '')\n\n // Remove fontsize for now.\n .replace(/fontsize ?/, '')\n\n // Remove styles - it just duplicates the format menu in a way which does not respect configuration\n .replace(/styles ?/, '')\n\n // Remove any duplicate separators.\n .replaceAll(/\\| *\\|/g, '|');\n }\n\n if (instanceConfig.quickbars_selection_toolbar !== false) {\n // eslint-disable-next-line camelcase\n instanceConfig.quickbars_selection_toolbar = instanceConfig.quickbars_selection_toolbar.replace('h2 h3', 'h3 h4 h5 h6');\n }\n\n // Next we call the `configure` function for any plugin which defines it.\n // We pass the current instanceConfig in here, to allow them to make certain changes to the global configuration.\n // For example, to add themselves to any menu, toolbar, and so on.\n // Any plugin which wishes to have configuration options must register those options here.\n pluginConfig.filter((pluginConfig) => typeof pluginConfig.configure === 'function').forEach((pluginConfig) => {\n const pluginInstanceOverride = pluginConfig.configure(instanceConfig, options);\n Object.assign(instanceConfig, pluginInstanceOverride);\n });\n\n // Next we convert the plugin configuration into a format that TinyMCE understands.\n Object.assign(instanceConfig, Options.getInitialPluginConfiguration(options));\n\n return instanceConfig;\n};\n\n/**\n * Check if the target for TinyMCE is in a modal or not.\n *\n * @param {HTMLElement} target Target to check\n * @returns {boolean} True if the target is in a modal form.\n */\nconst isModalMode = (target) => {\n return !!target.closest('[data-region=\"modal\"]');\n};\n\n/**\n * Set up TinyMCE for the HTML Element.\n *\n * @param {HTMLElement} target\n * @param {Object} [options={}] The editor plugin configuration\n * @return {Promise} The TinyMCE instance\n */\nexport const setupForTarget = async(target, options = {}) => {\n const instance = getInstanceForElement(target);\n if (instance) {\n return Promise.resolve(instance);\n }\n\n // Register a new pending promise to ensure that Behat waits for the editor setup to complete before continuing.\n const pendingPromise = new Pending('editor_tiny/editor:setupForTarget');\n\n // Get the list of plugins.\n const plugins = getPlugins(options);\n\n // Fetch the tinyMCE API, and instantiate the plugins.\n const [tinyMCE, pluginValues] = await Promise.all([\n getTinyMCE(),\n importPluginList(Object.keys(plugins)),\n ]);\n\n // TinyMCE uses the element ID as a map key internally, even if the target has changed.\n // In the case where we have an editor in a modal form which has been detached from the DOM, but the editor not removed,\n // we need to manually destroy the editor.\n // We could theoretically do this with a Mutation Observer, but in some cases the Node may be moved,\n // or added back elsewhere in the DOM.\n\n // First remove any detached editors.\n tinyMCE.get().filter((editor) => !editor.getElement().isConnected).forEach((editor) => {\n editor.remove();\n });\n\n // Now check for any existing editor which shares the same ID.\n const existingEditor = tinyMCE.EditorManager.get(target.id);\n if (existingEditor) {\n if (existingEditor.getElement() === target) {\n pendingPromise.resolve();\n return Promise.resolve(existingEditor);\n } else {\n pendingPromise.resolve();\n throw new Error('TinyMCE instance already exists for different target with same ID');\n }\n }\n\n // Get the editor configuration for this editor.\n const instanceConfig = getEditorConfiguration(target, tinyMCE, options, pluginValues);\n\n // Initialise the editor instance for the given configuration.\n // At this point any plugin which has configuration options registered will have them applied for this instance.\n const [editor] = await tinyMCE.init(instanceConfig);\n\n // Update the textarea when the editor to set the field type for Behat.\n target.dataset.fieldtype = 'editor';\n\n // Store the editor instance in the instanceMap and register a listener on removal to remove it from the map.\n instanceMap.set(target, editor);\n editor.on('remove', ({target}) => {\n // Handle removal of the editor from the map on destruction.\n instanceMap.delete(target.targetElm);\n target.targetElm.dataset.fieldtype = null;\n });\n\n // If the editor is part of a form, also listen to the jQuery submit event.\n // The jQuery submit event will not trigger the native submit event, and therefore the content will not be saved.\n // We cannot rely on listening to the bubbled submit event on the document because other events on child nodes may\n // consume the data before it is saved.\n if (target.form) {\n jQuery(target.form).on('submit', () => {\n editor.save();\n });\n }\n\n // Save the editor content to the textarea when the editor is blurred.\n editor.on('blur', () => {\n editor.save();\n });\n\n // If the editor is in a modal, we need to hide the modal when window editor's window is opened.\n editor.on('OpenWindow', () => {\n const modals = document.querySelectorAll('[data-region=\"modal\"]');\n if (modals) {\n modals.forEach((modal) => {\n if (!modal.classList.contains('hide')) {\n modal.classList.add('hide');\n }\n });\n }\n });\n\n // If the editor's window is closed, we need to show the hidden modal back.\n editor.on('CloseWindow', () => {\n if (isModalMode(target)) {\n const modals = document.querySelectorAll('[data-region=\"modal\"]');\n if (modals) {\n modals.forEach((modal) => {\n if (modal.classList.contains('hide')) {\n modal.classList.remove('hide');\n }\n });\n }\n }\n });\n\n pendingPromise.resolve();\n return editor;\n};\n\n/**\n * Set the default editor configuration.\n *\n * This configuration is used when an editor is initialised without any configuration.\n *\n * @param {object} [options={}]\n */\nexport const configureDefaultEditor = (options = {}) => {\n defaultOptions = options;\n};\n"],"names":["instanceMap","Map","defaultOptions","importPluginList","async","pluginHandlers","Promise","all","pluginList","map","pluginPath","indexOf","resolve","pluginNames","pluginConfig","Array","isArray","filter","value","entries","elementId","getInstanceForElement","document","getElementById","element","instance","get","removed","delete","_ref","options","target","setTimeout","setupForTarget","lang","querySelector","tinyMCE","langData","language","fetch","M","cfg","wwwroot","langrev","then","response","json","addI18n","initialisePage","getPlugins","plugins","getStandardConfig","config","Object","assign","base_url","baseUrl","min_height","height","clientHeight","content_css","css","convert_urls","a11y_advanced_options","extended_valid_elements","xss_sanitization","quickbars_insert_toolbar","quickbars_selection_toolbar","rows","block_formats","skin","help_accessibility","promotion","branding","table_header_type","entity_encoding","ui_mode","browser_spellcheck","setup","editor","Options","register","on","contentWindow","this","iframeElement","expectedEditingAreaHeight","parseFloat","window","getComputedStyle","lineHeight","getContainer","style","adjustEditorSize","addEventListener","dispatchEvent","Event","toolbar","getEditorConfiguration","pluginValues","instanceConfig","menu","file","items","format","replace","replaceAll","configure","forEach","pluginInstanceOverride","getInitialPluginConfiguration","isModalMode","closest","pendingPromise","Pending","keys","getElement","isConnected","remove","existingEditor","EditorManager","id","Error","init","dataset","fieldtype","set","_ref2","targetElm","form","save","modals","querySelectorAll","modal","classList","contains","add"],"mappings":"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"} \ No newline at end of file diff --git a/lib/editor/tiny/amd/src/editor.js b/lib/editor/tiny/amd/src/editor.js index 6e3fa15d692fe..13d92eead9a46 100644 --- a/lib/editor/tiny/amd/src/editor.js +++ b/lib/editor/tiny/amd/src/editor.js @@ -23,7 +23,7 @@ import jQuery from 'jquery'; import Pending from 'core/pending'; -import {getDefaultConfiguration} from './defaults'; +import {getDefaultConfiguration, getDefaultQuickbarsSelectionToolbar} from './defaults'; import {getTinyMCE, baseUrl} from './loader'; import * as Options from './options'; import {addToolbarButton, addToolbarButtons, addToolbarSection, @@ -271,6 +271,11 @@ const getStandardConfig = (target, tinyMCE, options, plugins) => { // eslint-disable-next-line camelcase quickbars_insert_toolbar: '', + // If the target element is too small, disable the quickbars selection toolbar. + // The quickbars selection toolbar is not displayed correctly if the target element is too small. + // See: https://github.com/tinymce/tinymce/issues/9693. + quickbars_selection_toolbar: target.rows > 5 ? getDefaultQuickbarsSelectionToolbar() : false, + // Override the standard block formats property (removing h1 & h2). // https://www.tiny.cloud/docs/tinymce/6/user-formatting-options/#block_formats // eslint-disable-next-line camelcase @@ -408,8 +413,10 @@ const getEditorConfiguration = (target, tinyMCE, options, pluginValues) => { .replaceAll(/\| *\|/g, '|'); } - // eslint-disable-next-line camelcase - instanceConfig.quickbars_selection_toolbar = instanceConfig.quickbars_selection_toolbar.replace('h2 h3', 'h3 h4 h5 h6'); + if (instanceConfig.quickbars_selection_toolbar !== false) { + // eslint-disable-next-line camelcase + instanceConfig.quickbars_selection_toolbar = instanceConfig.quickbars_selection_toolbar.replace('h2 h3', 'h3 h4 h5 h6'); + } // Next we call the `configure` function for any plugin which defines it. // We pass the current instanceConfig in here, to allow them to make certain changes to the global configuration. From ca7286c14c17c8f329f4b14357d8f7472bd7ebaa Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Wed, 26 Jun 2024 16:26:21 +0700 Subject: [PATCH 007/178] MDL-81991 core: Reload accessdata for dirty users in CLI mode --- lib/accesslib.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/accesslib.php b/lib/accesslib.php index be16794bfb300..ccddb2905fc8f 100644 --- a/lib/accesslib.php +++ b/lib/accesslib.php @@ -979,6 +979,15 @@ function get_user_accessdata($userid, $preloadonly=false) { $ACCESSLIB_PRIVATE->accessdatabyuser[$USER->id] = $USER->access; } + // Unfortunately, we can't use the $ACCESSLIB_PRIVATE->dirtyusers array because it is not available in CLI. + // So we need to check if the user has been marked as dirty or not in the cache directly. + // This will add additional queries to the database, but it is the best we can do. + if (CLI_SCRIPT && !empty($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) { + if (get_cache_flag('accesslib/dirtyusers', $userid, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]['time'])) { + unset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid]); + } + } + if (!isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) { if (empty($userid)) { if (!empty($CFG->notloggedinroleid)) { From 8723c4099bad503496733133ca2f24572c42a1ec Mon Sep 17 00:00:00 2001 From: Daniel Ziegenberg Date: Wed, 27 Mar 2024 21:58:00 +0100 Subject: [PATCH 008/178] MDL-75025 backup: Final deprecation of base_controller::get/set_copy Signed-off-by: Daniel Ziegenberg --- .upgradenotes/MDL-75025-2024062015422824.yml | 7 ++++ .upgradenotes/MDL-75025-2024062015430029.yml | 6 ++++ backup/controller/base_controller.class.php | 36 +++++--------------- 3 files changed, 21 insertions(+), 28 deletions(-) create mode 100644 .upgradenotes/MDL-75025-2024062015422824.yml create mode 100644 .upgradenotes/MDL-75025-2024062015430029.yml 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/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__]); } } From 77f9238cf4ea99b7a763ad0e962f3de05015cc96 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 25 Jun 2024 21:21:00 +0800 Subject: [PATCH 009/178] MDL-82287 core: Deprecate long-deprecated functions These were originally believed to be so widely used that we could never migrate away from them but it seems we have! --- .upgradenotes/MDL-82287-2024062501330649.yml | 40 +++++ lib/classes/grades_external.php | 2 +- lib/deprecatedlib.php | 140 +++------------ lib/tests/accesslib_test.php | 20 --- lib/tests/component_test.php | 176 ++++++++++--------- lib/tests/csslib_test.php | 51 ------ lib/tests/event/base_test.php | 50 ------ message/tests/messagelib_test.php | 20 --- tag/tests/taglib_test.php | 55 ------ 9 files changed, 159 insertions(+), 395 deletions(-) create mode 100644 .upgradenotes/MDL-82287-2024062501330649.yml delete mode 100644 lib/tests/csslib_test.php 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/lib/classes/grades_external.php b/lib/classes/grades_external.php index 671351f9ea2aa..20f36442f4263 100644 --- a/lib/classes/grades_external.php +++ b/lib/classes/grades_external.php @@ -122,7 +122,7 @@ public static function update_grades($source, $courseid, $component, $activityid ) ); - list($itemtype, $itemmodule) = normalize_component($params['component']); + list($itemtype, $itemmodule) = \core_component::normalize_component($params['component']); if (! $cm = get_coursemodule_from_id($itemmodule, $activityid)) { throw new moodle_exception('invalidcoursemodule'); diff --git a/lib/deprecatedlib.php b/lib/deprecatedlib.php index 8f694eabf3285..b0efea8673748 100644 --- a/lib/deprecatedlib.php +++ b/lib/deprecatedlib.php @@ -1,5 +1,4 @@ (string|null)location */ +#[\core\attribute\deprecated('core_component::get_core_subsystems', since: '4.5', mdl: 'MDL-82287')] function get_core_subsystems($fullpaths = false) { + \core\deprecation::emit_deprecation_if_present(__FUNCTION__); global $CFG; - // NOTE: do not add any other debugging here, keep forever. - $subsystems = core_component::get_core_subsystems(); if ($fullpaths) { @@ -78,15 +74,14 @@ function get_core_subsystems($fullpaths = false) { * Lists all plugin types. * * @deprecated since 2.6, use core_component::get_plugin_types() - * * @param bool $fullpaths false means relative paths from dirroot * @return array Array of strings - name=>location */ +#[\core\attribute\deprecated('core_component::get_plugin_types', since: '4.5', mdl: 'MDL-82287')] function get_plugin_types($fullpaths = true) { + \core\deprecation::emit_deprecation_if_present(__FUNCTION__); global $CFG; - // NOTE: do not add any other debugging here, keep forever. - $types = core_component::get_plugin_types(); if ($fullpaths) { @@ -112,13 +107,12 @@ function get_plugin_types($fullpaths = true) { * Use when listing real plugins of one type. * * @deprecated since 2.6, use core_component::get_plugin_list() - * * @param string $plugintype type of plugin * @return array name=>fulllocation pairs of plugins of given type */ +#[\core\attribute\deprecated('core_component::get_plugin_list', since: '4.5', mdl: 'MDL-82287')] function get_plugin_list($plugintype) { - - // NOTE: do not add any other debugging here, keep forever. + \core\deprecation::emit_deprecation_if_present(__FUNCTION__); if ($plugintype === '') { $plugintype = 'mod'; @@ -132,7 +126,6 @@ function get_plugin_list($plugintype) { * in a certain file. The plugin component names and class names are returned. * * @deprecated since 2.6, use core_component::get_plugin_list_with_class() - * * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'. * @param string $class the part of the name of the class after the * frankenstyle prefix. e.g 'thing' if you are looking for classes with @@ -142,10 +135,9 @@ function get_plugin_list($plugintype) { * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum') * and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice'). */ +#[\core\attribute\deprecated('core_component::get_plugin_list_with_class', since: '4.5', mdl: 'MDL-82287')] function get_plugin_list_with_class($plugintype, $class, $file) { - - // NOTE: do not add any other debugging here, keep forever. - + \core\deprecation::emit_deprecation_if_present(__FUNCTION__); return core_component::get_plugin_list_with_class($plugintype, $class, $file); } @@ -153,15 +145,13 @@ function get_plugin_list_with_class($plugintype, $class, $file) { * Returns the exact absolute path to plugin directory. * * @deprecated since 2.6, use core_component::get_plugin_directory() - * * @param string $plugintype type of plugin * @param string $name name of the plugin * @return string full path to plugin directory; NULL if not found */ +#[\core\attribute\deprecated('core_component::get_plugin_directory', since: '4.5', mdl: 'MDL-82287')] function get_plugin_directory($plugintype, $name) { - - // NOTE: do not add any other debugging here, keep forever. - + \core\deprecation::emit_deprecation_if_present(__FUNCTION__); if ($plugintype === '') { $plugintype = 'mod'; } @@ -173,14 +163,12 @@ function get_plugin_directory($plugintype, $name) { * Normalize the component name using the "frankenstyle" names. * * @deprecated since 2.6, use core_component::normalize_component() - * * @param string $component * @return array two-items list of [(string)type, (string|null)name] */ +#[\core\attribute\deprecated('core_component::normalize_component', since: '4.5', mdl: 'MDL-82287')] function normalize_component($component) { - - // NOTE: do not add any other debugging here, keep forever. - + \core\deprecation::emit_deprecation_if_present(__FUNCTION__); return core_component::normalize_component($component); } @@ -192,66 +180,34 @@ function normalize_component($component) { * @param string $component name such as 'moodle', 'mod_forum' * @return string full path to component directory; NULL if not found */ +#[\core\attribute\deprecated('core_component::get_component_directory', since: '4.5', mdl: 'MDL-82287')] function get_component_directory($component) { - - // NOTE: do not add any other debugging here, keep forever. - + \core\deprecation::emit_deprecation_if_present(__FUNCTION__); return core_component::get_component_directory($component); } /** - * Get the context instance as an object. This function will create the - * context instance if it does not exist yet. - * * @deprecated since 2.2, use context_course::instance() or other relevant class instead - * @todo This will be deleted in Moodle 2.8, refer MDL-34472 - * @param integer $contextlevel The context level, for example CONTEXT_COURSE, or CONTEXT_MODULE. - * @param integer $instance The instance id. For $level = CONTEXT_COURSE, this would be $course->id, - * for $level = CONTEXT_MODULE, this would be $cm->id. And so on. Defaults to 0 - * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found; - * MUST_EXIST means throw exception if no record or multiple records found - * @return context The context object. */ -function get_context_instance($contextlevel, $instance = 0, $strictness = IGNORE_MISSING) { - - debugging('get_context_instance() is deprecated, please use context_xxxx::instance() instead.', DEBUG_DEVELOPER); - - $instances = (array)$instance; - $contexts = array(); - - $classname = context_helper::get_class_for_level($contextlevel); - - // we do not load multiple contexts any more, PAGE should be responsible for any preloading - foreach ($instances as $inst) { - $contexts[$inst] = $classname::instance($inst, $strictness); - } - - if (is_array($instance)) { - return $contexts; - } else { - return $contexts[$instance]; - } +#[\core\attribute\deprecated('\core\context::instance', since: '2.2', mdl: 'MDL-34472', final: true)] +function get_context_instance() { + \core\deprecation::emit_deprecation_if_present(__FUNCTION__); } -/* === End of long term deprecated api list === */ /** * @deprecated since 2.5 - do not use, the textrotate.js will work it out automatically */ +#[\core\attribute\deprecated('Not replaced', since: '2.0', mdl: 'MDL-19756', final: true)] function can_use_rotated_text() { - debugging('can_use_rotated_text() is removed. JS feature detection is used automatically.'); + \core\deprecation::emit_deprecation_if_present(__FUNCTION__); } /** - * Returns system context or null if can not be created yet. - * - * @see context_system::instance() * @deprecated since 2.2 - * @param bool $cache use caching - * @return context system context (null if context table not created yet) */ -function get_system_context($cache = true) { - debugging('get_system_context() is deprecated, please use context_system::instance() instead.', DEBUG_DEVELOPER); - return context_system::instance(0, IGNORE_MISSING, $cache); +#[\core\attribute\deprecated('\core\context\system::instance', since: '2.2', mdl: 'MDL-34472', final: true)] +function get_system_context() { + \core\deprecation::emit_deprecation_if_present(__FUNCTION__); } /** @@ -259,56 +215,10 @@ function get_system_context($cache = true) { * provide this function with the language strings for sortasc and sortdesc. * * @deprecated use $OUTPUT->arrow() instead. - * @todo final deprecation of this function once MDL-45448 is resolved - * - * If no sort string is associated with the direction, an arrow with no alt text will be printed/returned. - * - * @global object - * @param string $direction 'up' or 'down' - * @param string $strsort The language string used for the alt attribute of this image - * @param bool $return Whether to print directly or return the html string - * @return string|void depending on $return - * */ -function print_arrow($direction='up', $strsort=null, $return=false) { - global $OUTPUT; - - debugging('print_arrow() is deprecated. Please use $OUTPUT->arrow() instead.', DEBUG_DEVELOPER); - - if (!in_array($direction, array('up', 'down', 'right', 'left', 'move'))) { - return null; - } - - $return = null; - - switch ($direction) { - case 'up': - $sortdir = 'asc'; - break; - case 'down': - $sortdir = 'desc'; - break; - case 'move': - $sortdir = 'asc'; - break; - default: - $sortdir = null; - break; - } - - // Prepare language string - $strsort = ''; - if (empty($strsort) && !empty($sortdir)) { - $strsort = get_string('sort' . $sortdir, 'grades'); - } - - $return = ' ' . $OUTPUT->pix_icon('t/' . $direction, $strsort) . ' '; - - if ($return) { - return $return; - } else { - echo $return; - } +#[\core\attribute\deprecated('OUTPUT->[l|r]arrow', since: '2.0', mdl: 'MDL-19756', final: true)] +function print_arrow() { + \core\deprecation::emit_deprecation_if_present(__FUNCTION__); } /** diff --git a/lib/tests/accesslib_test.php b/lib/tests/accesslib_test.php index be464e2f66043..37a5b6da6569e 100644 --- a/lib/tests/accesslib_test.php +++ b/lib/tests/accesslib_test.php @@ -4045,26 +4045,6 @@ public function test_permission_evaluation(): void { context_user::instance($testusers[102]); $this->assertEquals($prevsize + 1, context_inspection::check_context_cache_size()); unset($testusers); - - - - // Test basic test of legacy functions. - // Note: watch out, the fake site might be pretty borked already. - - $this->assertEquals(get_system_context(), context_system::instance()); - $this->assertDebuggingCalled('get_system_context() is deprecated, please use context_system::instance() instead.', DEBUG_DEVELOPER); - - foreach ($DB->get_records('context') as $contextid => $record) { - $context = context::instance_by_id($contextid); - $this->assertEquals($context, get_context_instance($record->contextlevel, $record->instanceid)); - $this->assertDebuggingCalled('get_context_instance() is deprecated, please use context_xxxx::instance() instead.', DEBUG_DEVELOPER); - } - - // Make sure a debugging is thrown. - get_context_instance($record->contextlevel, $record->instanceid); - $this->assertDebuggingCalled('get_context_instance() is deprecated, please use context_xxxx::instance() instead.', DEBUG_DEVELOPER); - get_system_context(); - $this->assertDebuggingCalled('get_system_context() is deprecated, please use context_system::instance() instead.', DEBUG_DEVELOPER); } /** diff --git a/lib/tests/component_test.php b/lib/tests/component_test.php index 97b6a31fdc1be..c511952f7756b 100644 --- a/lib/tests/component_test.php +++ b/lib/tests/component_test.php @@ -99,11 +99,16 @@ public function test_deprecated_get_core_subsystems(): void { $subsystems = core_component::get_core_subsystems(); $this->assertSame($subsystems, get_core_subsystems(true)); + $this->assertDebuggingCalled(); + $this->resetDebugging(); $realsubsystems = get_core_subsystems(); - $this->assertDebuggingCalled(); + $this->assertdebuggingcalledcount(2); + $this->resetDebugging(); + $this->assertSame($realsubsystems, get_core_subsystems(false)); - $this->assertDebuggingCalled(); + $this->assertdebuggingcalledcount(2); + $this->resetDebugging(); $this->assertEquals(count($subsystems), count($realsubsystems)); @@ -138,10 +143,16 @@ public function test_deprecated_get_plugin_types(): void { $plugintypes = core_component::get_plugin_types(); $this->assertSame($plugintypes, get_plugin_types()); + $this->assertDebuggingCalled(); + $this->resetDebugging(); + $this->assertSame($plugintypes, get_plugin_types(true)); + $this->assertDebuggingCalled(); + $this->resetDebugging(); $realplugintypes = get_plugin_types(false); - $this->assertDebuggingCalled(); + $this->assertdebuggingcalledcount(2); + $this->resetDebugging(); foreach ($plugintypes as $plugintype => $fulldir) { $this->assertSame($fulldir, $CFG->dirroot . '/' . $realplugintypes[$plugintype]); @@ -175,6 +186,8 @@ public function test_deprecated_get_plugin_list(): void { foreach ($plugintypes as $plugintype => $fulldir) { $plugins = core_component::get_plugin_list($plugintype); $this->assertSame($plugins, get_plugin_list($plugintype)); + $this->assertDebuggingCalled(); + $this->resetDebugging(); } } @@ -199,6 +212,8 @@ public function test_deprecated_get_plugin_directory(): void { core_component::get_plugin_directory($plugintype, $pluginname), get_plugin_directory($plugintype, $pluginname), ); + $this->assertDebuggingCalled(); + $this->resetDebugging(); } } } @@ -308,92 +323,83 @@ public function test_normalize_componentname(): void { ); } - public function test_normalize_component(): void { - // Moodle core. - $this->assertSame(['core', null], core_component::normalize_component('core')); - $this->assertSame(['core', null], core_component::normalize_component('moodle')); - $this->assertSame(['core', null], core_component::normalize_component('')); - - // Moodle core subsystems. - $this->assertSame(['core', 'admin'], core_component::normalize_component('admin')); - $this->assertSame(['core', 'admin'], core_component::normalize_component('core_admin')); - $this->assertSame(['core', 'admin'], core_component::normalize_component('moodle_admin')); - - // Activity modules and their subplugins. - $this->assertSame(['mod', 'workshop'], core_component::normalize_component('workshop')); - $this->assertSame(['mod', 'workshop'], core_component::normalize_component('mod_workshop')); - $this->assertSame(['workshopform', 'accumulative'], core_component::normalize_component('workshopform_accumulative')); - $this->assertSame(['mod', 'quiz'], core_component::normalize_component('quiz')); - $this->assertSame(['quiz', 'grading'], core_component::normalize_component('quiz_grading')); - $this->assertSame(['mod', 'data'], core_component::normalize_component('data')); - $this->assertSame(['datafield', 'checkbox'], core_component::normalize_component('datafield_checkbox')); - - // Other plugin types. - $this->assertSame(['auth', 'mnet'], core_component::normalize_component('auth_mnet')); - $this->assertSame(['enrol', 'self'], core_component::normalize_component('enrol_self')); - $this->assertSame(['block', 'html'], core_component::normalize_component('block_html')); - $this->assertSame(['block', 'mnet_hosts'], core_component::normalize_component('block_mnet_hosts')); - $this->assertSame(['local', 'amos'], core_component::normalize_component('local_amos')); - $this->assertSame(['local', 'admin'], core_component::normalize_component('local_admin')); - - // Unknown words without underscore are supposed to be activity modules. - $this->assertSame( - ['mod', 'whoonearthwouldcomewithsuchastupidnameofcomponent'], - core_component::normalize_component('whoonearthwouldcomewithsuchastupidnameofcomponent') - ); - // Module names can not contain underscores, this must be a subplugin. - $this->assertSame( - ['whoonearth', 'wouldcomewithsuchastupidnameofcomponent'], - core_component::normalize_component('whoonearth_wouldcomewithsuchastupidnameofcomponent') - ); + /** + * Test \core_component::normalize_component function. + * + * @dataProvider normalise_component_provider + * @param array $expected + * @param string $args + */ + public function test_normalize_component(array $expected, string $args): void { $this->assertSame( - ['whoonearth', 'would_come_withsuchastupidnameofcomponent'], - core_component::normalize_component('whoonearth_would_come_withsuchastupidnameofcomponent') + $expected, + core_component::normalize_component($args), ); } - public function test_deprecated_normalize_component(): void { - // Moodle core. - $this->assertSame(['core', null], normalize_component('core')); - $this->assertSame(['core', null], normalize_component('')); - $this->assertSame(['core', null], normalize_component('moodle')); - - // Moodle core subsystems. - $this->assertSame(['core', 'admin'], normalize_component('admin')); - $this->assertSame(['core', 'admin'], normalize_component('core_admin')); - $this->assertSame(['core', 'admin'], normalize_component('moodle_admin')); - - // Activity modules and their subplugins. - $this->assertSame(['mod', 'workshop'], normalize_component('workshop')); - $this->assertSame(['mod', 'workshop'], normalize_component('mod_workshop')); - $this->assertSame(['workshopform', 'accumulative'], normalize_component('workshopform_accumulative')); - $this->assertSame(['mod', 'quiz'], normalize_component('quiz')); - $this->assertSame(['quiz', 'grading'], normalize_component('quiz_grading')); - $this->assertSame(['mod', 'data'], normalize_component('data')); - $this->assertSame(['datafield', 'checkbox'], normalize_component('datafield_checkbox')); - - // Other plugin types. - $this->assertSame(['auth', 'mnet'], normalize_component('auth_mnet')); - $this->assertSame(['enrol', 'self'], normalize_component('enrol_self')); - $this->assertSame(['block', 'html'], normalize_component('block_html')); - $this->assertSame(['block', 'mnet_hosts'], normalize_component('block_mnet_hosts')); - $this->assertSame(['local', 'amos'], normalize_component('local_amos')); - $this->assertSame(['local', 'admin'], normalize_component('local_admin')); - - // Unknown words without underscore are supposed to be activity modules. - $this->assertSame( - ['mod', 'whoonearthwouldcomewithsuchastupidnameofcomponent'], - normalize_component('whoonearthwouldcomewithsuchastupidnameofcomponent') - ); - // Module names can not contain underscores, this must be a subplugin. - $this->assertSame( - ['whoonearth', 'wouldcomewithsuchastupidnameofcomponent'], - normalize_component('whoonearth_wouldcomewithsuchastupidnameofcomponent') - ); + /** + * Test the deprecated normalize_component function. + * + * @dataProvider normalise_component_provider + * @param array $expected + * @param string $args + */ + public function test_deprecated_normalize_component(array $expected, string $args): void { $this->assertSame( - ['whoonearth', 'would_come_withsuchastupidnameofcomponent'], - normalize_component('whoonearth_would_come_withsuchastupidnameofcomponent') + $expected, + normalize_component($args), ); + + $this->assertDebuggingCalled(); + } + + /** + * Data provider for the normalize_component function. + */ + public static function normalise_component_provider(): array { + return [ + // Moodle core. + [['core', null], 'core'], + [['core', null], ''], + [['core', null], 'moodle'], + + // Moodle core subsystems. + [['core', 'admin'], 'admin'], + [['core', 'admin'], 'core_admin'], + [['core', 'admin'], 'moodle_admin'], + + // Activity modules and their subplugins. + [['mod', 'workshop'], 'workshop'], + [['mod', 'workshop'], 'mod_workshop'], + [['workshopform', 'accumulative'], 'workshopform_accumulative'], + [['mod', 'quiz'], 'quiz'], + [['quiz', 'grading'], 'quiz_grading'], + [['mod', 'data'], 'data'], + [['datafield', 'checkbox'], 'datafield_checkbox'], + + // Other plugin types. + [['auth', 'mnet'], 'auth_mnet'], + [['enrol', 'self'], 'enrol_self'], + [['block', 'html'], 'block_html'], + [['block', 'mnet_hosts'], 'block_mnet_hosts'], + [['local', 'amos'], 'local_amos'], + [['local', 'admin'], 'local_admin'], + + // Unknown words without underscore are supposed to be activity modules. + [ + ['mod', 'whoonearthwouldcomewithsuchastupidnameofcomponent'], + 'whoonearthwouldcomewithsuchastupidnameofcomponent', + ], + // Module names can not contain underscores, this must be a subplugin. + [ + ['whoonearth', 'wouldcomewithsuchastupidnameofcomponent'], + 'whoonearth_wouldcomewithsuchastupidnameofcomponent', + ], + [ + ['whoonearth', 'would_come_withsuchastupidnameofcomponent'], + 'whoonearth_would_come_withsuchastupidnameofcomponent', + ], + ]; } public function test_get_component_directory(): void { @@ -484,12 +490,16 @@ public function test_deprecated_get_component_directory(): void { $plugins = core_component::get_plugin_list($plugintype); foreach ($plugins as $pluginname => $plugindir) { $this->assertSame($plugindir, get_component_directory(($plugintype . '_' . $pluginname))); + $this->assertDebuggingCalled(); + $this->resetDebugging(); } } $subsystems = core_component::get_core_subsystems(); foreach ($subsystems as $subsystem => $fulldir) { $this->assertSame($fulldir, get_component_directory(('core_' . $subsystem))); + $this->assertDebuggingCalled(); + $this->resetDebugging(); } } diff --git a/lib/tests/csslib_test.php b/lib/tests/csslib_test.php deleted file mode 100644 index 11c4b83dfd880..0000000000000 --- a/lib/tests/csslib_test.php +++ /dev/null @@ -1,51 +0,0 @@ -. - -namespace core; - -defined('MOODLE_INTERNAL') || die(); - -global $CFG; -require_once($CFG->libdir . '/csslib.php'); - -/** - * CSS optimiser test class. - * - * @package core - * @category test - * @copyright 2012 Sam Hemelryk - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class csslib_test extends \advanced_testcase { - - /** - * Test that css_is_colour function throws an exception. - */ - public function test_css_is_colour(): void { - $this->expectException('coding_exception'); - $this->expectExceptionMessage('css_is_colour() can not be used anymore.'); - css_is_colour(); - } - - /** - * Test that css_is_width function throws an exception. - */ - public function test_css_is_width(): void { - $this->expectException('coding_exception'); - $this->expectExceptionMessage('css_is_width() can not be used anymore.'); - css_is_width(); - } -} diff --git a/lib/tests/event/base_test.php b/lib/tests/event/base_test.php index 6b47fe16fcc4c..088ae9eca2edb 100644 --- a/lib/tests/event/base_test.php +++ b/lib/tests/event/base_test.php @@ -573,56 +573,6 @@ public function test_deprecated(): void { $this->assertSame($event::LEVEL_TEACHING, $event->edulevel); } - public function test_legacy(): void { - global $DB, $CFG; - - $this->resetAfterTest(true); - - $observers = array( - array( - 'eventname' => '\core_tests\event\unittest_executed', - 'callback' => '\core_tests\event\unittest_observer::observe_one', - ), - array( - 'eventname' => '*', - 'callback' => '\core_tests\event\unittest_observer::observe_all', - 'includefile' => null, - 'internal' => 1, - 'priority' => 9999, - ), - ); - - $DB->delete_records('log', array()); - $this->expectException(\coding_exception::class); - events_update_definition('unittest'); - - $DB->delete_records_select('events_handlers', "component <> 'unittest'"); - - $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER); - $this->assertEquals(3, $DB->count_records('events_handlers')); - set_config('loglifetime', 60*60*24*5); - - \core\event\manager::phpunit_replace_observers($observers); - \core_tests\event\unittest_observer::reset(); - - $event1 = \core_tests\event\unittest_executed::create(array('context'=>\context_system::instance(), 'other'=>array('sample'=>5, 'xx'=>10))); - $event1->trigger(); - - $event2 = \core_tests\event\unittest_executed::create(array('context'=>\context_system::instance(), 'other'=>array('sample'=>6, 'xx'=>11))); - $event2->nest = true; - $event2->trigger(); - - $this->assertSame( - array('observe_all-5', 'observe_one-5', 'observe_all-nesting-6', 'observe_one-6', 'observe_all-666', 'observe_one-666'), - \core_tests\event\unittest_observer::$info); - - $this->assertSame($event1, \core_tests\event\unittest_observer::$event[0]); - $this->assertSame($event1, \core_tests\event\unittest_observer::$event[1]); - - $logs = $DB->get_records('log', array(), 'id ASC'); - $this->assertCount(0, $logs); - } - public function test_restore_event(): void { $event1 = \core_tests\event\unittest_executed::create(array('context'=>\context_system::instance(), 'other'=>array('sample'=>1, 'xx'=>10))); $data1 = $event1->get_data(); diff --git a/message/tests/messagelib_test.php b/message/tests/messagelib_test.php index 15103d9087ab7..18f963225fafd 100644 --- a/message/tests/messagelib_test.php +++ b/message/tests/messagelib_test.php @@ -115,26 +115,6 @@ protected function send_fake_message($userfrom, $userto, $message = 'Hello world return $DB->insert_record('messages', $record); } - /** - * Test message_get_blocked_users throws an exception because has been removed. - */ - public function test_message_get_blocked_users(): void { - $this->expectException('coding_exception'); - $this->expectExceptionMessage( - 'message_get_blocked_users() has been removed, please use \core_message\api::get_blocked_users() instead.' - ); - message_get_blocked_users(); - } - - /** - * Test message_get_contacts throws an exception because has been removed. - */ - public function test_message_get_contacts(): void { - $this->expectException('coding_exception'); - $this->expectExceptionMessage('message_get_contacts() has been removed.'); - message_get_contacts(); - } - /** * Test message_search_users. */ diff --git a/tag/tests/taglib_test.php b/tag/tests/taglib_test.php index 1f9efba513305..275952ed4b4e6 100644 --- a/tag/tests/taglib_test.php +++ b/tag/tests/taglib_test.php @@ -40,39 +40,6 @@ public function setUp(): void { $this->resetAfterTest(); } - /** - * Test that the tag_set function throws an exception. - * This function was deprecated in 3.1 - */ - public function test_tag_set_get(): void { - $this->expectException('coding_exception'); - $this->expectExceptionMessage('tag_set() can not be used anymore. Please use ' . - 'core_tag_tag::set_item_tags().'); - tag_set(); - } - - /** - * Test that tag_set_add function throws an exception. - * This function was deprecated in 3.1 - */ - public function test_tag_set_add(): void { - $this->expectException('coding_exception'); - $this->expectExceptionMessage('tag_set_add() can not be used anymore. Please use ' . - 'core_tag_tag::add_item_tag().'); - tag_set_add(); - } - - /** - * Test that tag_set_delete function returns an exception. - * This function was deprecated in 3.1 - */ - public function test_tag_set_delete(): void { - $this->expectException('coding_exception'); - $this->expectExceptionMessage('tag_set_delete() can not be used anymore. Please use ' . - 'core_tag_tag::remove_item_tag().'); - tag_set_delete(); - } - /** * Test the core_tag_tag::add_item_tag() and core_tag_tag::remove_item_tag() functions. */ @@ -143,17 +110,6 @@ public function test_add_tag_ordering_calculation(): void { $this->assertEquals(1, $DB->get_field('tag_instance', 'ordering', ['id' => $ti5])); } - /** - * Test that tag_assign function throws an exception. - * This function was deprecated in 3.1 - */ - public function test_tag_assign(): void { - $this->expectException(\coding_exception::class); - $this->expectExceptionMessage('tag_assign() can not be used anymore. Please use core_tag_tag::set_item_tags() ' . - 'or core_tag_tag::add_item_tag() instead.'); - tag_assign(); - } - /** * Test the tag cleanup function used by the cron. */ @@ -834,17 +790,6 @@ public function test_move_tags_corrupted(): void { $this->assertEquals($collid2, $user2tags[2]->tagcollid); } - /** - * Tests that tag_normalize function throws an exception. - * This function was deprecated in 3.1 - */ - public function test_normalize(): void { - $this->expectException(\coding_exception::class); - $this->expectExceptionMessage('tag_normalize() can not be used anymore. Please use ' . - 'core_tag_tag::normalize().'); - tag_normalize(); - } - /** * Test functions core_tag_tag::create_if_missing() and core_tag_tag::get_by_name_bulk(). */ From 8a0d02d373c3654f0d5dc308bab0708ed636bf3f Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Thu, 27 Jun 2024 17:41:28 +0100 Subject: [PATCH 010/178] MDL-82326 calendar: correct course name formatting and shortening. --- calendar/renderer.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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) { From 012b65fd78d279039085090097639ad49be648cc Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Wed, 19 Jun 2024 23:23:08 +0100 Subject: [PATCH 011/178] MDL-82245 restore: use translated lang string in page heading. --- backup/util/helper/backup_helper.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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'); } } } From f5ead497c6870ed27379b4f86082c551dd885eb1 Mon Sep 17 00:00:00 2001 From: Leon Stringer Date: Fri, 28 Jun 2024 10:22:54 +0100 Subject: [PATCH 012/178] MDL-82323 core: Fix get_striptags must be bool If an exception occurs during install $CFG->formatstringstriptags may not be populated, if so fall back to a default value. --- lib/classes/formatting.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/classes/formatting.php b/lib/classes/formatting.php index 4576b80fc84cb..a003e6b5ef2c2 100644 --- a/lib/classes/formatting.php +++ b/lib/classes/formatting.php @@ -116,7 +116,7 @@ public function format_string( } // If the site requires it, strip ALL tags from this string. - if (!empty($this->get_striptags())) { + if ($this->get_striptags()) { if ($escape) { $string = str_replace(['<', '>'], ['<', '>'], strip_tags($string)); } else { @@ -387,7 +387,7 @@ public function get_striptags(): bool { return $this->striptags; } - return $CFG->formatstringstriptags; + return !empty($CFG->formatstringstriptags); } /** From 4fa3b63d19290240c3ae88096fe59d9584b9c66c Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Tue, 9 Apr 2024 23:33:58 +0100 Subject: [PATCH 013/178] MDL-81349 group: case-sensitive name validation when editing. --- group/group_form.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/group/group_form.php b/group/group_form.php index 01e1c86bad877..2c0114090947b 100644 --- a/group/group_form.php +++ b/group/group_form.php @@ -180,8 +180,8 @@ function validation($data, $files) { } else { $idnumber = ''; } - if ($data['id'] and $group = $DB->get_record('groups', array('id'=>$data['id']))) { - if (core_text::strtolower($group->name) != core_text::strtolower($name)) { + if ($data['id'] && $group = $DB->get_record('groups', ['id' => $data['id']])) { + if ($group->name != $name) { if (groups_get_group_by_name($COURSE->id, $name)) { $errors['name'] = get_string('groupnameexists', 'group', $name); } From 1f1d1a3920222a6832c4fd2e0c7e370831c439a4 Mon Sep 17 00:00:00 2001 From: meirzamoodle Date: Thu, 27 Jun 2024 17:40:27 +0700 Subject: [PATCH 014/178] MDL-66994 core: Remove display contact form setting in site registration Removing the display of the contact form causes deprecation of some language strings and unsets the site_contactable during the upgrade. --- lang/en/deprecated.txt | 4 ++++ lang/en/hub.php | 6 ++++-- lang/en/moodle.php | 4 ++-- lib/classes/hub/registration.php | 2 +- lib/classes/hub/site_registration_form.php | 11 ----------- lib/db/upgrade.php | 8 ++++++++ version.php | 2 +- 7 files changed, 20 insertions(+), 17 deletions(-) diff --git a/lang/en/deprecated.txt b/lang/en/deprecated.txt index b934616128dbf..8b27ec06d729a 100644 --- a/lang/en/deprecated.txt +++ b/lang/en/deprecated.txt @@ -116,3 +116,7 @@ coursecalendar,core_calendar importcalendarexternal,core_calendar nocalendarsubscriptions,core_calendar datechanged,core +siteregistrationcontact,core_hub +siteregistrationcontact_help,core_hub +registrationcontactno,core +registrationcontactyes,core \ No newline at end of file diff --git a/lang/en/hub.php b/lang/en/hub.php index 424a0c57b864b..a82ad1af2fc60 100644 --- a/lang/en/hub.php +++ b/lang/en/hub.php @@ -150,8 +150,6 @@ $string['siteprivacynotpublished'] = 'Do not list my site'; $string['siteprivacypublished'] = 'Only display my site name'; $string['siteprivacylinked'] = 'Display my site name with the link'; -$string['siteregistrationcontact'] = 'Display contact form'; -$string['siteregistrationcontact_help'] = 'If you allow it, other people in our Moodle community (who need a login account) can contact you via a form on our Moodle community site. However, they will never be able to see your email address.'; $string['siteregistrationemail'] = 'Notifications of new Moodle releases, security alerts and other important news'; $string['siteregistrationemail_help'] = 'You have the option of subscribing to our low-volume mailing list for notifications of new Moodle releases, security alerts and other important news. You may unsubscribe at any time.'; $string['siteregistrationupdated'] = 'Site registration updated'; @@ -178,3 +176,7 @@ // Deprecate since 3.11. $string['sitecommnewsno'] = 'No, I do not wish to receive any emails'; + +// Deprecated since 4.5. +$string['siteregistrationcontact'] = 'Display contact form'; +$string['siteregistrationcontact_help'] = 'If you allow it, other people in our Moodle community (who need a login account) can contact you via a form on our Moodle community site. However, they will never be able to see your email address.'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 9a27682c6c309..5fc2940c046ca 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1816,8 +1816,6 @@ $string['refreshingevents'] = 'Refreshing events'; $string['registration'] = 'Moodle registration'; $string['registrationcontact'] = 'Contact from the public'; -$string['registrationcontactno'] = 'No, I do not want to be contacted by other people'; -$string['registrationcontactyes'] = 'Yes, provide a form for other Moodlers to contact me'; $string['registrationemail'] = 'Email notifications'; $string['registrationinfo'] = '

This page allows you to register your Moodle site with moodle.org. Registration is free. The main benefit of registering is that you will be added to a low-volume mailing list @@ -2522,3 +2520,5 @@ // Deprecated since Moodle 4.5. $string['datechanged'] = 'Date changed'; +$string['registrationcontactno'] = 'No, I do not want to be contacted by other people'; +$string['registrationcontactyes'] = 'Yes, provide a form for other Moodlers to contact me'; diff --git a/lib/classes/hub/registration.php b/lib/classes/hub/registration.php index 6a301fa63155e..0b1cc88037df4 100644 --- a/lib/classes/hub/registration.php +++ b/lib/classes/hub/registration.php @@ -43,7 +43,7 @@ class registration { /** @var array Fields used in a site registration form. * IMPORTANT: any new fields with non-empty defaults have to be added to CONFIRM_NEW_FIELDS */ const FORM_FIELDS = ['policyagreed', 'language', 'countrycode', 'privacy', - 'contactemail', 'contactable', 'emailalert', 'emailalertemail', 'commnews', 'commnewsemail', + 'contactemail', 'emailalert', 'emailalertemail', 'commnews', 'commnewsemail', 'contactname', 'name', 'description', 'imageurl', 'contactphone', 'regioncode', 'geolocation', 'street']; /** @var array List of new FORM_FIELDS or siteinfo fields added indexed by the version when they were added. diff --git a/lib/classes/hub/site_registration_form.php b/lib/classes/hub/site_registration_form.php index e8b667e79b689..74ccb47b8827f 100644 --- a/lib/classes/hub/site_registration_form.php +++ b/lib/classes/hub/site_registration_form.php @@ -129,15 +129,6 @@ public function definition() { $mform->setType('contactemail', PARAM_EMAIL); $mform->addHelpButton('contactemail', 'siteemail', 'hub'); - $options = array(); - $options[0] = get_string("registrationcontactno"); - $options[1] = get_string("registrationcontactyes"); - $mform->addElement('select', 'contactable', get_string('siteregistrationcontact', 'hub'), $options); - $mform->setType('contactable', PARAM_INT); - $mform->addHelpButton('contactable', 'siteregistrationcontact', 'hub'); - $mform->hideIf('contactable', 'privacy', 'eq', registration::HUB_SITENOTPUBLISHED); - unset($options); - $this->add_checkbox_with_email('emailalert', 'siteregistrationemail', false, get_string('registrationyes')); $this->add_checkbox_with_email( @@ -300,8 +291,6 @@ public function get_data() { $data->commnewsemail = null; } unset($data->commnewsnewemail); - // Always return 'contactable'. - $data->contactable = empty($data->contactable) ? 0 : 1; if (debugging('', DEBUG_DEVELOPER)) { // Display debugging message for developers who added fields to the form and forgot to add them to registration::FORM_FIELDS. diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 22b4c9bdb8d46..754a5f6433f1d 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -1167,6 +1167,14 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2024041200.00); } + if ($oldversion < 2024062700.01) { + // Remove the site_contactable config of the hub plugin from config plugin table. + unset_config('site_contactable', 'hub'); + + // Main savepoint reached. + upgrade_main_savepoint(true, 2024062700.01); + } + // Automatically generated Moodle v4.4.0 release upgrade line. // Put any upgrade step following this. diff --git a/version.php b/version.php index b9719c5349254..0ca09eabadd9c 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2024062700.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2024062700.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '4.5dev (Build: 20240627)'; // Human-friendly version name From 0dbfd10c0c580fe7af0e7ff2ebd91fc105a01d43 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Tue, 25 Jun 2024 16:02:42 +0100 Subject: [PATCH 015/178] MDL-77834 mod_feedback: consistent HTML decoding of answer data. Restores functionality from 72629ee8c0 that went missing along the way. --- mod/feedback/classes/complete_form.php | 6 +----- mod/feedback/tests/behat/anonymous.feature | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/mod/feedback/classes/complete_form.php b/mod/feedback/classes/complete_form.php index 29e3190abcff2..af5cd44a4226b 100644 --- a/mod/feedback/classes/complete_form.php +++ b/mod/feedback/classes/complete_form.php @@ -301,10 +301,6 @@ public function add_form_element($item, $element, $addrequiredrule = true, $setd } else { // Add non-group element to the form. if (is_array($element)) { - if ($this->is_frozen() && $element[0] === 'text') { - // Convert 'text' element to 'static' when freezing for better display. - $element = ['static', $element[1], $element[2]]; - } $element = call_user_func_array(array($this->_form, 'createElement'), $element); } $element = $this->_form->addElement($element); @@ -323,7 +319,7 @@ public function add_form_element($item, $element, $addrequiredrule = true, $setd // Set default value. if ($setdefaultvalue && ($tmpvalue = $this->get_item_value($item))) { - $this->_form->setDefault($element->getName(), s($tmpvalue)); + $this->_form->setDefault($element->getName(), htmlspecialchars_decode($tmpvalue, ENT_QUOTES)); } // Freeze if needed. diff --git a/mod/feedback/tests/behat/anonymous.feature b/mod/feedback/tests/behat/anonymous.feature index 6b51733df173f..dd19589e35a02 100644 --- a/mod/feedback/tests/behat/anonymous.feature +++ b/mod/feedback/tests/behat/anonymous.feature @@ -251,8 +251,8 @@ Feature: Anonymous feedback And I should see "Non anonymous entries (1)" And I click on "," "link" in the "Username 1" "table_row" And I should see "(Username 1)" - And I should see "usertext" + Then the field "this is a short text answer" matches value "usertext" And I navigate to "Responses" in current page administration And I follow "Response number: 1" And I should see "Response number: 1 (Anonymous)" - Then I should see "anontext" + And the field "this is a short text answer" matches value "anontext" From 314fcb3cf80665af98e917d46ccc31fba99a6a2a Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Tue, 2 Jul 2024 15:52:56 +0100 Subject: [PATCH 016/178] MDL-82360 courseformat: remove console upload error string debugging. --- course/format/amd/build/local/courseeditor/fileuploader.min.js | 2 +- .../format/amd/build/local/courseeditor/fileuploader.min.js.map | 2 +- course/format/amd/src/local/courseeditor/fileuploader.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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":"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"} \ No newline at end of file diff --git a/course/format/amd/src/local/courseeditor/fileuploader.js b/course/format/amd/src/local/courseeditor/fileuploader.js index 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]; From 38a6f3fcaf9be4a5e612fc0846ffc4c3db5ecff3 Mon Sep 17 00:00:00 2001 From: djarrancotleanu Date: Wed, 3 Jul 2024 13:38:55 +1000 Subject: [PATCH 017/178] MDL-78388 course: Copy permissions when duplicating module --- course/lib.php | 17 +++++ course/tests/courselib_test.php | 75 +++++++++++++++++++ .../tests/behat/duplicate_permissions.feature | 62 +++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 mod/assign/tests/behat/duplicate_permissions.feature diff --git a/course/lib.php b/course/lib.php index 37a28627d1b70..3eca82d2fa232 100644 --- a/course/lib.php +++ b/course/lib.php @@ -3244,6 +3244,23 @@ function duplicate_module($course, $cm, int $sectionid = null, bool $changename // The following line is to be removed in MDL-58906. course_module_update_calendar_events($newcm->modname, null, $newcm); + // Copy permission overrides to new course module. + $newcmcontext = context_module::instance($newcm->id); + $overrides = $DB->get_records('role_capabilities', ['contextid' => $cmcontext->id]); + foreach ($overrides as $override) { + $override->contextid = $newcmcontext->id; + unset($override->id); + $DB->insert_record('role_capabilities', $override); + } + + // Copy locally assigned roles to new course module. + $overrides = $DB->get_records('role_assignments', ['contextid' => $cmcontext->id]); + foreach ($overrides as $override) { + $override->contextid = $newcmcontext->id; + unset($override->id); + $DB->insert_record('role_assignments', $override); + } + // Trigger course module created event. We can trigger the event only if we know the newcmid. $newcm = get_fast_modinfo($cm->course)->get_cm($newcmid); $event = \core\event\course_module_created::create_from_cm($newcm); diff --git a/course/tests/courselib_test.php b/course/tests/courselib_test.php index f811ec9ac6b20..cbdbb136e1347 100644 --- a/course/tests/courselib_test.php +++ b/course/tests/courselib_test.php @@ -3058,6 +3058,81 @@ public function test_duplicate_module(): void { } } + /** + * Test that permissions are duplicated correctly after duplicate_module(). + * @covers ::duplicate_module + * @return void + */ + public function test_duplicate_module_permissions(): void { + global $DB; + $this->setAdminUser(); + $this->resetAfterTest(); + + // Create course and course module. + $course = self::getDataGenerator()->create_course(); + $res = self::getDataGenerator()->create_module('assign', ['course' => $course]); + $cm = get_coursemodule_from_id('assign', $res->cmid, 0, false, MUST_EXIST); + $cmcontext = \context_module::instance($cm->id); + + // Enrol student user. + $user = self::getDataGenerator()->create_user(); + $roleid = $DB->get_field('role', 'id', ['shortname' => 'student'], MUST_EXIST); + self::getDataGenerator()->enrol_user($user->id, $course->id, $roleid); + + // Add capability to original course module. + assign_capability('gradereport/grader:view', CAP_ALLOW, $roleid, $cmcontext->id); + + // Duplicate module. + $newcm = duplicate_module($course, $cm); + $newcmcontext = \context_module::instance($newcm->id); + + // Assert that user still has capability. + $this->assertTrue(has_capability('gradereport/grader:view', $newcmcontext, $user)); + + // Assert that both modules contain the same count of overrides. + $overrides = $DB->get_records('role_capabilities', ['contextid' => $cmcontext->id]); + $newoverrides = $DB->get_records('role_capabilities', ['contextid' => $newcmcontext->id]); + $this->assertEquals(count($overrides), count($newoverrides)); + } + + /** + * Test that locally assigned roles are duplicated correctly after duplicate_module(). + * @covers ::duplicate_module + * @return void + */ + public function test_duplicate_module_role_assignments(): void { + global $DB; + $this->setAdminUser(); + $this->resetAfterTest(); + + // Create course and course module. + $course = self::getDataGenerator()->create_course(); + $res = self::getDataGenerator()->create_module('assign', ['course' => $course]); + $cm = get_coursemodule_from_id('assign', $res->cmid, 0, false, MUST_EXIST); + $cmcontext = \context_module::instance($cm->id); + + // Enrol student user. + $user = self::getDataGenerator()->create_user(); + $roleid = $DB->get_field('role', 'id', ['shortname' => 'student'], MUST_EXIST); + self::getDataGenerator()->enrol_user($user->id, $course->id, $roleid); + + // Assign user a new local role. + $newroleid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher'], MUST_EXIST); + role_assign($newroleid, $user->id, $cmcontext->id); + + // Duplicate module. + $newcm = duplicate_module($course, $cm); + $newcmcontext = \context_module::instance($newcm->id); + + // Assert that user still has role assigned. + $this->assertTrue(user_has_role_assignment($user->id, $newroleid, $newcmcontext->id)); + + // Assert that both modules contain the same count of overrides. + $overrides = $DB->get_records('role_assignments', ['contextid' => $cmcontext->id]); + $newoverrides = $DB->get_records('role_assignments', ['contextid' => $newcmcontext->id]); + $this->assertEquals(count($overrides), count($newoverrides)); + } + /** * Tests that when creating or updating a module, if the availability settings * are present but set to an empty tree, availability is set to null in diff --git a/mod/assign/tests/behat/duplicate_permissions.feature b/mod/assign/tests/behat/duplicate_permissions.feature new file mode 100644 index 0000000000000..5d560386727f7 --- /dev/null +++ b/mod/assign/tests/behat/duplicate_permissions.feature @@ -0,0 +1,62 @@ +@mod @mod_assign +Feature: Duplicate assign activity module with permissions + In order to ensure that locally assigned roles and permissions are correctly duplicated + As a teacher + I need to add the roles and permissions and ensure they are correctly duplicated + + Background: + Given the following "courses" exist: + | fullname | shortname | category | groupmode | + | Course 1 | C1 | 0 | 1 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student10@example.com | + | student2 | Student | 2 | student20@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + | student2 | C1 | student | + And the following "activity" exists: + | activity | assign | + | course | C1 | + | name | Test assignment name | + | intro | Test assignment description | + | markingworkflow | 1 | + | submissiondrafts | 0 | + + @javascript + Scenario: Add a locally assigned role and duplicate activity + Given I log in as "teacher 1" + And I am on "Course 1" course homepage with editing mode on + + When I open "Test assignment name" actions menu + And I click on "Assign roles" "link" in the "Test assignment name" activity + And I click on "Non-editing teacher" "link" + And I click on "Student 2" "option" + And I click on "Add" "button" + And I am on "Course 1" course homepage with editing mode on + And I duplicate "Test assignment name" activity + Then I should see "Test assignment name (copy)" + + And I open "Test assignment name (copy)" actions menu + And I click on "Assign roles" "link" in the "Test assignment name (copy)" activity + Then "Non-editing teacher" row "Users with role" column of "generaltable" table should contain "1" + + @javascript + Scenario: Add a permission override to activity and duplicate + Given the following "permission overrides" exist: + | capability | permission | role | contextlevel | reference | + | mod/assign:grade | Allow | student | 70 | Test assignment name | + And I log in as "admin" + + When I am on "Course 1" course homepage with editing mode on + And I duplicate "Test assignment name" activity + Then I should see "Test assignment name (copy)" + + And I open "Test assignment name (copy)" actions menu + And I click on "Edit settings" "link" in the "Test assignment name (copy)" activity + And I navigate to "Permissions" in current page administration + And I set the field "permissionscapabilitysearch" to "mod/assign:grade" + Then "mod/assign:grade" row "Roles with permission" column of "permissions" table should contain "Student" From 0beb2ec33a0e9a48dee56101d914eb03822e2c0e Mon Sep 17 00:00:00 2001 From: Safat Date: Mon, 20 May 2024 12:32:21 +1000 Subject: [PATCH 018/178] MDL-81932 core_communication: Fix communication provider change issue --- communication/classes/api.php | 12 ++-- communication/classes/helper.php | 15 +++-- communication/tests/api_test.php | 4 +- .../behat/communication_configuration.feature | 67 +++++++++++++++++++ 4 files changed, 87 insertions(+), 11 deletions(-) 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/tests/api_test.php b/communication/tests/api_test.php index bb1eb99d25e81..d7bcf7fbab02b 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); } 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" From c9887d8e599eec3db374ebaadf100a829127afb6 Mon Sep 17 00:00:00 2001 From: Safat Date: Thu, 4 Jul 2024 16:17:42 +1000 Subject: [PATCH 019/178] MDL-81932 communication_matrix: Fix behat tests --- .../tests/behat/matrix_form_fields.feature | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) 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" From efc7c6d87205b84a2e1895a23dfeddee33ddc0f9 Mon Sep 17 00:00:00 2001 From: Safat Date: Thu, 4 Jul 2024 16:17:58 +1000 Subject: [PATCH 020/178] MDL-81932 communication_customlink: Fix behat tests --- .../customlink/tests/behat/custom_link.feature | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 From ec57300048ff35227952895a24fbf47800efa457 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Thu, 4 Jul 2024 09:58:44 +0100 Subject: [PATCH 021/178] MDL-82090 completion: account for alternate gradepass form fields. Some modules (e.g. Workshop) have "*gradepass" fields for each grade item, which should be accounted for when trying to freeze them. --- completion/classes/form/form_trait.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) 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; From 23eee37e81ee645c9e3b2f6613a731365f7a7282 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Mon, 24 Jun 2024 17:21:12 +0100 Subject: [PATCH 022/178] MDL-82282 question: consider only editable flags in JS module. --- mod/quiz/tests/behat/flag_questions.feature | 15 +++++++++------ question/flags.js | 9 +++++---- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/mod/quiz/tests/behat/flag_questions.feature b/mod/quiz/tests/behat/flag_questions.feature index 29a415d41c0c5..7870e199aaa5a 100644 --- a/mod/quiz/tests/behat/flag_questions.feature +++ b/mod/quiz/tests/behat/flag_questions.feature @@ -46,12 +46,10 @@ Feature: Flag quiz questions Scenario: Flag a quiz during and after quiz attempt Given I am on the "Quiz 1" "quiz activity" page logged in as student1 And I press "Attempt quiz" - # flag question 1 - When I press "Flag question" + When I click on "Flag question" "button" in the "First question" "question" + Then I should see "Remove flag" in the "First question" "question" # Confirm question 1 is flagged in navigation - Then "Question 1 This page Flagged" "link" should exist - # Confirm that link in question 1 is changed to Remove flag - And I should see "Remove flag" in the "First question" "question" + And "Question 1 This page Flagged" "link" should exist # Answer questions And I click on "True" "radio" in the "First question" "question" And I press "Next page" @@ -64,9 +62,14 @@ Feature: Flag quiz questions # Confirm only flagged question is flagged And I should see "Remove flag" in the "First question" "question" And I should see "Flag question" in the "Second question" "question" - And I should see "Flag question" in the "Third question" "question" And I click on "Flagged" "button" in the "Second question" "question" + And I should see "Remove flag" in the "Second question" "question" + And I should see "Flag question" in the "Third question" "question" And I am on the "Quiz 1" "mod_quiz > Grades report" page logged in as teacher1 And "Flagged" "icon" should exist in the "Student 1" "table_row" And I am on the "Quiz 1" "mod_quiz > Responses report" page And "Flagged" "icon" should exist in the "Student 1" "table_row" + And I am on the "Quiz 1 > student1 > Attempt 1" "mod_quiz > Attempt review" page + And I should see "Remove flag" in the "First question" "question" + And I should see "Remove flag" in the "Second question" "question" + And I should see "Flag question" in the "Third question" "question" diff --git a/question/flags.js b/question/flags.js index 3720d2409b17f..a91d90cb2c1fb 100644 --- a/question/flags.js +++ b/question/flags.js @@ -29,12 +29,13 @@ M.core_question_flags = { flagattributes: null, actionurl: null, listeners: [], + editableSelector: 'div.questionflag.editable', init: function(Y, actionurl, flagattributes) { M.core_question_flags.flagattributes = flagattributes; M.core_question_flags.actionurl = actionurl; - Y.all('div.questionflag').each(function(flagdiv, i) { + Y.all(M.core_question_flags.editableSelector).each(function(flagdiv) { var checkbox = flagdiv.one('input[type=checkbox]'); if (!checkbox) { return; @@ -64,17 +65,17 @@ M.core_question_flags = { Y.delegate('click', function(e) { e.halt(); M.core_question_flags.process(this); - }, document.body, 'div.questionflag'); + }, document.body, M.core_question_flags.editableSelector); Y.delegate('key', function(e) { e.halt(); if (e.keyCode == 13) { M.core_question_flags.process(this); } - }, document.body, 'down:enter, space', 'div.questionflag'); + }, document.body, 'down:enter, space', M.core_question_flags.editableSelector); Y.delegate('key', function(e) { e.halt(); M.core_question_flags.process(this); - }, document.body, 'up:space', 'div.questionflag'); + }, document.body, 'up:space', M.core_question_flags.editableSelector); }, update_flag: function(input, toggle) { From d4ebc6644e3d8392da82e722eb959a845213e840 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Tue, 25 Jun 2024 17:05:09 +0100 Subject: [PATCH 023/178] MDL-82289 mod_feedback: correct comparison of current $course. --- mod/feedback/classes/output/responses_action_bar.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod/feedback/classes/output/responses_action_bar.php b/mod/feedback/classes/output/responses_action_bar.php index 0505a31869bb1..8afdb845c3bba 100644 --- a/mod/feedback/classes/output/responses_action_bar.php +++ b/mod/feedback/classes/output/responses_action_bar.php @@ -54,7 +54,7 @@ public function get_items(): array { $options[$reporturl->out(false)] = get_string('show_entries', 'feedback'); $selected = $this->currenturl->compare($reporturl, URL_MATCH_BASE) ? $reporturl : $this->currenturl; - if ($this->feedback->anonymous == FEEDBACK_ANONYMOUS_NO && $this->course != SITEID) { + if ($this->feedback->anonymous == FEEDBACK_ANONYMOUS_NO && $this->course->id != SITEID) { $nonrespondenturl = new moodle_url('/mod/feedback/show_nonrespondents.php', $this->urlparams); $options[$nonrespondenturl->out(false)] = get_string('show_nonrespondents', 'feedback'); $selected = $this->currenturl->compare($nonrespondenturl, URL_MATCH_BASE) ? $nonrespondenturl : $this->currenturl;; From c13d579b5e2bba02738fc4a391c04924d22dc4fe Mon Sep 17 00:00:00 2001 From: ferranrecio Date: Tue, 25 Jun 2024 17:00:37 +0200 Subject: [PATCH 024/178] MDL-81765 course: module delegated section logic Having mod_subsection into core was a pre-requisite to incorporate some phpunit and behat tests that are pending from previous issues. Also, some new sectiondelegatemodule methods are added tu support the new plugin. --- .../format/classes/sectiondelegatemodule.php | 58 +++++++++ course/format/tests/stateactions_test.php | 113 ++++++++++++++++++ lib/tests/modinfolib_test.php | 52 ++++++++ 3 files changed, 223 insertions(+) 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/tests/stateactions_test.php b/course/format/tests/stateactions_test.php index 1479592d28c55..9aa21ecdef8a9 100644 --- a/course/format/tests/stateactions_test.php +++ b/course/format/tests/stateactions_test.php @@ -18,6 +18,7 @@ use course_modinfo; use moodle_exception; +use ReflectionMethod; use stdClass; /** @@ -1419,4 +1420,116 @@ public function test_section_move_after_capabilities( 'section0' ); } + + /** + * Test that set_cm_indentation on activities with a delegated section. + * + * @covers ::set_cm_indentation + */ + public function test_set_cm_indentation_delegated_section(): void { + global $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $subsection = $this->getDataGenerator()->create_module('subsection', ['course' => $course]); + $otheractvity = $this->getDataGenerator()->create_module('forum', ['course' => $course]); + $this->setAdminUser(); + + // Initialise stateupdates. + $courseformat = course_get_format($course->id); + + // Execute given method. + $updates = new stateupdates($courseformat); + $actions = new stateactions(); + $actions->cm_moveright( + $updates, + $course, + [$subsection->cmid, $otheractvity->cmid], + ); + + // Format results in a way we can compare easily. + $results = $this->summarize_updates($updates); + + // The state actions does not use create or remove actions because they are designed + // to refresh parts of the state. + $this->assertEquals(0, $results['create']['count']); + $this->assertEquals(0, $results['remove']['count']); + + // Mod subsection should be ignored. + $this->assertEquals(1, $results['put']['count']); + + // Validate course, section and cm. + $this->assertArrayHasKey($otheractvity->cmid, $results['put']['cm']); + $this->assertArrayNotHasKey($subsection->cmid, $results['put']['cm']); + + // Validate activity indentation. + $mondinfo = get_fast_modinfo($course); + $this->assertEquals(1, $mondinfo->get_cm($otheractvity->cmid)->indent); + $this->assertEquals(1, $DB->get_field('course_modules', 'indent', ['id' => $otheractvity->cmid])); + $this->assertEquals(0, $mondinfo->get_cm($subsection->cmid)->indent); + $this->assertEquals(0, $DB->get_field('course_modules', 'indent', ['id' => $subsection->cmid])); + + // Now move left. + $updates = new stateupdates($courseformat); + $actions->cm_moveleft( + $updates, + $course, + [$subsection->cmid, $otheractvity->cmid], + ); + + // Format results in a way we can compare easily. + $results = $this->summarize_updates($updates); + + // The state actions does not use create or remove actions because they are designed + // to refresh parts of the state. + $this->assertEquals(0, $results['create']['count']); + $this->assertEquals(0, $results['remove']['count']); + + // Mod subsection should be ignored. + $this->assertEquals(1, $results['put']['count']); + + // Validate course, section and cm. + $this->assertArrayHasKey($otheractvity->cmid, $results['put']['cm']); + $this->assertArrayNotHasKey($subsection->cmid, $results['put']['cm']); + + // Validate activity indentation. + $mondinfo = get_fast_modinfo($course); + $this->assertEquals(0, $mondinfo->get_cm($otheractvity->cmid)->indent); + $this->assertEquals(0, $DB->get_field('course_modules', 'indent', ['id' => $otheractvity->cmid])); + $this->assertEquals(0, $mondinfo->get_cm($subsection->cmid)->indent); + $this->assertEquals(0, $DB->get_field('course_modules', 'indent', ['id' => $subsection->cmid])); + } + + /** + * Test for filter_cms_with_section_delegate protected method. + * + * @covers ::filter_cms_with_section_delegate + */ + public function test_filter_cms_with_section_delegate(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $subsection = $this->getDataGenerator()->create_module('subsection', ['course' => $course]); + $otheractvity = $this->getDataGenerator()->create_module('forum', ['course' => $course]); + $this->setAdminUser(); + + $courseformat = course_get_format($course->id); + + $modinfo = $courseformat->get_modinfo(); + $subsectioninfo = $modinfo->get_cm($subsection->cmid); + $otheractvityinfo = $modinfo->get_cm($otheractvity->cmid); + + $actions = new stateactions(); + + $method = new ReflectionMethod($actions, 'filter_cms_with_section_delegate'); + $result = $method->invoke($actions, [$subsectioninfo, $otheractvityinfo]); + + $this->assertCount(1, $result); + $this->assertArrayHasKey($otheractvity->cmid, $result); + $this->assertArrayNotHasKey($subsection->cmid, $result); + $this->assertEquals($otheractvityinfo, $result[$otheractvityinfo->id]); + } } diff --git a/lib/tests/modinfolib_test.php b/lib/tests/modinfolib_test.php index cd69f15250f33..e7f084ef9e9b7 100644 --- a/lib/tests/modinfolib_test.php +++ b/lib/tests/modinfolib_test.php @@ -1787,4 +1787,56 @@ public function test_multiple_modinfo_cache_purge(): void { $cacherevthree = $DB->get_field('course', 'cacherev', ['id' => $coursethree->id]); $this->assertGreaterThan($prevcacherevthree, $cacherevthree); } + + /** + * Test get_sections_delegated_by_cm method + * + * @covers \course_modinfo::get_sections_delegated_by_cm + */ + public function test_get_sections_delegated_by_cm(): void { + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(['numsections' => 1]); + + $modinfo = get_fast_modinfo($course); + $delegatedsections = $modinfo->get_sections_delegated_by_cm(); + $this->assertEmpty($delegatedsections); + + // Add a section delegated by a course module. + $subsection = $this->getDataGenerator()->create_module('subsection', ['course' => $course]); + $modinfo = get_fast_modinfo($course); + $delegatedsections = $modinfo->get_sections_delegated_by_cm(); + $this->assertCount(1, $delegatedsections); + $this->assertArrayHasKey($subsection->cmid, $delegatedsections); + + // Add a section delegated by a block. + formatactions::section($course)->create_delegated('block_site_main_menu', 1); + $modinfo = get_fast_modinfo($course); + $delegatedsections = $modinfo->get_sections_delegated_by_cm(); + // Sections delegated by a block shouldn't be returned. + $this->assertCount(1, $delegatedsections); + } + + /** + * Test get_sections_delegated_by_cm method + * + * @covers \cm_info::get_delegated_section_info + */ + public function test_get_delegated_section_info(): void { + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(['numsections' => 1]); + + // Add a section delegated by a course module. + $subsection = $this->getDataGenerator()->create_module('subsection', ['course' => $course]); + $otheractivity = $this->getDataGenerator()->create_module('page', ['course' => $course]); + + $modinfo = get_fast_modinfo($course); + $delegatedsections = $modinfo->get_sections_delegated_by_cm(); + + $delegated = $modinfo->get_cm($subsection->cmid)->get_delegated_section_info(); + $this->assertNotNull($delegated); + $this->assertEquals($delegated, $delegatedsections[$subsection->cmid]); + + $delegated = $modinfo->get_cm($otheractivity->cmid)->get_delegated_section_info(); + $this->assertNull($delegated); + } } From 72efec7fb6df56b3914f95cd59eb0ad0247dc8cd Mon Sep 17 00:00:00 2001 From: ferranrecio Date: Tue, 25 Jun 2024 17:01:43 +0200 Subject: [PATCH 025/178] MDL-81765 report_outline: add subsection tests --- .../tests/behat/subsection_reports.feature | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 report/outline/tests/behat/subsection_reports.feature diff --git a/report/outline/tests/behat/subsection_reports.feature b/report/outline/tests/behat/subsection_reports.feature new file mode 100644 index 0000000000000..e038b929337cb --- /dev/null +++ b/report/outline/tests/behat/subsection_reports.feature @@ -0,0 +1,31 @@ +@mod @mod_subsection @report +Feature: Subsections are shown in reports + In order to use reports + As an teacher + I need to see sections and subsections structure in reports + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | category | numsections | initsections | + | Course 1 | C1 | 0 | 1 | 1 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And the following "activities" exist: + | activity | name | course | idnumber | section | + | page | First page | C1 | page1 | 1 | + | subsection | Subsection 1 | C1 | subsection1 | 1 | + | page | Last page | C1 | last | 1 | + | page | Page in Subsection 1 | C1 | subpage | 2 | + And I log in as "teacher1" + + @report_outline + Scenario: Course Activity report show subsections' information + Given I am on "Course 1" course homepage + When I navigate to "Reports > Activity report" in current page administration + Then I should see "First page" in the "generaltable" "table" + And "Subsection" "table_row" should appear before "Last page" "table_row" + And "Page in Subsection 1" "table_row" should appear before "Last page" "table_row" From ae5fc380b5c1a511d56f03aa8a06d799f65c6575 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Fri, 7 Jun 2024 14:29:12 +0200 Subject: [PATCH 026/178] MDL-43938 badges: Merge newbadge.php and edit.php The newbadge.php and edit.php pages have been merged in edit.php to make them easier to maintain because they were quite similar (newbadge.php was only used for creating badges and edit.php for editing badge details and messages). --- .upgradenotes/MDL-43938-2024070407235835.yml | 7 + admin/settings/badges.php | 2 +- badges/classes/badge.php | 142 +++++++++ badges/classes/output/standard_action_bar.php | 15 +- badges/edit.php | 204 +++++++----- badges/newbadge.php | 108 +------ badges/tests/badge_test.php | 298 ++++++++++++++++++ lib/badgeslib.php | 2 +- 8 files changed, 586 insertions(+), 192 deletions(-) create mode 100644 .upgradenotes/MDL-43938-2024070407235835.yml create mode 100644 badges/tests/badge_test.php 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/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/badges/classes/badge.php b/badges/classes/badge.php index be27fe6d17743..777655582331f 100644 --- a/badges/classes/badge.php +++ b/badges/classes/badge.php @@ -991,4 +991,146 @@ 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 = $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; + } + + 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 = $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; + if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) { + $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/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/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/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/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/lib/badgeslib.php b/lib/badgeslib.php index 357aad0d3638f..6030c3aa2a005 100644 --- a/lib/badgeslib.php +++ b/lib/badgeslib.php @@ -517,7 +517,7 @@ function badges_add_course_navigation(navigation_node $coursenode, stdClass $cou navigation_node::TYPE_SETTING, null, 'coursebadges'); if (has_capability('moodle/badges:createbadge', $coursecontext)) { - $url = new moodle_url('/badges/newbadge.php', array('type' => BADGE_TYPE_COURSE, 'id' => $course->id)); + $url = new moodle_url('/badges/edit.php', ['action' => 'new', 'courseid' => $course->id]); $coursenode->get('coursebadges')->add(get_string('newbadge', 'badges'), $url, navigation_node::TYPE_SETTING, null, 'newbadge'); From 71aca6e27ee2c808d4df03161574dc13c8a653bc Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Fri, 7 Jun 2024 16:12:34 +0200 Subject: [PATCH 027/178] MDL-43938 badges: Fix coding style --- badges/classes/form/badge.php | 77 +++++++++++++++++------------------ 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/badges/classes/form/badge.php b/badges/classes/form/badge.php index f64f201e42c6b..a3d6ed6fba7f2 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 { @@ -51,13 +45,13 @@ public function definition() { $action = $this->_customdata['action']; $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 +64,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,16 +74,16 @@ 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']); @@ -97,7 +91,7 @@ public function definition() { if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) { $mform->addElement('header', 'issuerdetails', get_string('issuerdetails', 'badges')); - $mform->addElement('text', 'issuername', get_string('name'), array('size' => '70')); + $mform->addElement('text', 'issuername', get_string('name'), ['size' => '70']); $mform->setType('issuername', PARAM_NOTAGS); $mform->addRule('issuername', null, 'required'); if (isset($CFG->badges_defaultissuername)) { @@ -105,7 +99,7 @@ public function definition() { } $mform->addHelpButton('issuername', 'issuername', 'badges'); - $mform->addElement('text', 'issuercontact', get_string('contact', 'badges'), array('size' => '70')); + $mform->addElement('text', 'issuercontact', get_string('contact', 'badges'), ['size' => '70']); if (isset($CFG->badges_defaultissuercontact)) { $mform->setDefault('issuercontact', $CFG->badges_defaultissuercontact); } @@ -120,17 +114,17 @@ public function definition() { $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 +159,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 +167,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; @@ -219,11 +213,17 @@ public function validation($data, $files) { // 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)); + $duplicate = $DB->record_exists_select( + 'badge', + 'name = :name AND status != :deleted', + ['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)); + $duplicate = $DB->record_exists_select( + 'badge', + 'name = :name AND id != :badgeid AND status != :deleted', + ['name' => $data['name'], 'badgeid' => $data['id'], 'deleted' => BADGE_STATUS_ARCHIVED], + ); } if ($duplicate) { @@ -237,4 +237,3 @@ public function validation($data, $files) { return $errors; } } - From 8c0afe86aebc85f469ac9c22dc413ae6f2eea18c Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Tue, 25 Jun 2024 12:41:11 +0200 Subject: [PATCH 028/178] MDL-68211 feedback: Export percentage format The feedback was exporting the percentage in the Analysis file using a wrong value for the format, so instead of having only 2 decimals, the number was not formatted. --- mod/feedback/analysis_to_excel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod/feedback/analysis_to_excel.php b/mod/feedback/analysis_to_excel.php index 24adeb82d448a..152f898f00621 100644 --- a/mod/feedback/analysis_to_excel.php +++ b/mod/feedback/analysis_to_excel.php @@ -73,7 +73,7 @@ $xlsformats->head2 = $workbook->add_format(['align' => 'left', 'bold' => 1, 'bottum' => 2]); $xlsformats->default = $workbook->add_format(['align' => 'left', 'v_align' => 'top']); $xlsformats->value_bold = $workbook->add_format(['align' => 'left', 'bold' => 1, 'v_align' => 'top']); -$xlsformats->procent = $workbook->add_format(['align' => 'left', 'bold' => 1, 'v_align' => 'top', 'num_format' => '#,##0.00%']); +$xlsformats->procent = $workbook->add_format(['align' => 'left', 'bold' => 1, 'v_align' => 'top', 'num_format' => '#,##0.00']); // Writing the table header. $rowoffset1 = 0; From 86b2a799e13cf57dbc8dd5f4e97eb57cc2e2ab2b Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Thu, 27 Jun 2024 13:19:06 +0200 Subject: [PATCH 029/178] MDL-81661 theme: Update FontAwesome to 6.5.2 --- lib/fonts/fa-brands-400.ttf | Bin 207972 -> 209128 bytes lib/fonts/fa-brands-400.woff2 | Bin 117372 -> 117860 bytes lib/fonts/fa-regular-400.ttf | Bin 68004 -> 67860 bytes lib/fonts/fa-regular-400.woff2 | Bin 25452 -> 25392 bytes lib/fonts/fa-solid-900.ttf | Bin 419720 -> 420332 bytes lib/fonts/fa-solid-900.woff2 | Bin 156496 -> 156404 bytes lib/fonts/fa-v4compatibility.ttf | Bin 10832 -> 10832 bytes lib/fonts/fa-v4compatibility.woff2 | Bin 4792 -> 4792 bytes theme/boost/scss/fontawesome/LICENSE.txt | 4 +- theme/boost/scss/fontawesome/_animated.scss | 1 - .../scss/fontawesome/_bordered-pulled.scss | 4 +- theme/boost/scss/fontawesome/_mixins.scss | 3 - .../scss/fontawesome/_rotated-flipped.scss | 4 +- theme/boost/scss/fontawesome/_shims.scss | 464 ------------------ theme/boost/scss/fontawesome/_variables.scss | 18 + theme/boost/scss/fontawesome/brands.scss | 8 +- theme/boost/scss/fontawesome/fontawesome.scss | 4 +- theme/boost/scss/fontawesome/regular.scss | 8 +- theme/boost/scss/fontawesome/solid.scss | 8 +- theme/boost/scss/fontawesome/v4-shims.scss | 4 +- 20 files changed, 40 insertions(+), 490 deletions(-) diff --git a/lib/fonts/fa-brands-400.ttf b/lib/fonts/fa-brands-400.ttf index 5efb1d4f96407d7019631b361b571ea454f6ce09..8c8a402329635864ed6ece2b50f4bbcbd158b839 100644 GIT binary patch delta 7365 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 delta 6324 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 diff --git a/lib/fonts/fa-brands-400.woff2 b/lib/fonts/fa-brands-400.woff2 index 36fbda7d334c3ecea7fc1c378c84bd9b3e4d9be4..64bf1ea454dd543dc8ef4be7720190edd7d7a7fb 100644 GIT binary patch literal 117860 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 Pe& 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 V!DH6x8}&I3KU>H5y@3k@0|2$XHs=5U literal 117372 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


Fu2c+838Y&)^E+3-Bm4W3Quvmd7ZA;T~L2aTdA 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 F_!s#n;VS?D diff --git a/lib/fonts/fa-regular-400.woff2 b/lib/fonts/fa-regular-400.woff2 index b6cabbacb67f4ac88248ef235c5d7a5361f7003b..ed14925f29677750a66d06d056a0b5d9dbb07e58 100644 GIT binary patch delta 25281 zcmV)mK%T$s#sRR#0ULKuMlt{a00000V=w>;00000{(%4hV&ww`1(6#i47v!*gDM4x z1qUDnK#@!_f4RF^D_ODN)Z7Y~Ql69iD+WTurPp>qiHCq->5~CH5B2_me>TbV@g&m| zDv1uR;!5%dT#@hYeK>uaS;ty~w9;DA$YIB_rC^`eAO(KglsNw|N8EDmOFVD4)9@^N9QVsh{(R`-sR351-w?clQOtM0#Mv5BOpBd1ue=e6>lc zROwWKe?^HpwNQz*;Q#;9lsmog^3&c$X@{p17`iNl`Mnu|_a%Xu-36`M9;Hg_s#QjA zwV@Q2nU!0cU9x5=9XW^h#)1rB5*8Xv6HGz?jQ{_(^uPQgr5Gvtz&XJ{2!KIy_B|Ec zf}pBfK{W)`mV=h;pjsMe>AhRGs!LtnEvY5@fBDINM6w^z2ky2$NR|Q_0(;Pqcru9a zA=qyKFq$CJ@CVHNvq{3A|25I169+GxG#DGBm9t)@frW_Pr-jUw+D5y-!WOc+0>RSDxZXI@TB@v>1As>EecdI7xnL0Bm*ryO<|U?1dAk<)NO7EcbjTYtF6F&MYRgd|8N>^C zKztw`Oz|S(bsmWiw|GEs9*M_!C?4mre|VjzZuQWI-qo8&om;%|8TbE65PyJporg4T zy0kCNSn|mx$4%~|<#e9&WaIZb=dlr1?L35iQl7%}5eyFz;`3+7m*dC!Ug&Y28D8gM z8sB*q?$FhHohSL#IuAA{eX2f_`QF3vd*c?L^ThDRrQDNwkV-!lPgq8HQC|1Of1Rh2 zuNxoY&3O2o^wx26+w>vSQ=hpd8~O5klaJ&yQg2OY*IFUosB5?l;dHaP48CWQ`O5Xq zdgvs)(dRtLXFU3<)_I&q)>Agi<{%?gU#AZz-n-{K2wM;@v=bVBy@!Zcz#s4f(*140 zrTC4!ARRv{hW#&V{e#^g?+66(1r8}>kVBL611S_*X4>tvXBj&)-p_cP@x;Oku@bD4 zFa#8T9bi|mP1|J`+4b#)c00Sh-N7!m``PPq;&S41(sNq>IPb@2>Zbu3s6iU8ks773 z8m9@Gs_B}qwY0uA(1zMsyJ>eFsDpKcj?%F@UZ<&7=j#GpsEc&5uGRIrLAU8n-K{;W zvuEsi``o^}WKQF2LXDJ$iqqEwZdQb!s~ z8yPE8WxC9ixw1tr$Q8LS59E=&l8^FTe#kHRtx>dsR@5q5U2AG1ZKA!kzYf$vI!Y($ zWSyc@b*9eI#kyDb=>a{chxD)>(W81?PwFYXskim9ehGvI(gzv^)&y>N(Y-WYL9dvT zLIo{|Fc%00Q-+C+P3e;sh&yB~c*PwDlCs{UgQ@Q3+x{nh@i|LVc#!Dqoo!F$0w zyd+4vDQPZhwRN^X39v}E!bFA=vRrM%el&YsdwdVz$4 zcFB~AM1iCN_3JM2AhB#k5)D$9&laDjEYCB9VO#|X1_=c52QgCHXE9^5Q~NGZ*SkJL zZ;($unIyA4Eg1OyUjAQF2%&@#e@+CEL=jC4vBVKi0*NG%ObV%_kxm8{Hko9RO%A!_ zkxv1|lu$}#s!*L8)T9n|sYe4E(wOG7rVZ`sKu5~yOjo+ogP!zaj@IGY`@NpZG`!4V z9*bGRN>;OmwXA0Y8`;DbcCeE@>}4POImBU(aFk;l=L9D?g=_TL<}K_de+__?_nrcz z0U1k-Hzpg%0Rn(Rpehgq)Bw5wAwX}KNT8320{WV0pr45W`kPo_fQbVJns{K4NdN|$ zL|}+X0*0DoV3NNnsi{4$pA(h3m9W;V64dm#+fW&yvYV8m>giD$pt2v zJYcfP2d0<;;E`1b|&1*a3D! zhHVER9PIWC+W~_=Z~JyR*xG&oiosq71VT<6a1wIj88#h2IpnkkO5n$NKq3723@8Hi zi=hpuKWIRCz6Js)0Sy95LBoNappig-&?uk`G#1zc8VBqLe@y_ogQfzNK+}QBp!q-* z&{{xM(E30%&;~#+(1r=<30e-?8K@50&9JrI0n`8;2n2!-1_D4w0JT6z0kuKL0v^!u z38(`)6?7U<7u0Kj`k?cH2B1BR1KNQd53~n60VoT0BJ^FTGQ&kgszeFJWreG6_w{70~#fSON^xK1f2KK1gB%Gy+KiX5J`}F(6~-3SXvLU=7Ih7*>PK0GYY@G8fD*y9F_H0XYG3Veatd3V?nf_ksQ( z57=)Xfq89T0T=-C5f}*a9T)`igRT7qU@*vUAP#7h1PlQ!2U-Cb3R=+s!$GS6BS5PI zqd;r+f5J%6+Mtc%Y!f745@-+5-oRwg{-zhEgAM>4Xzd^XJ3vPP=XdA zK&LY7OaP&vbAWK5ixW@;bT{Z;U^eJJ1Iz_I0L%kD$gqb1%m+Qputxwa06ogE$B}^C zpeI020t-P;nFK5Xy$*U4SPXjG02_MX|L?$YfBydqoS>=!PEs|Gaf)g>)hu1?S7!mv zQ+*6BP-=sVlqO&gr6;&V=?5-T27oJ+LEtK72)ITW2Ch?PgBz6fU<&0bxJmg2+@kyp z_K>YXU9tnHM|K5u$N``hIThR{=YI&?Ay<=YaF^Uj?!cKNuYEQ zxrH)82am)KXkdT?!_WYTV;+Y}dNscmDjs{GCl!zVn(wK2B8ruYf8q&69(ht} zT;mfe2!l{o*4Nk9N1uD{x%Ktt`uh6%hrDlL@9$^`2Mdlc5Dnq9la&2~Q-(f2ge*k;JRsfqgS2Y!@$g@?D zr*U6Zp@l<15ypifif)k;i$=PusuEW#$nUCpRJXAjW9x=>EhQeOPjG6Xp@8$@h$X zeO4__+UV%$5u-=o%S%Q)?G2}krLaelMeXKVr>WcwYgV+TIL zwJ28=eT{2e^XOG;J=hy@jcZ{L24N7JuT<5m)?6MJHdGWxaYR!W`pT_wc$-Ha#lFUk z8yOKx4wS|x(wc6NGnmh_`j|10Obm=qM7exkx>;_!Bg0ODC3{o#;Mwf2QNXPiV2MkAMwq8o?$lpDG zq1FEvUWrW@my;4-X`H;8l7XM3rlL3qebsoA5wR9AMlIjm@ausLcTB}@;xe|tg;@Yw zYXszx=N_ViFbEs5#+A~zD~&-EM{z^SN`cMI?YisS9CP2hRT(cJe-_3oH3%fxBd^1) zVPU*-BsC)g{Ei0vA&dgp^)TSP-y35k+iPpBd~@Ulx}hH^)56|$g&3m)4u=(b*oN?xg#k|}7|o-av}KeuM#ZJApAr`P7K#=}De*vq>@f zDRZjn?F3cnU|wQp5KVeZR3-lEWj# zkV?goVOd)IHo8(!ArF-Ock;re-~dbp<4rf6<8SW%&wD<7=G>fga5 z>v*-Td1s;KQCwD0To21C;E7ZY%e_Z(sjQ1DIvTwz-R$lTT$p@=9}O5-{Y|W33+k{3 z%sV0~o~u}$;eQ%O>l)dQ8nccoq05M3dr^6aWhAdyD0+lAUiU|Nv}glt!BC;5zC72(Ier| z5VCTaVX;`-B_GNlqm>a`Hw@h#SzTIUcTTyE<4(CE0D$%1=|6(Uts&AQD-@nJ{t)9> z)s&|wj(@_0?brNRdG~9UrEPG>%@j9_8Q0+(yWe=@jUWFwo+%JTZ_UkcXX{pnXIyOd zo_P3S&{e@WDA-w|Wj!LtG9ATIvbpEi{KoV?SB}^ zKZqyb6)*{afv=RvE>Vr6l|9P~uh>tQYay;iyX_WLtI=+|MV1s|c7A?-es)#}A!hw& zjS$kpE%kb|`|eS_E`-d@n5JpY%q=V|EX>ZDLYT8s3Ner6ABioHFsBoyB4^Sm_$U;0 z?SE?d*Fw&7M1J5ts>)bZ?Q`HfnpXb69jmp`F~qUaT6M<*nR3WA%aXn;O>?EDf1W554^} z>{_X0&Lzg^a+%dJy~7S4?f43-+na`!tL%nrU%n2`Oz+Hyn6BqOvarbd|3z4AoSU*V?gux|>7ocNPFJ*}lZ-{Rxugys7w9Q?@ z4TD>jcndEd5WUw?iUCayXb~>In$5o2>+JI~sRfVWk@!Qz4T(qwH10!ike7Pb`Sa z@=9V5bo~Bz%xA_2yjAniM zU{94)mB*(L>vMHcVV24^Mi+x>HAGDb6+MzXoAIt1ZxCXrEp5B35I(zjU54Kgm57kR z!q(Qp0Q0+((dgQ>(FpnUFCS_fENuNH_3+R3(dgP=?u&DT`gh}}e6fpzmr!X;DeD(J zG(P%K)j2TAIe&eT2KZexC_jY{2Gj`4J%-3|KQ!9g)BheHk#qP-Or3iXwHrv8O1tfU zz)onq;nk|^s;_>75eCXWbshE5M^(pFUr+-4ZY3yx5N~r3o$`n9N}w2FxyKP5ytVvE z2c2DWPNl!C71OQUvjWYMtWQ-TvNSBkI>*L0+T7>g^nXp^Jcu^Ihu`B#Ou_>K5YuEqR6&JMf( zvCk~`7k;6y+VNd@zy(rgSe;_l$tqsRPB>MK@>g(<4}0F^R5fs={F8vH&1P1q z*LH(8Gk@{1jKU+J{cp%&tS%sE=ZCsiW>K6aSwD`cx*9`dQJ!a!kTV5gr_%|uXs_aM zT=zUg&#U8f?XCK*#jyD<2l=}Ux@@^0<5=$ELAi?u$jd25JV4K@m$N4if+UOKm?K@b zkhgdDZ*oL_b7t7zTz%%S1n@gy5ncr#5|Jd?#eXM&l|L#*MVh4o!nNYEl=x|qWoecK zJFH(z!YmRZE3!N|x=&S_W@$#Vl%~4l-|mkkN&4IhVy8XaUS+1LDeCI>u-!pi`J80C zu5F{|RVuYOiV)kM`&=6_isD+O;(167g9e0uMd%by^Y zg@3nV5NMi0>$Q1lsr89Zw3ej#TAeDI76eEs5D1Tdf-K?;25=DoBOx-8MIxd!6ErxM z^xi9qqk1Z&1y#3GS|L)g4R866kR;gV^xU^{MK+$P5zDgQ_deUQNbMOzR`|D`qkK3V zD)#H|)i8YAG#?9ye(&pTWr#DhKNLxf8GlcI2QxH!`7+gY_8m_fj7ialY`pWyC!aJW z_a5E4v97A?H!S@gS2CXj0FVKWXE?(dSdhRFZcpq&)etXUvv7_A>I?&_Dx(pGQm51D zhbQG;FF`=nxRZ60{t#1427?6u^-&I8x7qA=n@!hUdmUHs;qv47uom}LA6kujT7O1X z^D7Ts$ybT4+2xyUO~>(aC5kG`%SVVC4bOGGMk8MOU*61GEyPwU^RDEpgskRQY)#iS z+v`iF@=`Sz!exN@K;k`OxaNl)(c?%$w*FjNETt+f!eJN)uoP$Wk??S?o@GUmW%aqW zuTiGh>7L%Y_13M^-41^Kss7U3^?#Y^-9w^?%r;h58gp}vm6eU~*`n&CLt9 zLp}fe^UvFKYrg~cF~EMl zI=BUKH|Y;^T_lOd^o4f>NjJ%|-b=MG4cg!##vGC3j3e@LJchay=xK?YQ-AE%Ve77w zs6IPB|Hxk2cnw`=?I#DghrHa?hi%3&M`t$*!p<6x8-VlMF^P6pGUKMB&*N+^gfpDM za$*E)yPcbbY)6cW-ltlV7k6D;F)V1&)C|jd8f$k1#;49-?gog=cJ*Gr9~YxZta0!mDOihtR)$ntx?(Z>u`_ndr>qHemVeB$MoU$&KoY#8h{p8awdT2Z4B zm49MZs_4vDs{v9k%XG_sbWO`L-9TiSJ$$!iS+3hS**@ zU1})y%Tc2dSz(x9wK`w^qX|MYXmS%eu4!dd8mt|@7XViQHGfJSKwQ%WCUa!EsVpMQ z_?CsFO`9_9x=j$!^VVHuY-NQp*Iik0-SusV``En&yxI=h*K^E_Ts-y`zB)gbqM|rh znx-J1d6Ady*mb9eNQd{>XNJd&P2s#Zj;r<&_!@xuHNOf<%!J8_5S@ZZ5%l8F-K1b3 zJ2%^s`W#Vzsec>xlQ`?rP%E_?GE%A(+xvS|#{A8?=OVV+XaB8Q(vWSc#0Ud*gzxDP z@bA@wu3@0()_cAe5fa&q)?}TbLIPC}$&XVD{ZHo5ad~`1Zp}eAUxw@O&KeFXOsLwR zf+HkGG3h5XjgpQP#kfk8%Q3zwHl<`>i7btyP^BvECVxSe)>JzjUUH8>Y-fW(s}cC7 z=?9J0V36H+B0t|t)2*#E?ZrY^ied>7e?E<=D5enc=j&RxFCszDt7{rYqASWCfw+gh z*Q_@hLNprnrsw1PZ=g7l3m7 zU&lX6-hYc<=A4ns@EAPjM=eB>=Fw~>T!3AP=z6-(sMGdHL?V*Wa4lI);;tc}A`!`` z42_B;i**ph`$;0E6Ubmd_G`6Bd~P{p;&aP+tPupJEO+c_^2HOKy>A}fJ}&!UcwTkH zus1)-;*-B{?b{!RG@4SA2j6*1_*J~&0#mn*7?0sxJ9@~68e>As0+39qg5nIsc zHBRe8Fm%y@wlv8vS9j85(3mcy$`g5iXz=3t`t|W2yGhsA z4+et)$Msu1^1;FSIsn0n4-a?%6}S_i7=MW<-mR)srNPX+!l`SoXCR;bcOK66TsC*)aFI4ZR^ zB5$|ro`-(Tm!_ma(T}4eh#`YpDmvYfNX4i&k`bm&iuyDR+?Y`jb~>FWAQW{Y%-%xf zZYv0I1{FueBO8Uc-8a#&dhh{zDZcELMG?#{ptIuO3x7MojUKCb(odA@H zI88dy29?rdyThXnE-qfayts(CxOh4697fejO_mAn;4(oTnkwaIHz*aA*%800c21ol zc(WoX-8jpsVuTzKmyxU&_kXBolRkDLjzp3WYZa9;#|u{CIH=TWtJ(GknzioYYQIlN zzrVWL?-NCneITn2e!Rmn502W>>CMJdR^|C`(dZLYteFIf6(|jxT4K zBC4p)&#x?YYfVW~6hf#eNtz_1>((`Cxn4uarA~+I_ywrJVimIuO@E?jR&r!{Coax= zh}Jg*PNQ_M(P%V2ewbI{Pb*&eHb(DEmtK>I4>{7?PMtdCuc9&DOaABaZ{b()MM&X6 zcp3l$6fwr8I^US*DY(UGk}Z8`-*E)ai-%z+JNN@sKnK%aTN!_x%c*vypd5blptz^V zEbzmnlP8lt=cYo4f`7WsmYm40h)~iL<_F8mr#4Sjs)qIUw3C~Ed3bKnY+70r$WA}W zBTWu)4;4Zblkh(~2QYO3BExsJxB0)?Al1@^){?Z@jMo_mF>_kdJfx$KBmsj>rq8qoX6yl0VuNGWp%K zkw#q-xH=Aj08YV%EqDr^gLlI>0AQTQQ5a^xV@WM5H%{S=>;b<;k?0RcMSr+f^oRHZ z0#uj@6q51#2oMiNp-TVvRT__c!0x-b??;evw5?PG6o2hp32B@!0Q|WQXW#dI-$&Z} zjSO_1RghoxQPe{e8e@_Cv7BSO9v(J(t=96-9`b9eM@Fn&u$q=rCQ!ZZ}T{&6XuAE#G#9>k8b&!Ei)A7)+3(=hf?;hnhC< z46Ehau75$Www3f^dNExg3I@Hr63g(MIgF6UV&r7`nUknv$C zfeJCKed4eM&%j&Y1MnRHemD$Ad2R^!DixTFoqxhKOEV1KB5K@E)VQ0dG0urjT9ZP8 zIY$Lp$e~dliT{;h;&HJ#Z;uGjL@8Mn8{bu!$h=`U};#tkQT~0^SaOO)tt7I4imuSbZjI&{D1@ zjDK$>x>{^J7-o4f*7G6!V(2HsS+&t|ht>>LX|D8LNwIoTY3iacynXt(z) zrrf!ow_+Cle8A2zeGCak9VP##X!C z!qV_=Im59 z`=kn)P(@&lwj66rXp1JM6(6M#u}vZ0MqKE@wjT8^i!91liz0hPK*~!Je`6d#;2n=S ze%A2==OwM_hv37_Ua$G#{huI@K5SVZjyS&NrI%jfi2S8zg?RSvb}6R^k%=jO!i0bRDNF zBJ?3Ff?vcLEWsIg1$t8qB7b!3^O<_B3HS-L;Ni8TABQl;VweTj!f8gusPN-FPLDWT zM@-k(;~wH7*Hy}KpHoHSi-^5=UDsLKYU7|hO!91gDU8SCsqfcnzK=6qXTG(#c&d7# z@GaX$v~9~TE>uq~E?Pd*^<8YY(&}wnTenryRvXKMn(zC54b!s!>NUxJj-$&!j5%m*pCRWAYogXLQrJXu#?BR zTkGRvZ8`RJ3vo6lc5Pld>1(aa`8uRea_-h@1$G#@M{}*ZgB&;(D-`Re?m@<_c z71_tqz4GAbfjSbznSU!nk?`dqyFH(0uih}L55UPDf7uEG+-U^?#!}Y*Kc%{MeWN{h zFbG;V|8r+&Cn8>6bG`JXkwiP?^_`s^eAI4n4bPM4W<_dNW{7?pTF5=CUH zQMlud{&PJ3L#jex7!6#{WMFzyh2^9Jwqf0vaYu53X`PI3VKs4N$jFK;6eD2+N1#S- zQef4LSp=F%vwx|F_AKu@=Nf}SW6rg?>vmDsZKTiwTi0RQ^Eth?P z?x+}2)qmNoi?u!(h&zn%kDVnzzXvyuj*dM4RgTCnNg*nf9LJgbFN}PQ%f{dY`7wBI z+d96x!5RL!b7@;$&hCv%X~?anxWshx7cP(-QXe&9>z$v`BbBax1gt8+DV=Ho=FV#=7Y%NRUev%IRN#9fvm3NqAA|NCH&pISfkA39r?WEDL z6uzm0NI5?K_}FnAU1zeTf55WdZ`j;0oRp+!EkPi3O;uD$l8K9GB9Ux>(-TQX&!1y| zY4n&A@EHNoD5Dx$Ywyo<&f6R@3VX~WIDa~8848Ba3m(MLX zn~ti;5+O(&iy|T0>w%J@v%2r$p~>)so6dMkT{t<9Gd504D(x}j2j$f;^v*9>RWP$g5u zM!NmS+Mda6M4Ov?+J2`sW)aMju>x1JD6&yLaA|kD;WV_yKAW+hhqF=PCgF={pbkB5 zQ7HkW56O*fi2Hj1(aZbY__1MrDu2Rr^avpXP)3ED@#vYuU@gfB6USWvpv{JDsK+tK z3Dfn1ZM#qc#}<6L=0D&El*c>Bv90TDg4uF$KQO!b0l!}UZi6^a#n_)QTW}dbj9xrm zk^<1i&AYPQpn3!#_u3)JF7seVB1(YRopSBd>vBsi2&~yHnJ`^1k2vBXf`7~w^SnCF z*Zl|lV~}DBQkJpO^`C;r;avdOw=QfN-_i{$U`{l^vdv+c<_PRGa*0T4fXxd3LsMeF zv#kWwSipFrVessKC^4RER;#+I+G{rVoIKQ3Eed2+^E8#R_i#krk|a$=%T{DfLfw}U z`J;ie?K9KcYIGm1dmakJ3V%u-qUY5gqKcw1)+m3E>AJ47G1GNjXJh^GFV4D* zpE74(A`~+I2lj~StWcHCLu#yHYF}klL}AL`Ka+mWH?*g3>oC3{&Lj)(U+*jTEFb^h z@lO&7zX-R$SHlP4TXV_@*kRG+e}`uISt4_uStvxNypi=+Ns=tdu(Tq=3QOc#=8~*p zkswGVQ59IB4Mv}c9DmOw7?Oj)8SbjLo|xtXZpZ&UvlYoXUv)cCdgLGv}3=k0&RhD$aAP7~s-xuPzim1pkQ7wZY>IPj! zq!$clDs=@vX@69cfz0xXwNaN`2xKgD4O4TbVdZ3-wz(?N0`f^`Wa0zua6bDsUH$Wx zKrM=F+qR>qHkFQfId0dsBT=o2YQ#eX7y98EP{2jFA0C4@S(ZF_7=$GJjIf=1)1s9w>21u@@CBVQ@OA za$f4-bNmv<*Y3kV;tW>cGTa4^!L#u703vw0AU9&)EraX9`-A)RmnLYW?O9Gkv; z%5jQ(%yD-QyhSzBQOJ$Q;|X-m+gd;l_W=~H>%Lvwte^PzmdNn7guntfFiS~dfbLFG z?K5HoS)dE=ZSB*A76o#ux~^KSs;;Z1au8{g*MDBE0#3`RD!L<%vr~<}e}Dc{AZyWq zgXr9KmxJgWM4BAjeYb_4>M|S(c_jd%wod!NYU=YaWdjg3^O%-EXvfwHPHyqoW)!zuh>m zXqvKMDY~u;Qkfl8kL1U%OVume*& z?uO-fflQ_03`(%G!^jVH9jzGW8|(OGsn9ali7(o;o*!;o%< zB`|I-C5S{3Ej0V|AbpCF7$Y15!j(<}qutnT@JIX1Flf)^h}`W_L&tkP&kj7@vVU|h zuzd>khtuiQP$U}5+>;bjNipPRzu)w9I|wS3Ah2~$4qW9SWbmLDL@+P0DJYA~6&dpW z<&w-aM4b)$OZH)+>ui_l`m!XOh^8zp&uqNm!hY-LGzps5O(ad{VeVZRGrs0;g?qqa z>!{ZC97Q1&8XhW^eUvfRO&LpFmw&NVYq!V?8XqKr@(*3Nl5r_xuG?yLANkRSae`vz z^(LHx%WxmOmivT~VhC9;MZz4*Y+{NKj4eYdjfA&mLnKB;i>-u5LKev~zC?=@8kwL% zd`b&s_O**l4Fgdar+;AIKDu&cG>PiNW2dkI^)Lno4IXE zW8yeoIbOqi%kRN^e-Gb;cYl8R)1Us0^e0T;*Ee6sbi=xBO`#gcgTOTPW2k-ci(mXV z%s+7c{P}+68 zxH#l=_$BA`!_@7|V9#_*Vx-`{{c4Cx146|NZyhuThmm zkq}WtRI1%Cl-nthq;BiPc~#TY^CwzeNh0)iMSMRZ68tg}>544Nh=~75h}C(-^Ll%WuB)VS`_efM8+9ruGjYzpioj|E;Zuwj5IDhHJAAEbxojv+hONLpBsVbAy{zoS(Y^!3Pj>3!bOamp~9Ko-JwYnD!I+P9a+nk z_|w32{cP{+f0S3Nt+drf|4)8sYisL!+jIpNxu#H#%_xOJC9K)*yk+K2^wM?a^Wy7X zsBoS|(SLRM`+v%RPt$bu)Vy^*-NJ6CQ?vm7t+&X|NYWo>8KYsY81<02{S2ZY*hbxi zdF+p;8$wju?W*fAmgMZaXZMQ3x3S*wr?^jvlx{L{7zknm) zrp93bZiA6#vRT2mUu?KAAIDac$VG+E`{y!fv$K&?g(n7tC zSg$WE&Dq>@Sabt_S9~9B7OWVJu~RylHds}66FIjXul4t|v9T@Bu{lRzsZ1+Nx*}r1 z;r8(nY2(3l^qi~kG*O(x5`vRNx_PgjN@Jq%oocyjT6Oe1Rj+eInp)fnX1YTgB8EHd zxB1*>lz%Vo#VfQh)bN59hFbY!lCHC-&yBZN*YYnv!dMAHt^DuG zi>emJ4XoWF(G@qfXBOH3+)oEZrZ@u!uE5>!8h8rc3_k@w2fqb>1>mpc5BsBHHOW2b zCrJ>_BzG#govl@e5mJQhwQwedTI_D(HsuG}9e;XH5JRFIji8Zg@oA>awPbc&l|#$0>`0`*BZBo_cc_h8jl;*g8d_H zGMz1qScrVE${C!Wuf31Myt*z+D%DGwKc8XRdukHMRTF%egS&kQYrbQI*@!qq`bnZ1 z2fqx>tP0K4*d_D0qxt3Rh}#1r%O?K;zkjBVK(}(jbp429xreU&YdKPD{saD8G@QT& z+~tLZ00B}J;g5a%5V<>6=zV23DvnwS*U95^g~mXRaf34bY&Krav12KMD9nzZL=D^7 zK|UTIl_~ttIshS_|A?b=4vZbu<~7$iXapxadv{5*(g3)FU<$Bz;=ZOHNKQhTM}O-i zmE4lC*EF946z3gixOnaA5^89miiS&9kM@5?RenTKu3i0Xa1{Iq5AGrn@o!nadTr?z ze-U)j{9Bf;U0uG#CsFxHg;M3()qkmXeZSiVKZmo*u88^DVNg*p)WO%q2touDIRhl< z_#(L);x9QQ`{TuU>5} z`u<|`+PPh?byaVtzI{By1DwHouL}_onuTDXW}7A%lipr}F+-@Hb#>|gb2hCjG6rBk z0!pIDuCW#Q`iJ@n&8{xR$n4_rV3-Z}2er$wn5ih+1)GIZB#Sj%4?V9g%YR<~|JP9W zJoK37>B4nejOm7Tb$bSuZrl1I8iqp0OM(;al2cWeHtVieJWo^IcdOx4`P^K5-Mf=( zk^ei4o>w26j^misuGR0fqaaYJ{8U6pGz}$GeP3;lEXOoe#pPB!%f>kqb$Em`*o5!k z^2DTMg)GLVN(l~^r_*pY!851}hLR z1YS#*;E)jpo?+EJ4<)}=4Sav6w{mh#(-4tdSV>ifULAT~-7>rZr3Mp>@b&@8z1|Q} z(eB;8&oE4L$zlwj6U<0gM(eCe^8KJ%^LKiIPghnh5JbG`ihOla_kTQ;yJVN-i9*kF zM84k72|&EppG6$@dNQ8gF-^l*G!^RnAxGpd;25?1M_z-_7(NV8u#}gIJVRwbheoCW zf8Z`0K_>z3qx!X?pR8qT8CCZT6)KHHAtHfrH@2Cg2Oiqh`f;3fX_ASIWIay4S%EhXC>LI_sxZF9Ab#wZm#X#c&om(?)hZQk?@Z>R5j&WRqm!M z9Y;o3Ma-G5<#|4CFrx5#ev&DK{e&Z_)cEn2>b^&a8b!6b&6GrugESn4gjMUPK( zpc9BFyI$QRoS}-0Su*W_M%)#aKnDl;Y!N7i+u}K$0ms)vhsqd zJ?1(`{iF-{I(v_Uf-h?F`pEV~)G)w{fzFBAs)d^C{Gef;RLaS70H|};m;~pf~ zaIqK(0bnAZ-Df85m$>~vtmdmE$sFV(vP1RA|JT@0lJ8M1yH<;Y*0j|ytXCN0PTjWE zf(-^kUDLIl^AClx@KVwsxx(8fhg*YsX} zPG2XC%QZEysYV*exZF|#Puaw0YLU=X%dS0fq0X4BcwR-8X%y9ef34L*9A5OQ)hLu? z*M4VBR17MGOj6hn2fE@hcl=j^k=Y3#AZSjJ}}AQN~-$YDml! z6tpx-I>nyM#4wEq&4Z=5@0bc_M_<=aG+UA^%aqmoX}R^Sxk}Z!m72Dm7_JkPUC9xZ zKqv~oRmAZCzpiuEQ`K)9hS!xxOf!9Z5l9BcKpX`$cHDpSt}0s;`DVRkD>9V{bMiEo zv+c@jZJnB%Ojg|h;Rt*2|2;`&yjl~l?f5=+yZQAHaj1e)4G|GGSRT^V<#(MOVkqGQ zd;Ggk})2sjaHcvP%qYu;CZC=e6p2@Lf)V@i?bX>(tPJ4ekJh=6G5Xj%bgA|eQk28rsaPix| z?c4ALrpoN`$R1_Mj7gvD5@JZLZmcj}pL*Yc=hYF3Jof^x?ZmB(+`jYX9(WDgekmM= z%e#nB8d7O$R4Rty(2W7SVmg%cgl%1UYu*ff+U*jIM0Y15DI76XH&_*~@9pjBb?n2X zYd(K9!z*H`DY-)x#AA%E(Kw`;^&(DDmP)FWK%Pr#IU_J$ROz&|1f#JEs9X*R zg5^Q(6hYmx*Ry%9)Qm)fK{M@H6AeMmR?vU0xBRQ~v`{D17gd04jnvwD(T}XGtVm02 zOU7D$7Wc2Lybs<}doDgM?)l+NQj^vXUEJH-OMsg0N7_8{*xueAONM&vBO(W9+21E) z`~^(&Y%Hz02GnP%miS;Reb5hEE2iM zvWzyPI?RSy#(CK~_D$h--hG_WgMfe8c-eA3z$t6r5%S$saW!2>&?$6m?gw6rV%y-1 z48*rBQyy`AvurD}Y=l&1Z+l{!JVJBb4)e%)b1Pic_$Y+K5ahS>sE9;u^L>W>q$9bm+sQ)EhpqFFKx90MI79SPd{2dt?QKFT z<$ElAO{!^W`5m~+8_(Hm#-kdS(ql-+V^UF-4}3sTE2KPx>Xe`I18uoI{x6y06y6D+ zhR?x|0HFMxr^b%<5s`^30|0-BL?(iikx52@OCh0UCeEf!kq}w#)FMtmn&7dj@e(y* zfU*MAs7jEHiX3PFFp^ZbIUp=R@l7ubfGB~8XzcFta)mU0cvL8(fN;!t`aOS&HW#aUI9IO( zm5NuVJ zzAevl)v2&4Mm<{BbxE=$NjG&}GZ|H=VY%+@9p4kpX60;x*sCKyTwL}x^r~X&fka#f z@e7%FAO5rtIg-je;_-j}{=V9&V(qMEnAEavwYlqF;lbzI&Z=3X`?O3Imf#|QznZ0a znt5gzNF0mx!yo=I8D&4D z+YVaxikt&orY>9fy1E5v`ZKR^-b4`85w)NLGt zjH|~D_oI3ha8~>KOWuR7!b@;BJQ_*u_D9-ef|8!o$cp_WrX8kL_SR6<1{Fz?k3AY} zm;U$2$er2GTTXDj^WF4U_2h*F&YNp;=bb`B{NP3GZ-`?oov=IaHM8- zdou11^q+rl1yYxXLqU>nni{9 zatl}y-`a92Nz8IIm862p8^21Drkz>NhQ2VNx?_LZ?@58SXL>;I5WoQwpr~vFYB`TR z988@rugrbsa?H1caZbo!m|!r`a2+WZz%KG@O|(rz!>p5qzB*5Asy7kEu!@L%bi(-# z^vWqkccDCTVA6a=RkO3Quj`CU*LWK%P1_{7@LloQ49Bmtfn!>tA9v(SID;4MaG1-R zX%>HG#XcWbOzhU;N8TCIxr&Z{GlNl}|?G^*`cn z@81zfn!)M6wi(XAh5|0Zz3>|N8UUXbG;l`Itqp}pgan0fn>;=SO47^oHF8ZPsO!4P zAfr}`(a_CgW)hfqnj_2qKi~IX@Fn?SS^9=vugBIy<9zE;Ec~?E_PpD8xZ(cYedvE2 z`NU&(dg2G?=H}*Pxn2}Sp%~8LkVD$JeTxcW<#pvDrsd%>wu)qqa@g zY-(BbyhE6Jq=VWrZh1h2H4`o!_;Uv-xTj4@p2=+(UD`=f2Z4C$3>UjHnJ8 z(<(=s{Uo+~!D@B>s8FtGuG@qjjNvi>POmYe6hbg0LePrN)&!7n;}{0w6y{X9G8ni` z75CUJ8W?IAHk)Cn8oxC~k9SD4vl9_#3iLQ@65%{!&8Sp&hqSLAbiei`!Vt!iaY%-4yDNzogOnUnWIw znbOxd0}#~$Sv_wN4S$n*Pp2F3i9ctHA@-Rmd!!&P)0p;8M+Ix`bSo@G-)KwWlz{sn{&o*QL-2#2kli6qCBkZ?QpcdY7!GzI+iHH zUj|35LY5eJA7{vAYlwf}oOTQ=Clc3ab@~m7@}itNAgx28RPq&Z4v3t>5^9XQ$)NqtiPBoYVo)M=%b-PaA#mcha)|A z0JP($N=pgg`{i-~I7R5cJ1CS5W4G3xw>bciQmfQ=G(Z&AY}*4IF5ct1?!UXAzWDz4 zzdtZ5%(na=`2`a3%*6lz0N5Rqa<}Jcy*ZgH z+DZsrRdz9)7|wssH5{AyAs(fTMx(I|Zr_l;W!si7jlK(rAhd?vc^5i_jxjwd)b^NT z2?;iFFwA?sFbYI{e)fR(QP{eDvjOR~dQmo!1F4WAz=r`xbh53J9QIr8;ln$Z;u2-?GN7pd6 z04$8x9<}U>=R)V>uFL4oMx*Y+3Gfl3x-b4Y*g(-2PNMrYaSRej;$##hVK+kzQ10ARk91T}3J-?Mo_FR&w%XcB z@aS}-(P-5B{j*l%0=VG4wddp5q1>o($hT2{B|m=)72F{*I-xbKEXi7}lLM2dIrh3y zm3CC)qTk9yn4vjzLLvAttM}b%w+^JFl(JH*)v{7cg&2ns)U~yG6k;5H@YA3EwEOSw z#e!L-y=mAC-VdGXwy+gEu)Mrn15>^}Rb-btRS$c$Yjl;9gXmMge`*7B*g!Vghb}|6 zBLsimQ$#|j;0TL%7p%^)LKnp>igYG+DAJwMP>;z%^@9H>zO`m>&kY@A+rGRdzp7p; z>DK1WEmbbnugY(czHKWfbUkj=LRp-iF3Pahk1{J+sej=aq!ieSbD@f_TEnhenZ|g! z;<|Y4Re=_qD?2C=?6oj%d3f~tM#r)!Hd=qK*YOBm8Lr(!^bqbp5F7=7)DzjwLhA*A zQipk~?gfR3iHUpVwc62MWhZaWza3(0ne;4>nVc$uZF!J-mJP)znOb1=2tPY*^k)!` zz7L`MYZu(YS;~(--lbggufjYb7J30fd93lQSAS+9U(nbQYEov&LuPsxXL{ul z@C17nT;!Esw#m)yHc@?;i@d%RsH%TSKuzkSF9nN>x2Bz!VFG}G@$CQmj#4T(hHBI* zq>D?NH#O^H+ql5IQwI12OjEBJ*F z>h;L7wKiSP#0aGDZy~g;Q22#ewv^}eYB_}NKu_3OqVfcj-{^3g*yWu0kv5KJ*ZJzAq)Q&H+391dL+-Fc-nxOmcNH#UOt$#vBp0%k*Mf z3GhVmItWH-H}+>;fK+E4xe{lcZmejU=SmC8tgS{erz;nVA7~QFCD_0HWV7@I25|^! z>TLaUI$Hi}`hGEEmsDgTqng8%>nO(Tb*SX=JNLlX4SxK*qH}(ajp_?*|=+={?8iD#7MyJteG*%johGE2A zhF*W}+&TD*vD10!QY3KP#dkPb!&>S8(7s&$4E`+BO`lD2dDef;{T;Ajlrip#4;{&_ zj?cqxuBGPD0=Pw+rYO|waa^w#glRal-R|s1GWc(TeQcVl=6jxBQ>KY6>%{W$kKz#x z0h2qGFba#ZQw^tW90-G`62xQXvAdXAx$~10hxRj(P)0;EI~{n;TN{_B)U(llEkfU% z)i-06J2gXTmKJ~Y;WhJKk_>FbBQ=rf2K+q8vNZX9_8b6Axz<{9iUA1Y;5wy};{xMk z-NQ3DJC8UcX?9ST1ICmq0L3ZBKmqewv50Z8SmREKeHdk;*E>-SU5ZYize68KOsots zwBmSoc~Js#x!27T1Mn;i^pSnj15id7#)WYXqlY;eCRu+5sKD+J;+96-7qcvsDq(VK zUW7(w*-OUeAribP&C+5|`DdB||3G9Je94g#9Bp%MSW*B3r31mQ+;*8kr$4%=v{Gf@ z2|F=cwp@QG2jCb7!Zh`4w>#??1+v9Iq^2gNJ6`wj`v7)R+xFa2)mwoNP4 zWA-L$TY8r!9!(Nt6;|l~A&87mJ zloWrvY@7?N<7P8-25%|>a3jNHn2U`G4}Ls|pG{`l8dOt+l6a70iSf)6hIt+qqbv%> zoL(ctd>Dbitxfqb?-onfVTLz3LEt!A)y8~@g|J5JyRN$WFvbk;SX#;lLQHwSA*Q6Y z9iAutho2XtWXIBh#iM$cX)7wES*XCVVaux;1B1A8EZ0Nj8xl<5bXPy z5EfSmW5AlXXWrIS1`~sGdwR(RI<>*TE4D=%OcJy0>+F`|}j14oZkw8@hTu!4m zew@UD#w=njDDXz5`1BM(HW9b7tlvsARbRQJ-)e##e&G-UOm5w}4dZQFZzUM$!G(D|`hpFhoI*hh#t6lxU#;5ekWY?X zt@ zJ+0iYZgF~$U=QyV3pVuMlbvCk+}y<ExU) z(v>0#P6XzE-Ks%|e2ZaF2*F?(g)FUbei3diC(m?@NYb^vIl6kSxTe<1j_UQ&m|5OC z2TI=tRjrh+Rvo1PXWK0_nxwl1ZtK3%wN%cri%q0xb+kX5%!7YPIkZ+X!3MDGq8q;=4UF0bEGR7?ol*T&!{o3#dF*gh;4TD?2IROAd8RLX-eOWaC zm@$0IQ#pgvwQz28<50%Xg2>wUFSegRykz=mtiLoGPRNr2q53Zq&YFJyVt2lr7uNyM zwdr6I(>I0TLhTbM7!`;x8b(?go0B{k zin@86B>8|O{N|$US54YWi`}d@W`*d;LN3x)kf#V*-b8JiTcDLxGJ7&Z*Pa|w`{?Zk%Ff&)$2J}FbJQiwYY{#eCgtlJ?C+Fp#It= zn5J4MDE5E<7}JjD877UGbB$-1j5^v67Fq^Pre2U>X??`h!%dg@zeV@!!aEN+`d5Js(~n z#D-Q1+idVSD<6) z0XrTb^+)o#&yWDrA_mga1dv+voOu$uGNXTxtT-bfev8fa`?EL<@wn%~dKc+=<7>&G zhlh6XTGK;}>{IGfSdB{W=~Jgpg&2oVMi4%E(h*%_F=cv%=Hhtl?ljP$K;2S0Wp32* z%KMDyWAj6lS&=vDcJv@Z`OIyD{zo6P!(>)3>}R9a%!&CwtQ@!??$cR$;EQGyz)XJ^ z!|B!pQP?|I4T?q=6S!w|9WPAyu5#vgrivXuORxj=x(HB?b8CKwF($6s!mjLcgbtN_HpI-?lkaEHPG+>URg%ZNB@^}zQDg= z)LgQe2OsAhqAuB-gc-b{)2!I0FB5-Rn$Rj7KhFEw$pwh#d3yHW!$}smU??1m^sVr< ze{dJ$a9#nZmqyqQ{kLbD6GtzF?&u@Xz1Lvaxfcoc^u4l#ZbEm)HSuv{QyGE6r%L-b zF!+>78UE8D?Gp|$WtJKH^*Eh>8M;$r;Uv(ON_jKX_EI@q@0>J`a{Tjnmeqg9u@Hve zjN_(n2!Y3^AAg&-bBvI&v6jIYF&xJrj2W%5xBUEXAzDUHpzp)e;R5^-PvevL@9~#O zm)uSMi((qn*U}%efNf_d*n9XP{t*AXsEK1@U3}bV8dn=n7#EE1o6_8EKHYqWr1BQ| zMyqdKZ@t7iZ~fSA*!%5M_D6rzxH_x;s88z8I)Co`*qwG?<~6;4^bP;jg=0Z2xKNxe zK2-cjI3IpJnv6bB8kXKzzPB=8`DOLLYMt8I+ULi*V;kd_jsJV=MXjGt?w*UF`+E zzwFEY6NA~osbM<&YJPM6`NbzTl{R0$d3}q$<=!oy-TIAwzcJ$U-y*+cTs7Y{#h3BP0y zow`r;L-o%ani?)N1{;6RH|=e@-Q3fBza`W1zjb5lo3>N!N8A5*-0ifx_IADKKHg*X zI(`1WSN)F%ZVY}Hx|SSCwWPkL|I0dSEW0TCJ$EI4IDe;bwpdmCS$bFAUA}Lh1Qr>y zFaUsM+{6IDU^z!vA26BB=>Z>WIMX!&e*+T&wTsh$5u(_>yg1>=@WQ5xIg9(ZwZ`qyK;x;dvQcL<8D5i(_(XaiwLrzLfJa>4H&O*rQEUEHPc28 ziC`2-icNS3%BU0n_65#~fUk9!5=mSHdD19s1Rm1Y;Y)vFlR==V&f4#d|D_NrT)Z7Y~Ql69iD+WSh6K~>$pNh&Sg^87idjG&bn`HWUlIaPR zLNjt2cXnqMv>>u2OO{sVUC}y#B?sEN zoUj80N;w5e`icAx(?fsaT#j<=BhDux6&^mj>h~O^Ixh!n^zw{B&A4k`cdya|6kc@B z6cG`le{*E}-!Jv$>6Mq4b`MG$o=#w>Sql62?k>1b64;dtYFnjB{#901X|} zvNo|zoq93HSS^>{{f))$7Msu ze?O9)2hK%ffxH0(LNdO9yy08~!{a<~9vBPqs(sN9} zlHxAwrR8+K^JL@qCg-scmUkY)J}FOO`Z|W2aPj)HfvH98MAC%spn%Xr_T@h`+BUgwG7iSyi-al=c01@5q{@S?Wvw{@OMzHWSke>WGw z@1(bmo7<)jq3-(JP?C`^zqffwN+b2w_;x)Y#2Iysw!uxu^^EX6o5WYHx65OXzTiB` z=OXk~qw_cqS1ugc+{*Ck>-6Cz_wG9n!fuWS+KU^$pPTSRz!&fV()?Y4CHRcIFby9n zhy5>WeFI$}?+66&4#}jGO;x(kll}uHe`-eR?bK)KJJa7!f1Lir!V0$Ht*TZRtE<(` z>TdP0dRqOh!`2b&n(b#-vQ0b4F0||04efSzd%J^OVfVAwWyfa6Wv6Ag{&C)q&(uf# z)L#QMOd~W>V>DLdHAT}jPitv?ZJ-Ucvv$+&I#37e2py$kb-YefkIvTxx=ve-})1A6o59q77_zg0v?HPOSxIP|t{J1JoUFt|9 zX)R;S)-#UJiS5gsvD(^1duV?haO}HJc|uR=^%JZ9sSWUl`E&i%{;vP(q35Azp+}*6 zp*y^!NV+L$E{L8LqGth!o|OJoqQ{OND|%G($A5e`1@k(Wam;md&0H~;%{g=095W^6 z2$)^I_)TU5n6+kt8E<;TH?=n{=t`TI`e4fXTZ@}wby}FoU=o=4VB(lqCMKAuCY)Pd ztosn$;kO&Qoxn zfPeGYtTN{TIG4dWtbLAs1x3GM%-{OoEBg?=@GFt-zH^W!%$#?RXQD#&# zsu_7kfl(l*tZpk@kP}jRYLvN5r9vLLDoh2Cf{9gWFQZONe5=J-?L=r_bF~kx_ zJP9O{L^3I)l14ffHW_4+MK(F)l1Dy86jMSuRj5u4YEp-~)T03nX-sok(}wnRpd%G@ zrYqg)K~H)yN9%Cy{a#OH8Xo2_kAKB1VI`|s!&=s}fsJfp3p?1!9`>@2{T$*jM>xtc zj&p*OoFZuS+2$=AA`O7#_u&Gh0_jVPHzo_n2K;~mpehgm)Bw5w!9Z`A2%wLN1p1mN zpr45b`kNSFfQbbLnmAyPi3bLo1Yn3s1csU%gh>TPnlxaPNmd6&8w(g? zY+$U(0LGb2V7$o!CYWqsqR9a!nOtD9$pfaCeBh9o1{^0Fb7hf0Ye~t zC(s(w-v=rn{V~VcCkSZWyugB&ffc*~J;90xlgJ1of518nRDgAa{q`Dw$zb~dJ;AO7 zv;o@$=7AjqEC#y}@B_O(umkLd4BHMs7})I@wgc9D-VW>vu(kaF6oI`C@Q3VJ;3QD%u3dn8^6vL16fCBjO8Bhr7lRz6#Ur@jD0`&(_3>pBGfQA7(K_h_vppif+Xbi9i zG#1zoe;N;T2TcLWK+}M7&^(|DXf2>BXnmj>Xak@ZXu}lr1g!w=3{(g0X4u;90BV2^ z1pGk<1Ad?*fLfrVfZCvA0XOLQ6x0Eo3OWs_3+gdIebD(p1JDJ)KIopJm!mC0-!wDwt=2&zKZc1$z$cd7v8D=Z5>< zz5%z*z6G}-`6Jj*Kuxega2fVHfVN* z2T2Ij2T5#zMj%Omd?2ZSvLNY!*dQ6#eBCOLTmTw_lm?oBlm(iClw)fvB7tTgRY0oB ze_5GS2dO!y1X5>9ARI^|kj7)6Oj?7qnK#O049M8IB9N&TSOYRWfz=>0KxS@%%mwqy zZb1TFKu&;Mm^%Ww0-ztreV{+c1NPfTU|!o-00w}31O|e92L^%sU~7K?7!2|ohyxlm z1w%l~fmQ&9f>t!ZaL_8i2+-=lDA1ZCe=riXHfW;I|aZj(5VbN6F^wdIY4;O#VIHPx*K#aFdKBA0p@}p0Oo-n zWY|Lh=7Sz)*dqWIfF5Po<48en&=a61frX%_ObQl(UI)DiEC#)8fDNFJfgXQiumQX5>PGy!`kJ;5bPKX92c09>IA0#_+Rz%|M+ zaGf$6+@P!nQz%!#P0DxR7UgHKhinb%k{v)jvMZ=V4gj^tso*v_|3lyoxtd&qyW~c4 zC+?H`$Rl`@JVTzv2jp$?4n86ue;Duy`2u`Oz5<_-?+~Ahsxf7es76tSiRydGQc?Xx znIft}=_bkmN?%dNQ09v=ma;&UIh4+#?53O-#?yeNzfA4^AY_hEQZ#)U{ z>r;?8eHvo*8Hm+qK>@A=eB(K>`aHzy3lOU>Lae?7q0^Vae*Y~4JU;!m1jOnPD8O-m zZ}}A9cEGo$D8L+7S>J@?%DdUJh!ef>k;x3KqjG=zf% zM;M5PaC)1LFmR~dZnt~aPPf~4w%cvow|!^3O_lxh<^9JNFo6ChEMp6@Fa}@;p~j^m zFN$TN8Y>kw>S5p)x#l_^)#e!yV?-=3(&=7)5@Xccf9%6TZ#A-8q|@-#g0%g| zv4cN=Jz*<=O`NNmidE#<2FTO6uNu(8AdXc7K^Vm~b-2jmJmxF*Z*!S>gvH5Ch&_lg z+B&*FF-9MjoZN(Y#Obno#=btQ7AI|Vbo7YPBk*#K_W7Wj&}bh<#F~tVeb8l`_PInO z7Lz!?*=#lee-N_$4%V>)pWs@YtBStHHLiK|s*iMYnKFbIP%h%HsB>Q!qlkINe> zilaE9X$pPi);PS&BadQV>x=UIKs7)T}t#wVg&J}=!Yx80Fp zC&3cE^FX76HG}+5K+=?>qhsTFmk+pO)3`y5QHAB4lVA@Ze-pXsEMo&nE#VSRCDr$o zmVx*^{2pW>4`l$1!hoaFJc=>N0p+c_evAQ&`?3&H_BujHiOan{dilJEuFJzU)d5lp z(d$Sl1TLGyrmmaAxLzNyBmnHc-QUFHu>}<&^+Dx7zu7#m;mBxdxAz#1EPz7=1 zd8(nJxI^q1f5*tk6HbsB!#Rdg*slUxj4s`pk&f|!4;YTjY`v7&k-vL>eyjg4yb_x* zE+G9K{VOs|GeVx9hHRbIg6rR%N_^e^?l=6c9*qM_$)j!@_vwNNPqF z_#F@ULl_0H>tVoozqiIpw%68L`R2$AbVEN-riH!h3Nc0p9!QMQ3Z5vq#+MC8W~O@A z=ocTqo`2@8cCIx8U~9a`f+!weIZb=8hU8ctOA9(@dbA{^5T%(N_?F3#;cWXJ6}9Yt zSz0oCll>4Ef0e?_{$XS?93E<8=AC-zBBxm|J7w1;PvaW*RWS@gDQY6GHmd%UzZY`kQzpwjq3FVZakg81twmZCNFaaq(?w+tHhvmNF!8 ztI4e<h^S)KFULk4UNsQ60-U*DE=2Ih{rzd&N%_ha@r_6Le%5shc zJk>@<#7;bIMu@?btMgoH$lNcT%HB9e_PPR`u5u8nw)b+Sxg(-AjA|V@BnX2ril?MH zXklTzQY|D0$Hncw%lVk8z^PNLoTcwIB6@voJRC&VyMG+VSH<$|&n(@_ee|n{!}<^7 zF4zJ`mcpt>JfVYfwv;4Vf@|8p3@4X)_&F(v({;VKmP;EB9@^KLMB5TtP=sCKa{zc$ zf2F`u$!!#VYrSc0yyiDn=9z>jj^diCsETrJ(x@nVTH(`rplhI^P6OAX&g^!>(SNe+(`L+TYrhGl8- z+vrju-4hnb#mduNOXx$g$9>ynlc%)lbg_yWK^TM@*SM$o?T~|`>aU4P*WQ8%`vcy= zwosH)X~5dSI2c%KkMwQn@eqIrr;Z? z+;>#sELg!Su>x7}v|bZ=6LZ9MCtN=dRUg-s+U1&C0V^srd*y?5So1r0WF7aiHE$Ja z9>rx9#r3eP0-i|au-tnjm&&@hqNCBf(#`G;$A!r^_;kR)>ThBNTTq8RVBQf?@m8RZV${;(sVi z*nZ8Am3O~pS=t77+)Qz^m~kDxvHOiT-uUs4*W23-}7 zgMytUTGk_StkF>{C6D#|n%|h-Z&2f^YaxyKyk#PqR{qq6@1t#xmlR6Xp@(kUwhj5A z>Y{hswr%IAj<{1fVhc?xzp;_Qw}0mQjq>Q2=|>IU$3Hc>!U=eDj+^$a{)2b|UICK; z82Czw?Cq*?w6bS;;T8KzxfbGTwA*e`wHocVTVzQgX6NVU=Vxby5MtK9YJ`v$ZmHL! z-FJ`bbs=PK#xzZHW^Q3&VPSUG6vCX9Qiyr1^hj)hggKpX1#%|693O?Eu76!E|60g- zj>r$(M^zcCs(lWeN7Kq5xMQ_8I)*qlTC47OAX6q>L*BcRBde-?_EA-qieMUtzGzx` zT+wyXr&m7LpR(eqS!v{>czx@2-K75_#^_%hZzRU(M&w~NeZ(^E^9v?= ze*tTo3n=wL84)YZZ`~Pj;eX{E(!mx~-IcTZ>C;v}(OnVO6zt(_$Zq!b&#-Hyk~x;D&Fv6VB8S?mWrlKYEL zb{lR7xPmS-v6hh?(>j4Gd{F3ZzQtjo^QS-ki5R07JfL2n>9oftxqmeDe*HV%v#%PU+u7lRPeL6XRPLs#ryiCD&likyp+!Z7Xxp`9etf ze5kRoe;rAT(N|aQm48{&e)xPTg?Rpc?#~o3PIF2mCv%LLAe1VK=ZQUdigPCwTa0=q z_DUX`{!q@jk$(s!06X$*WveLhsT+h+ai#giFjV)&M6r~aYp=~n>$ELi!VQC4mShVr z9}vCQQHlXg4rm1~znaay+Ux9dSE+@M;gR@5#1A76mF#;?n18Mt;=V%5%wo`b(KNHY zhu2E+@&Uc~LyT1t{-MnF94edm$n1Of(dbaTikspARA2z$x7{a`yfCE}Nm4QGCb9bP zb&@vW?wt2O%2C$ql}e>jua~WT{3EQ@*9Ip~4%X^5{QOb&O7X&pm6a0?*7iv+n-IudI32BtUt2!5rXde;F8{pz|TVZ$d? zsY(?&l2>VjF+)cBVtcswknMQ0U9fGnRAL*$i&h-jp-dw)j7(WR z-No1PR&3d3$dKv==W}zNtvEC#O`{U(QJ^3aq<@r3y8CHoLtmmSZ^fZWWLc(BAW5=J zN+sRJaGmJ7$ii7}Epl_5$?hR@6R{cZ@_2&~BW-EZWkvAW#p@dUjwwZi^cJ?Z7J8UJ zlnjU0t__FCr+@KKS8rkKFQ|uqb`OWw{$gL8BQ(DcKkbWM9Nd{on@XZv@X+|!$5iL? zD1YblCJpdAX)yaVJ`_+Rob540hWnw>-t+y>@ew(PpTg7|E~0irNvO2l{s-)Y#+zQP zx~}@_HyL4|>{HiKAA3x7T=hjI!0%Fm*$?0y4x%&rLEH`$Bb@DVL_kOQqbMHrf z#N#&qzMHe3t&h560KfeRr#bsgy~upm`Xq@W@=43}**AOxexqwK|Btf6p?~Bv%l-ME z@2hTnFCK7#)EVweQM9tO78I>aV$(6FsxkXzoa4jcw>VV|T%G-KK-Fb4Yo^zp1AlF1 z;^P^GPeA+cpTkI9K+x6?b+62dM5LmdBvf5(5{oF$MHI`Kvar=^g(BLkI~Y|x57G0g zI9YqEzH>3Gzt=(jUV|=M?#DUKcJW}giwDSO6OMR*o>!espS%^MSqw)U>9U2qxx0Uh zBl24_!~W*#Gm{d)?|?;k6+oQEX@4p%J^`%KVKFQ+kqHRbipx^vXQ>dGNP`{LuT`Ol z;#d?S5039rm1ZInRAe;M9shQJBuUceR}ovy!RbCTRZUU*rw7dz;_Bxm+jVUlJ+D-% zBvFLe{QT#eh*6YON+r)jY8W&ignV-Oj5O?}rkUtARTVig%wBIOomu`Qv41SQ9fLsA z6k4s!OG}MUezLJ7%~z^a(X=2yN`XLl{NrR1r_h6o02sxwh(#1fl!}-JFCe}5i{hxx z3~52t?UYuCG~R}{{6|PrY;t<;Teu<{&sKTh38F7z`BqjrVC7 zK4F@V2SmT`jkYqtDcT>6B!9+?XTF^o8ohj(>N@-OXAH)q=%Y5?_0&^OnUZ_2ZrxZ{ z)%6>eey=N;PXPeP0LN3D;uI`MVE|VWyGS*}Pu47)qkwwDfU3%9oq^P9wYuR+t=CHt zP&H|ZcG?|ahH0;t;=ex1q3hP`?RLHHx@)iFDn2s%1U{lAo&Ll9q<^Cc($BXa-p>0( z*X-HbY)!|}aw&>R%gaYdYBkSwy;?0wh!TyO%@}nQpAE*5>AFtE(H!N4T)L*=e^so0}JI6FvX@^UvFK zWO_c^cxy!Rbk(lAC5`X^I4r;^xD5BftKp6CLGa;0OCZ@zYf(hdcz}bvKe%N{JM9i~ zT_lOdRSWM3({?IE?|Cg;32ktK5l7@W<%oPX8bO^0dOE|+%YW_GVVka#s6Km9{*k-1 z@%?n2HJ|)p72HEU+tnpF;^^!~LD)H?Q3G&(hbGbPN@mn_^m&}kg>Z^fSWb*!Z4Yuq z$aci2=zXd+c}d&F6~clRP0g^Zr?GY?VRZ8R<#vF`Y*+7eyGe2K-p3w$Y#}|dyu4T% z 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 XpQ*0O`OolcCQkK%>u%^Di+A}Sy*SP6 delta 18689 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 w&5PSLk2EuR(y{p7oJgie)s*#rEzW@LL diff --git a/lib/fonts/fa-solid-900.woff2 b/lib/fonts/fa-solid-900.woff2 index 824d518eb4cbbd1fc837dcac2ccad718119d1ac9..1acf070801b8630a7bd78565b3e46235d6d65526 100644 GIT binary patch literal 156404 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 >hNirOq;xo+rToYKzif&sP)YJ+_y5pjP+_E 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�vXWh*ir#utQ0_RW;t<;k?L^MF~i?QMk4tXAul?gj>ae(+C$M4 zn*#b(QG{mK%qR@QwWvqg^YIlCi#W-ee!ayNYWE^@ocH#6J;n}X)HAE^jzhg(kHKGZA8oDduvT}!@r?AWnI;ho)`Lr)#a$5#{#LnZkc$q0JcILdqe7(DOa-u&h_ ztLpT5uX`U1u72xV-|AuwYm{A6v@A+FQhZjl`v5+SUXR|2-h)1jp7T-*je9z(Etbep zNanaB2*Kk_OQ&XPL5Mw|d0D&v!zu6p#q7g$a)yqY`N@cPjstRSXqcZaNdv{=_u95C zNs(<@b9%AC!SW3Qs-&rIME< zS%|}Ll|924Sw$(3(}Z!bcL-q*lAzvOaPat=*9rhFrrgS!Warf)%1h zk8y67)OXqaQ!ZhC>UbxYmlcgL3Cp(qWp2zP0fs9E+BYn+IT_i!N;jmag1QJ5LS|=1 z@K-=#RlSbN^hDH6B?v(l$D2akbysgD+COHxS(joC4$p`0|;WG)uhd-U&*tQ}?k7HwS zlw$(7bv7a{LnA~%^AnoMl0UD=awpzA(?xY9eJ|0)iygD)p-Me3Dj|ks!zuy}$fKq8;VcV5+^H5(f0E~q?%+Fo9ZDFBQN}s2o0U{jUfk}WG zEn&uu=OaL zHl7QaFDM?MN81+H^SE#1jd-|xGe8TtR+r!UPBsPuuT<-SJ!ll>HcLo5X#|GxPr63) zgZHpw&#mN;?1W>M&>x~dLw|>|c1P4W6L1eIvd}2WHkq$79EKgrj4LaD;u!-Rl@RhI zB~=`xnIGf}9H{Ir@&Evfjy76Uh0&c`3=?9y<|yR3PACf2r*+0v9Zl%$`aL2+O+i69 z_6wYI${fxSu@-Z~(W#+vlA0tfRgNJiw5SC&MfSnt(~1tA3(OeZiK_f@q3FNY9C`JAq4zI*)gqqM4jhW{mm z$lP$!k8G)9aGA)OqFKgv2TjrBMM8+G3z5T7G2OBn4#m(<%*f)jf5r2SQkhGNFhDlj z?So*F)ytJD?ZsV?gk>L8JHRevJqic53O<&C4{k3UXoaTZltI7mau!*yo#O$8`?(!? z-ukI#s@W|1lO@zMB_BfQE{SqO1V{l;a4VZhrw7iV3l|PKptE9j_H0&U`smP7dv&$F zbZ84fE>~Z@`R3Jnj%k&ezSfty(*S5VoC?D%Vq7#0K_|h4xNjCF8^xj>1H^W**y#UP z@H@?mxw@-QkF+O+CUx6Xx4qaVV*@!G(;L%aOEgTXOo9s*0An_B`v>E?4rU~sV1(~q z2^pn?cK?PMNK%y$;$BsdVpMa_vaBj%TUFsO>N>NFM3lwcd2W3T~mQtNcCVts)z-}f@+th z_7`M^L@sp5y1m56wUHm9EaK*+QC&FPP*1EQI06JJOde5~K$Hrb3sX9ss2+&4z_Rln zlP_0&8LZHgnE#niQmM*C2#%x?N*Pwqw*2Dw5=CfB$=eo;d?tGAfQgm~IIq}a2Kr#? zZ`e>|wv7&=2V;UU4BK>6t4p(zSehlwR9hKDmwF5!9b?FVUm@Q7iDm}tb%3a!=*lu2 z)iGTQE*!pu#%1I05zhH%J~MC!j=u4Y+=dXr`E#LHFP7}!-m`&SD%K?g7g`(j5$le6 z9hYV0>{-Q=$fNuAk+81|&L^8ti0(+5TM0n}pUjsEX){wy7q3~|bboo#;_yX_k6Wf| zakylj45pVjw&C$tRiX^IM_393STZnR_^!G{B?x>wc<+lnA> z^As8^7j)0l3(EmjylpZRw-xGFx+)E>P~^xT4Zpey#mPukuK3!aC33E-OR}s5iqp=T zEK9oU=0r2KrlmyJuNi>vkKTGhV8sFWZbmFyrTU z#%q)cO_EefL!BGCN(f2%-h`;S!S#?*Rg#KCSoyV>&L?ia{r2eYSd7$2?D`&>v2Z|TgIkV@AqDb-lH$m`!Y=*Kv$rfd8P#$6GIy(wQ++#6u1g!RLv9!T8CtX9p)ZT8u(Wb7>CvO%rav1roVdandf z>J4HS=m~ev7wf%{XOIy1b`-X6y$ikFcUDG~UQw)P%n-Wf31~*6AuVEy(&twGluzh% zmUi0>qxEN7=OHbI*=82QCEL=3aazv1_a7zWf74B&PwQ-50W(%*a3d9)ZB=cXSXHrD zQq?7a)$wztxnu_wKcHe#RMjF2VV-*F@D)R7mfe?$Z-O$u#Pn%hnEFi$DVZSu*ZA)? z;BWL)KV4c{vXOdl`!tnDZ41dXy3+I4a^$-$0bIEUe-ZhJjH$l6x-9a(oXR0d!Y{PP zi7%&c9#|7;q{mz|JVm9I>|ql%a@*)?g5$R(SC{xtp2e}aXD|Y^n8l}4OD{Q62^UeMSGO$oa;0- z6hgJ!N8Li+JL9?^$ycjY&rEfVj(@s$?_S1kr1Vw|zG(cMo1vi5)by*>YVIShd&Vu~ zy^rQJjmSp2-5tiC-n(}%r8hG6O}ZSn{%321)f`8g{Y~^D`kq`|oIon=MT5=gMVP&F z5KOe`nT1J-*5wA~h-G<_ z7A-qsNXGPBo|j>F{1kqsK^qt#gq^+h@LDzioL>a)E$(w%@@2fQ-x!FCV0|}EB*1`R zXhz*fR(wRvl0I-F^;qn`AK>3M77hlog{?KES-Dt;_KaHsf0@RyRN@fc!okeugnl`%XXKp503fv;*VU~LMY8%NZi3rL|bF;y(Wgl@N7!JL*W zri6}NquAm*ya$~{_gG?9S@=u+1s-^`7Z*p> zR;^GFVUtp!4)A%|VLT#OQE;pZ`5h-y=8?{yoVX4{dfe)V0-4CV$YsEOEhZNG%xJGMi=eVb*WSTiv!jWCv?O148lv|g(n0g zS$E|-Yfs^lmhO9`rTY&~{we1F|5j^@OJ2Whg*)n^>bk$~ksDj8>+1`0)wS9YB3t9F zuh`MF)kO!;Nz0=o529hK0_BY{pR)-Tv(}W723gkzqdc5mH*@!0#u<#1vts(289nxc zl2I^D6IYiBZkC?*EevJPqaiodcEHvn;of*4eS324N2znBcH1>!){K*+^@QBVBLiCU zcCHc@gq852`ofO?S-34*zwy9cT_@f_J$LwHFhcv$9q4KFRrELiz^9zKyU;OBGNpj7 z&T4MzB>rNf8WSkDU68Ee5GBn#MZc{+XvvUzCrvB(zZ*&G=s*F{phzbiKmRyMlWfh; zMcRzRjlP^7C|Hs3{5+NvtN<{k9;3#eM7t9&DTxo@YHU25q~^4+abTLjkI|w;1M3Qk ziMuISqJBUYu_R$VCjnqv#**awl7wX&14udjj>V^qrF-vvV=%Osf@f#3!cC7ed6mLq zVT4b*!8}v1y3W)}6ihR4&FfU1S>usSc%0kA-zrI-EJ>0TNEmx7{Y#Cdz>*|M_LgS+ zQ{W>cC#3a8^mg=&U-})3gcq|YpGBu8aKr%G#~CfhrBmEzMav@f%s^ib4o>iQ(x+Eo zQ}?#Z$=qQ4&wiWC?cHNlYlXaZ>_mNT?;dN{zTF3xJ9ENuLaQuvac@)}%_YoNH#lg& zM<=XYi3ll@7^#)`E;}rdq9JM~4@s?P%(L}KqBU!8Zu`O7?%M9!wjiBS55tdJ+M3Dj zC3C+C19%ZtqP$DA?jS;Mx#Gul44lmJlEL6n-UFxx|5{qR3+;#vlD>ozo1WQ;gD(jlIAeqZrUVjRz~Y@V(~Jdp zylD!6fx+t~5R|@NclKy+zWHd#20#@)uErqR z!DUUsZ*L0z^_$=Prlb-2n+wGv-nMc&n_np1BFl?%6#iX`rr_h-IJX zDcBk9v{ResdH|cX9AXA7Q(@3{uMJMiARps#=Z=IQ8^~UFV+*tN+6~3w*btc~4=V}K zE_23~lEjiCB|Q{Pf)gN^Ddy=EqN-&1dRM7-+$s(tBz!^<-r+Uo@0$v%Mcb~gan-V} zH<`2xD}I;Dkc}1%!H6;Qr@vw_eSp$BByp-eB$B+8-;@UH$;7m=sMiSG@=f)|4_TN;B1UvkKT&O?zCp&x&{{7I6HgSJK$CH;NL0r z4n<;Oz|5PbPK`u8&GvD8?gcZ`hgZ*@T|GQK0}dc=a}LUaSiza+u(>Fd{o*}S$19aK zqCU$y;Y)yrz{AOWsMk+=@W`st6e@QmL+3d|dw^%BcGE~?dL0^Lap&R(gKy{fyQZZx`puE+&O7c zZ^;n9DpO3TPx17;Sp)5%3aD@LFjE0axsLGa0%FSe~xGoZ!;(UyAq4`LBr=7vo~f zojF4+)2dV&i*d7Kzna?W*lQ&T4uC8%>8-E32a0?IRXSSZ@BIW0E;KI$7(!)rstQn5 zRAY`@bzG;&m{Zef z2(ZfkaDXce?emWuD_>G?3Jh>@76=vN6uP9RsuNswdcQWk9A5+~zw|8mC%<~Eoz|X%@z*r%qn4_Is_2?}na7@n zp%5aU*J+bbUm@=AiSG1n(|PlO0|$t1nsHT8?yF5qP(swp+`Qg@x(Tb!BYc!-(V%C! zc5PMRQ1rz_SQtHXhogu@qOtJn7Ch&AHk3hdo}BGYOt>L>_FEI!5w$ED*@4w+wJf_D zM7nTveY@#T`{oYCEzeBPl;2~RW-z`Gm;#LVKuaFUVY7^JIatqLDWhc=E#p!ym(zks zH_g44wNL6-0jkfNUSOIa%)m3Bub)Go7h}hc9m*zRo8qZjYAfDcH7Y#BcAa~UR8vz! zM@lY5=hhttr^scTqs9Z+X4|h^2zZUwK#1|JETBhV#11+dbD@jZ=N)wkS_?wFav|C^ zUWb8g|A8kA7z{9%$)6*`#&|Eh33bsc(64%?vg4YYuZFtk_e1BdpHmQy8`w?j+PG7P zALduqap568`ZPSq<4rx%*LCB863$UfR}34$N8u}oqYgTTCF2n~MC7fPkE7`x0xXa$ zm-3r~3Bo-_`ot3jHcxZfwwZf)|1)4Ki(9j0IbD_go$mr0$eb+#nNp8OM0PFDa2aIHCcMm)F&o% zv#48^_;RPyY3A;H*_JL$zS`+@I;-3idh3B|`3AgZ<|`P%m&QE4=rYV19O9!v7=qk9 zOa-cgIoJ1#NSd8?yE8y%csMnpiOpiEP?*&@BllY8MR=>edE1VvQ&V%Z(^J*Rwl|04 z5Va2^PU7vL<@x!FiY5K|2>E=v*PMup!?WHOpcqdyds{Ig)l#;$w!S>}f|z3?1Dt+B z>utGiA_@Gs%Z3bSfz*+x``I69clohUgKjle*QcV%*_v55Ikbpe+cZm&RkvQ8L?R`O zX3f|d`2;!m|4ouQ`ILm`v0bk&d4Odyy}YeiE0r=8V`;ft@w{qhS;AP&N~KzJ+wzPm zWAHkax{c?tM6sc1rBa%fN}6V1x(~u!E(8d3xe!)#Tw$(nl!qOj#2qc3vD_zIM^TbQ zk>+Hu^sv>$TIf!kaL2?U?Bj!53vKE**9qoy07o{5LyrgysvcxnP*;Ej@rJ|ABQT&G zll7)g+Y4ItPv{kasc-gAQh-JNa=KE74Ii$polB75dOtUX&&By^opAsjrWs9 z!b>6v2WG${Zlz+xEk*V-@WSGzksIlDyyp`M6dxNq$ByN-gqcAiP^yfcFz7z2``Zg8 z#Y3Bm3;3~Usql)Bm=yUbzLqFjQ2itRDG$!B6OkfRkpo%wn`(*CTr zp+WJ3Hox${+*F~Vy11Bwlw#>m{e(=&tRo;)j~PuNb?AvfUOufMVj9l~Q`$v|9xIKs zNL5FX0H#WtczN(gOvKpCKbA7?xYbGpPIbcwA6S|$^=tEA!P?02A|*_G)9UJ~(W}yJ zY;JB6ajC+mj~+eR+-yB-b91w~*?j7+mj_2Fj=8J;lUuD+QpTPCn0C60e7bZ1r~cO~ z;a{idCZ_Vz@xg-!wOMt=B~cW4ZtQqZ7z_sCAiUyh?y-KqANE82dt`s<<@^Ce%^OHj z;=JM920LlhLw*8d87>lkrYOsjz*U8)iyG-^_5alZw@9VH(xLDBjnwxQ=PC?qsggh_ zC?%Pc>?xNON42)B$-UgN-B+(`ZCRU7>P9j22NU>;^8K{o`+le`P)af>nN$jZP*Ac` z3q!^E%kb)^WobQ`|GjHl{IYe+()#GCpXbG7&7q|l(%S|8iC;AM^zpUU6&i9rCBOTBZgOV|$5+f3pn0>r5 zDM!UPGQX%9xsRd}k6cfO!@trkz5M9WddF!KR@|P-PsnIr$!<9*+iO100EcI?TYi5> z_1^rx`|iu`c;}fjXF?1a_iwJwpGYaC7qF@wUF${8gNa63y(>^UN3aLpDMCWL`&&B> z2|49K0|wwGR^-`{)h zz12JNI~5+d=uou{_eY4LqaD3nSx~DBX{P$%fXP-S;SQGs7BWcMa5E2f?SBnwY z+p}bDo|kYH#?@MdVBgbt3f?II%f6;suhq7TP_m<+gwkDOhRl*p2>A)#0&Fv}=r-QJ zI0*}{w-rsf-=*a*^o1hu6(K_qGmVftg~P_=3_8RPX@t!Ua~-Al=?u3*_i#7F`oJc; z@ZE8k?#P#*SD@FTcTmC!1)SUlcReAOU_N_QkNybPQz1r04VMP zm5fDWvnA-$)COcygm{{fgj@h7U)U%~f5f6#Yps%qse0(xem%m*4)|8(n3(pQEUEjp z6ISK>8jVJo^(l$Gm6{x`vY!`E_oWn~xYTJ0YOtc#!@e{GMmwaRxG1> z@87kyuuz%*Bh0C?GhA`*E*whmK#!kshUi;qzen3!&*D@>7g(T z;U(exJnUJPJO4;C2myky2{*WwweN~N7iUF3d&D0x-|#;V$=Nh(ELnviH_=$rE#OMj zHzpV9v1GKtJ%&NJAkqSh67*{-{L!3;kE-B7w)L$npK4Gz+`|M z{$F>{?RHy%Macm|K?8}DooO0qB~2hHRf772I~155+kpLs3*g3=WUEI4WftIKQaZEk_TS1bNa{;KL05d#@4B8 z1yP%3p=WSH39C0&o+R#x$1=`kl5GdP&V_F?=xV9|hX zvywZ&cP2V!Kc>LnfAH^oe@MYQ(e3prJ|kM2qTb=`z|v^ctzl5gBz`41vB>DqaTewm z0ekN|__E^+DO)^|RQ$v!1-RB7jg}V~@6-#UHDP|G*Xy3}fDV0s;h$i7Y`66U!8`Ic z41KWWkCZ}BFa=zl`Gqr}7O;bBxwF?^+s5R1j4^WbW(w)@dbMm9}QT~k=1JRHwT$CMx5NwXxM(KBF!}#IH@mUcP&A>;QN}dc@k?eyg zW$b!AbhIK&DoTa0bBhVfhBhhXgk!>}d}DvJ-%dZM*+NjNBqK_>VZCmDeT=XRI#IoD z1DG}tt1NyDlwRaML{Tf(RWXk4WhbgSC zp8SY!47lxnqhU(PSWxZK#jNc|S~0egDr57o)fx;rXzl$OSWwl6Z>9J8*T4RW$BxyH za;^M8CvntyX{A~TDgdnk$Ji7$S0)cbyk!kf64c{VX>-W;sHT)+wPn0MR?H%aV!}f; zCQ>s4w{Ow*HP>7NQrhr+Bh?A&x^%NaU6<7x)OBg2&Rm!MDR}COdI7UYN+k^@^JuVG z0z2ni0O$Fxw>$TFE`U=fY*{EmO-WD{1b!+9A;jN19miwJx1!Ow>{gM4jS`&J3!PaV zlrX1j(vzcd-0r;eO?P2B|Lfw?OSe5QpRBC3&dbBR>5p*?riiUqmA2k&xDg<>J z9}kL5n|8QXImkzBlrU5=lWsYFH!^K+hZ_?N#x`Jc4G+|`J*1Ecx$a1CCkBSO^xu-b zyE)TCVILAz_J8i;-hY3DD1wXoa1q{sd^A8u;tG`c zJaPMMlBDUlMw3elWo?s5nT#gjBBpRf$$o3MNG_&-SbsUd=Nzd(WZ8mcY;n~>5?m!H zF{}Bo)0sCUnXEd&=ElYbZmtq;u*pJ!fJTLqt+x`CV1ZK6nVqd_lEfc*H+zJ0u6mxz zIe&z$Ed-BnNz$sbvmHR0dy)<6jNUqcu0gMe*Dun}KOGl4tDHYAajpbUlSUY{{rKr% z;S=J6uDiYy{UU}qvUH|54+@|-g8t1}hT;U_oFyDnO{|tCCMJk18M$IHXGk)c;Ff7z zvAVh{G>Un`I42VYEW-<*h6fb}5fOb2k+qv+NsBvz(sb9ZU7U|8iSHoiy9h}s6`fea z65nOjR4`{>M@jze>pj?fWgl*hF1pCCMm?S!4rF`*DkPY<)!3CMiOzTXO|ag;Xp^sR z(7l%Fc(XmmGC>P##|;l|BG)NgLsjL9p3g^Gl+Wu`NmaQQ-+r+b&EbWOLr&*{efHaq zDzc%fa+wth^?ISeDw3*lMWInX8;DVNCmv?6@mEYmVazyRaa`A_oHrO#6!TY>w+*0= zgRvBX?Qki;t-zJ>TJ#~j%e5jXj2qWSuIIVY^@1@4idu}&qRwvqr~Hi8oGNnbT8O(l zLCJTHUDP3j4os#{GL_y#foXd)ag%W6I!Pw3`VY3<7vgHGG25CH>`ogdIaE*z)}2E4 z9qa6BwX<2GD(sM*{qXAQ>aNvYzjpI^;mbnwZTqa{_`0~$g5!KgS+?)gjmg$*!#w-L zWErclJv%%8$aehpSG~$l1PuH6n-2{BY9!5$cw*pP3i(RCo&dyMifHNEr5szbk;g1$ z-S((~)N43>%;Og>TqwPm>nLAf7>ju7%=YbA5Fz-pEvSC0Dq3INCS29D`DGh{p@E8?s4}`!bEmt!8mD(C|{dc1|#>lTNwY|>S&ZHoJqE`Ow$f57!wBkL)2C|$Vfs;h|MpHviJDeMy%6w`Ev zGLYepqL}0VP*v5oZIRlS&tV6*R6|pZ*uuSyH9;vZmGTF7(BxE)2fe&mD*p2H>C+|; zPVxGi>B}Z|1&18m#sXI)Nz$-n>|jjSw=-2z4MEP8$rrG~gZyM|boc&@l$O~ulC+r# zFm!f6L=9L@G06eb#mD@> z^f__N+1c@9E`7D*I8-wXRm(8GqL9n!_7Z=)cf*RKszpNHr)!3xQpZq zmIFt_@={`0ws~Zqu4}~cndS%I=HB--O|uP^2)h7K(-|wa3(Tq(t7YmqT#^oN_Xvj) z!4;Z0-SZDiGKcP?u<@Y?9#6<9-j}U}q;c9I_Ga2`liUg%3#C-}@YG`3YTp7?x#*io z2*5l};F;zwby!C>6q2n)M`@J3T_?8NKvQL3!87tEb7!U^PPa#0qK0O0JHYZxfN}69 zfDZwDF+pSLlvO!Ur1WyxNM0e-b!Zoh#aUlbRfF4P2?hW}Jr0ulojl`d`fCAWC1)lu z?gQlncwSZ6$%_o|XQ?N+NuG1|;>C-{7)}ar8nnOZ1O3Z!AbhCO1lln`iLcranN|CG)-}l2_Cx6h_b}V zKUw-01`cvMczX+k;|pl4afp@@7eA%9-!+L|P8UDqlcl7+b6{F(GL|;mB`6QFudp zhktL|wx}LxR_+g@oc)Y-)mGaVf(F#SflFn_7u7v)zUMnh2C=*!xJ)r++&msStFq}< z8Kw5tyWjn8f_*H;ke`BnIUNz7GrJ~vc)Lq4h8@c5P7){AxV){d(J=mM?j*kNI4Qpz zy?2&PtK%X_F|V)Lwk_r#*xcMy>Ke_LgN|K?GZhgTGbBbC?Bi#(o{e6D9vI8O4)Zig zCyl*wIT9}Lz8O+YK?GNrBnV8DO1)FuT*o3&uryS}{pDh=3fgxV#8lxSN8?j>8(dGQ zC6pF>b<cwwL+*OQ+3{$xJP$YDm;v~T(wtTDA~- zHs^7L>C0FpUU?XWn4=7X=@9)ZZ!_**A%rmM6XH`w6t#wZs#`CGeWdMPZ|>vWKz&}- zT}NrvMrnV!1Oh1MWl$X9oUGSP(=0m~Co0z;9l+lf6ADih>xHPXIAvi9ss$_FI>kNo z-a@^-SA(VF$B#d6`pnneaKjD3;yod`#Dh!34uFQO9IT~qL)nx12K#cNqDRTCy zCj|)=#0__yB&~C1u$a^6f-*EQg~J{`h^|97!Z`(U%)EZ2r7u@_j|-YipF1epkU;j> zbSn%*(Mjp+Ig}zE$K$e$v}oE_Hzx_HVCW^ah$-y>^0Z=Mmh=Fsgy?JgrsmIsrvVem z!JVChiC{22aOjt+jDr{G4sXOb>R!2&xkt0#=tUiMv!yG$A;E9NNrEpTOt~;daEgIV zB@A|1uPGUkG2(BRAozaPX6mQ;iC3T-z^j5DO>#QI<+?Ty%3wu=l8=T$m+AVDEJ;a& zv$06nVBWGep-H1rn1bL}rDwce!5e6`Jo$&L(3(S}_?F;l3K|bf& zKYKSvlG!V|oUDx>Ovagg?u%->-u-`;pcvHMO3u#QXiwFMB;m=aop2=~nr}IAeRjP0 zS)i}#cq>sf~c}Kes-X@E~&=88HDJ= zp{9&5+45DM_+CJ;|FfS3s^tIC695NvYrwGrWWHy>?oZe#g0><)DD@rrS_%8}*Y!`x7i~qf+{YlQxZz!b1fc>$ki?45Wndgdp~dx>N^R%HD5T zmdadgUOC};I8Bc@hQY3Mr0_E1W}X9k%?1?UqrUG`?1rAWJYJY@A4yZWh|A<-U5Q}- zEI{Tcx#TsQotZj*O8ZyvKKLeT`ih-fqKW>;)Ni9Sg|XBl%8q;}aiZqww5R6WZb0{= zviJX1ND}y>BNhr*&dpUYlo#gD#$|>j^Yq+YS*k3w&Wzub&*!gC(<-he$z?DTa#^vD z#f>ViF1By<>=}4T<}EEhJ)KGmX}VySxMgXxt%U_CotrC+yM|$;OG~NLYBX|2XgIbS z*6S@P?RMMLoJ9ypZC5A6Xvya^>(;i1an<#^J^)FqjyzW9@RftO14>u)ZM1a7cPBfb zO+_|W&k9b#)JISg#s9u=b5d(7FyM{H z@$RNX)wnpiJC5Tb!?BTa!$i@@xDwZn>d_$d9fS6fnyxv9yjl2#VWrEHbqwA3#xS~M zSytqHjN5?@JLj5dUzX8e3q}#mPB~-RPc2+~ zk)oeF_vrOw;HTy~j&iM)Ie@zpE)4A+edgAo)#ZRqBlF{4Z0YHx18(P!h(Ptj9m$d~ z24R#tu2%8ib!(ukkWu8bk)(vOlZ7^H$uh`y2)emLzh193N*cMh z;BV#dxM`X>#$;W#?Q-5J+qt}L*txQ0tAi;O4ZO5P0tX6{X-N;ZVfJ0;)+}5ggRl+FOuyvr%_urjQ3en4anN zW~Kul@`ah1ZbPSx$F}VfombZ?wV+&CSm>_o+C4Li@$Ag*T`S#%g-RLVcE+g|FQ&65 zfZ3dzEgrYJC*NG}gFt$)L&3WCLoC3CO(hO10Kthd>7A3n9f(%)52@7&px`-1yz!pks3MKpotP!}D?tXDz! z>AVBnQfO**=F|!`^9nGvGp}5yM7|gEzcMH7-d;qfxJ8T(378h9q%~_76U#lFhxX!leo{ zu^J$em2zPtcr;>|T3_T{>QH|PQ)mH#)>VQD3>cq4^Mz&*v~VCenEaW|ctDp`Ow;XG zOWgoqPMux3ug@<|Xr9ribJOI^{#8@#SIFUnDjJG0SZ<}cJy9kgy2`1m4HS=O`g7*f zLLHxIcc$jhZj?_|c*f8s&+|NnA;xxKa)?Nx>;v^}tJ`TNwMbt+2vAjOE!XRS5w6vj zTarrMh`9g%vp_ZX?|=O9{rgQ7tY7|>VYd&6&r?vYD0MpfdVBXREtRfNfS$+hJABh% z5k=(ll9bO2g;>z8qSJns#*dZ6;QmXRSD7dtF4|2Vu z|I(belj!#{LyNX~W}%oMK90{JKo6ofqxYi>EhSF7fE!}YuNA`sE`-qab)*05_u?LwZGYd+tng~1^ZTmq~pbAAmlj1^Tdwq||&+QQ%>QhlZ{8T+!gUVIhZJQVbQm(jG9BLN5{BN6S5d-@$#E z@X;tv3Mg`4h{7n5zjGb2h2UFgmiGtRh9WQex)H3OQF6j#&z%tJ7?8^kLq@Xpxs$0B zh4hdW|24Z?_dIKVitZCMu8#!RdUg*xM{Yp=aFQ(JDuvE0t9ueoEp zV;DNM7FSB}HR{kpxm=jtQ_35_a~!nZ*cUk0Os`bZd_I2uD_{A_Dep?Hlk*E;7>>oc z%tXhhp!%h{g05?1K;GWv@lCHzQhxV3=(3v4bQPIMnu+UL(V1pak6X0m-5REfNORRs z8oY1GCme$-9DrTqj`5LO-)y@B+FM| z%`$`j#$9<%){kQ8Ojl_fH8cO|DHHTthWjXxw9!g?Zu_I?jp(iDU8B<&kmQ&{hdSg| zpyMdfWsXF9rMGOcC3Hi6S^ZmN+U_z zdW+E;uo6W`zYqUN2vHKER4U1`tY;;KQl+@8%d$N8z{O&5xma8-3L!*sS_mOZ2TG-q z_$?`k>}$n|mATW!TrQW(wOTDoMu(dX2%W}npP19D_p0P_#uRbR^IFTT8^-a=chYR< z6a9NSAXqrvPFh=yvXVSEHcg9?$=n?GVdFxSVfmpbiuy)2SJpPKj5m7+lCsc;-zGJ* z$RCEvv7VcNJkH{`k)_>?)M_;;ZQvf4!a8Mvd4f`4O+$ z80o~`!xpF5%SEjb9YzpRI$LZQ1i|XYJ(6B8zc6d$wFTJ2tlpQ?rI}9s$L%XxhX2L# zdvMV6BvVMLZ0-aesaJ);=z zGdw%wENing>BJ@x@VNs0c=ck}P%g%T%noGDGFz4h)~9sNG()eO$lISbTM<|ahjNGG zLqGI?2yF$%FNS0%I+BCULEOH4sBKoaAbiLOliXw6?)x?T_}WX-4%VZ+ZXn*oFH_vL zYnNeJ;#7zszvI=+6Ei!eC}T@}W@r3XF#T+LKWiV#mmpp%gz2|qnx?wmb4w+=`N|zU zcVO$Kv_^JkVbi{}sr|uS(K@VO5OjvVL-N{|%pgq3rcMSCc0TG-3V&F}Bgkvp*w<;= zINkwr?053^e+^wie~tbND3o9U_QI8L0bT=dLn!XtrP!Hfo|A6&?36pNJzO&lQrtz_ z-YPw6%AM2PX|{G{%`_9Fr0uh`h(=s{<44;5_X|aZ{)|k2tYNeYZmr_hMAir91dwI| z%X=Hm2&~(g?fA4$eyyeml<@;wXpXh+W6@N9>U*2AR|4LHBNQ)dO-pO?^ zO}T(LeD&EM_^2Yi^_Ti}iA=XxrHKJp08 z)R}E{kh->o6ldHc3da{ca%z2joeOGxXTxQJGc6Xu;DS!K+TQCCtvNG9piJLdpEsc2 z)Y{afahJ+lbpKqd)zbV^*4WX)D;;E5->Ahk6WUvKk4K-W3+pxsLnA6qOl94S2$6vd z%z6^4un~3WK~2^cklMku14lng0&8nd~p-=%I)fuQRh|k^pzz0-#ESwSNYCI??q}odfaNo7{z7YgfeR zB2VYi0JV^y7%Xy7sS%b7vywCADx#JA+JS&5e8%Gr{{?T41R2V%#h+KMAT?G2S?Vig z$Gu~JN(_T6+Ztn9kE+C68wsM4{#(xB)$z*?BqLK{#{f_#>r9aPK-q5e!`i|wBP%=1 zyMwVS0-8;ohdAG0r;}MqMYI(cQgKud06Ax__dwk*O#a<1%Oc$io!IKtb0a#GH*S)S zG}3WfCKsv(!b6xVun(JItCJ1{C%Wy7k&xw5JUL*VBXJLp|7G@8btJgFPDL2GcYV?} z3|YPVIsM++bq{pA-O2zW$2bB5)3z@=$A0voT;#~TpxK;bgRWbJv#*EAdyf2MW@hHL zeKjj6-G$;C-;Z1VjzrwPs`Z1?#jEY-n&(y9p&n#L{@jaL_uBj42I9Uyf%k|oY6$*! zjiWz)3jX}F5dMWQ{6`P|A1m00waH2TP2flB#w>qljzl)odH|es|N2YT@JtZ>->=Wb zEdL4*%IFJkB5tk2V8;E2;=kXGFD&;Ib^rQHR0ekyuIOna%2P`U+Jn?{Jpli11(M@5Ot6Xn(6b>I99x7h=+0rLEF!R})+yv#QZ3 z!Va4hOkn_?IN^)4hA`gySGK=Pjh$RmjrfxIm1vnl;nCjT>+lFhK==3jKk?wdUhnv! zReBlS%N-^?6(JVUb)MK<*#!oSkPD$cz97#Myehg-v8AZVa1@FJtM`ucC>CVvzP8U8 zRgC}c%GK(AVlVaGX0z$Kk5DSsS}Jr7;Glbv!Yl+?)hMDth<=U`Y@Uy!eM%g{qed_S zcNp<8TNE4LRpp#VLl=f~a|2V$90fsGp9pL};(kYZ5o6$L^xiHYm`{WXzwT$Ur;+@h zr2Av4%jhC@J$adD)GTlGXr*s9{@=2FXYRlu`6#p=ZpP*uvbEU*1?PZD8zz+yBkKk1?h* zwqp^335RpY=d0DAs@Qf;(zLHPwf9vXf15VP|1j0o=bKGm6|lT*+ct>5nUhV^YnIEN zN1qLaiI-UNi=yMeH_N$j>ho}RnerxRd(bYW%X!V2+$qO|vluiF99&bkzZc6&G;GY; zwQH@yNwsqrHe4Jr)vfEYt`!q!I2?h%V8`5aI2?`@9es3;D5O19ChoB$AEuNT;uQLf zfvQE=rEy3_1H|InqNPxB#!k9tGt4=!x>zzx*R>T@%(iQ?O*;I?UtN+cl0K_UrS2?w z3$)mrV>2ci>BTD=0~gio20HD+%(Pal;Bx1Lhfk?XNBHy_zOoy`aWyK*el4p|%T!Qp zMjmFVLJU)lEupRfy`-B+E!_>7-7g8rRvRm^@Kb^{t_VY`zk1=m1rx2d&v1|_OMz53 zQoV9{$eSu@YJscFb7Eg#s>dyc#pFKoEQZBmu`waz`b81A)Lbku^&6r~sU|3PSjp_SB(^>c}5i7Xpi^W$Rw|53d_5JffHZbKvp=n?^#un-SGhdU|6Vj4U zxF;084@C;V>;!WP((@t1fJ_523n0q|S@R(4K4gc%5{Eb3U^T!x3yNkz(L5+-hGJnT z?u6nIu)PDe7?f~8i6G=;K~4dbYyc%+fc-lt6^GIdpmYezWI>rAlx+ZIzk_mCD4zut zT;MREVhyO|hssf?>V%pG)OrH7Z$g~})Y}UU4QOP8#48-%VRb&NiNM!+@U0!bTM0i@fgfY=lNnFd#LA+Zv! z6~GM}+}sGaK8M?RkZc8ih2UNlxZeOCIN@O!9&LrxN_cz;{tm;FS@5(kJnIY3Zo>0? zc-0PG`;iJzA&?PAg*~Xq2xRu7^jRolJjx8BtSHJph2F3tYdcibhl-6y#q*FYib|wV z&PHVKi%RFCGRIKaC#ZZ4RKbTFUR0?ss$2tAnTD!*P>MeiPM*qMAWe%Y$l{MRls6 zx&u)?E2@`34cw?<05z(C8qY#aYoKO9l$%6vwL??ur2Zscr*{%J=aB+!Q& zQOjJ^x(aIJLv6#T-8s~u3F_Dmxj#ppf~a!>b+w{yeNp!p$m2yHokAZsLEb9plLGW< z5Pj}KUo=2p`cY3i>NO4Z4x&E!sGkoFFwmeXXm9}@P$i>^e`)i_GH(KS1|mO|Iv=*BzfMhe{wqFX)DtpvLL1SQQVnL>YELU&{6UKriK zi5|Q_kL>7C2&MX>R1`gSp~t7t-*NQBKu<=Xrv`dD9zBhqXI;^=5PI%F&)1?C4)o$X z^df~`wnHyd=+y}Hx(a%|6%!#$@?$ckFck`7GO{ok0Zc{;Q(8|!a?3m2&U@`|V zrPslf!G{$v-2WJU4@S6w5pQ6m4j4HCM%jQ-4`8$^82wL-UI(LZz!)ziV*uv7 zgSl#8ZZJ<2%(DUW4#0dlFy9!=UjPfVz=CJ6a2PE71&d6p$vA6fgS5$rVg2SWW@Hsdlz!5ueWCt7-21h5v(JydJ2ORSQ#}2@8J#c&pobUxFUcgB!aPk_Q z5(B5a!D#_b?|?J5;H*D!b{CxU0Oy{-d24WfT%11v7k~@8;KCNTC@c0-!Sp!dn!Bb!G z^anf}0neSl^BeF&2fVlfFHONKOYmw7y!t<0+kn^G;EgbNYYN`3g12AbodtNe1KtDg z_rZrz@KFYQ90s2R_%sJT9fHpu;0t-khx6ny&a(_S&t077?{Qx2$9Z`g=hbDLH~Dei z?#FrW;(Q3=d>o4NDS-3YkMsF6&X*9**ZDZ#f;ith;rvL2^V5U#s}mv>B8+16MUEFa z+mXD;ildl4QLO1Gwu@q~N1oRx&UO?xgyJUsp4WVT1 zkn2P4d6YbiQcOZA$Dvg1P^#M~^>mb`6H46*`X!pGHNt zqhcXcd=x729+iwnCBvxHdQ`d*Dsvf?+l|U+Koxwb!evzPII46WRi1>Z1W?t=sOo!E z-H&RtLN%MAnx|2%QK;5qRJ$^&y&Ba?jOyG+b&sQZlaSwo{6SPdH>!UdHJF4NmPU=d zs8JX-4x%QlP*YGd7d6|BnzusDucH=?P>b)VWi)Cv3AN6GTF*yqe5h?t)Gh;RcOA74 zp!V-khY;$R7S7FrKQ1A1oPfygh z8R}OX^>2j+R7V3xp+O!r=r$VcK|@?LGy)AjKBg5#A?dZ>OD0(&e zD+2x93XLj_MhDOs9~yHXjopvN1XT{i!N_QR}Q1AmC?2B==yYYqdK~AAKeV1Th-C+_~=dsbT>D; z??d;)=)rmPa5H)|6g^Ibo-9XC_oHX0(Tfb|#cTAc9eV9UZw8{b#nC$tdT;1MH2Rnu zeR9#Kc^+eN z7h~~iWAV>p3D#o?XJd&DV~HW8#&XQY za^A;sFUIn^SUwlap9(858!Hrz72b^%xsDavj+O9YrGi-L{a9HqR&F3xejrxiK32(( zRj!Ox>5Em(fK|JW)mV(xDvs6hVD*||^`~PE#$k;-SYt2N#KoEp#hL}M=0U7Q1lBS) z*77;l>O0mtgtg(r{%BbMg+-z8GAR5L6!8y=JdC2ALD4Hv%rhwV8x(gKCA3G0pP-~) zP;wGV>5Nj3qqG-L`gD{r8)f!KS(6*opD=h5XKa5`Z1iknIdg(ZNH444< z4|-z*db2rtyEl4gJbEXD-ra!SFM~b^q7VN-A5TZ0_C}u%M_)XEzFvX8>5smRLO-lQ zKORTFynudx1pToX{aGLV6+nLr{rwC2SLnad=>Ien8jQ9yM_a3-ZPn5C!Dz=~w5tr- z4chY$+Penr3!?q|(ZS*9;C^&?GCI;29W8^7#i8Td(a8`xwE>-8fzJGb&OU(7pGFt& zL6^>>%cs$mH_(mh=w@ehD-Pe*9^XD1-x0tXjvc^WgG=qkW$WXz+j03axcm>eVgOeh zjVoQofx)B2AgopfthehGx!|{j^9t9p1hsVsvWB$Qo$K!Fe@wgZ8ge*L9HlBDGPx=N= z4#QJ6;Hgn~S_scrgJUNwK#OZ@^MiI_x zkF!3(*+HBWhI0qwyeOO>!i8zLs5dSS;Nru0d40V63%qhXUU?j^-HzAi;SDSB#xT6O z4Bor~2l0?Wo&S%z@C9{IP?svG%g<0(zN4-cP}kp}Zq7s9{D8Xk6m@$K>h=fJoe!wH zUr_f!Jy-=S@#f$G04o3x@3hBqUPx!1hmYdp^v4sdWko)@-p3U=xLpW{?x}440zk}$sZqARJ_TLx3JTD z|M3|d@Hc&YCMoaN$LF!xfBs(0XNEsDYNBItFj;l7wY*IxOz33zh_<9G%Mo4LVx?)UlgLDe8pT+GvI)g%C&jQL&X`6$aH(fY zPdV+iR2UtY{aeR~3H~2z!2S+lD`mDJOkfD-e*Fz|1dYYeL@!H(j(a9Cdd}kA7#ajr z*byyG(N2mS`^fPUOR%Mb3!QY5Cq;%V-L#V@OOE#ElJU`shre~ty&{!TTS**eGdB_K~rK27NLpxe?9_<5be=nhiFhF rMhNcN9wJ!uUK>^Iqt2_XKkpjw=!XV45Km~_w2OuQ^4U^UZ&>B`J0064LjfW8_nms&4 zs;Z_X1RU44y{ZZT2!8aFpZ(%jzxmxC{^Za8;;;VZ@BZPR{^j5P37Avl9Rb#K>)5PiRvC@_pNc8vV4_K9Vj``!sF=UpB3el@eE}@8 zgQ}CO4(i#SBan#e`v^(*uemMy9qtIw*Wa?+7IP z%hOeVa`S2dc27;G7UqYIg~-DbUjLq%{Zz@V2qgQGf_EwsM0jA+Y+65IoE}7wV6}44 zLG4vcw*`kD#vjzYupoF4UH$D0|9tP%?vut=|+N6hTE&R9KT%v`j&fM^TnfnG!7$3emQFZ#ty6*D0SR2gw0@M4DP%J)x&3^n`@4 zzz#4A3^rgFvshquxeP;cnUuJu11UK{iKZ;sf?hl&IzU#UE>LigOlLS#;qKQb2RZy1 zPMOXp`2#M?y2J}nui4|ta`vxYcfZ%K!|RY{a?kXHosHc)JJYL`RvP6Ml3>-TZh5gN4Ze6y8OBF%5RY63MYkzPP-Q^ubcX<*8MEDR9 zp4JuOB0vEdK1Lw|wJdC95pWad~O!0LPF z9eMWPl7#1L?LW)E4>E-ArGuwxRs8?ELZ>{U-Fy4C7z#`XxO$b=Dz#NPXyh&<;+WHKTXq%r}JHxuO514u~&0I6F9 zrK@TpGhrkE8VP^~NXh_38M=*eZ&m%@1C*XVN$I{KDZNtrN2z(g(eGR6x72S{-m9|S z^Pc&}x4z%CmhnC3n`YOn%T#Iq(@em}g)#dK1jfvgm)%z-#Hi*| zu0vb72^P7X+xBfG{&hm%YwkDbd#zv0EhuGzx#3>x>G?_@=!PBvm&dExWr?p~4E3;l zoF@6ToU`|C-6YtPsmDdy;<}Wd6P?v>pnwAK+*SoS!wOJjSCvUQi0sO?kVFmd|;6&1W~o z0$0e7_&1ER@XtxbXa)Cd&*r=F6#X;oY4)4YdOrpo82G^oZ=*Wj3Wy{T^hczO=9DB78OWChsB(hV|6gnQzVv^|5;3vF2eaGxDsQA66=Jxxf!GP&AS?-H}&jQm!pSi}< z%9h#Z*DXII8wCHVWh2}AXclBv$d`roW)^+Y2XJua{`l-Iz;661z%dFY4z*Jec z9JAOr!7~DTKNF72w{?kiwx+A@hZ$q0kLuCgJ7;>|7l#e_gUX|83y>r4es7f%Fw}y0 z4@Pol$(%b2Sse|!Ssp(**_PY3&hOF6UJ&)%MP znXe~pt{T;QhC`Pv;l;I4>PzA8?U;|pNu4quTCS$PjrjH_RddJt&-f%Yuas{+PN=Ez zbD?TB&Z8N){Rv zj?W0|q+qV)g6W_2~hJ({o9-_MTx z$FnqFny(`tm4&|4zRLW5l0uIk+1E6e)IL2Q!Z@kR*3aeaIe}R>SB>Xco*jG2a#jop zoO?6*ZS$M@;*@OVXXQEDnS|mqe}D44yAyE^@1#- zm+IZDv-B;&t|W&BJKqM{1ZLcuo@hpI+2%bPIhU-b5xo15y>xUpDC{h}234kb8`Qm! z_tVMw-o-hv9`7oCO3vmOl#w9c`@gy76;f|DvOlI_EC`n2ZO)2 zH_9Ex zoQeJ}WBF(2QnKfCpLhS+V?2`>U*%A!T%Q;GA-%)`?qq%(@2$bw#~twUwK3Tb6YiA% zKjiiu%Dww-^etX5u_g7;Uz7f$ZO_-WtG$(osCr{{vv6%ld@gpbat+h5!W;jnl}Kri z(li_X|0`|E-~B49m!%p%Hs(Rown94P?!07N-okNOy6QGV{Z7xbRB6=0^;i46p`KRp z*PL|x9VDeHH|$60yn>%5ki-&MwS%%`Q5 zov@7-&M8Tcws#FlrR1n_6IHEXH9xCSW2aVKSy*DyCsNW?&{} zVK%BT2XiqG^RWO6u?UNC2uE-Nr*IZmaSPA!U1#eOU8-B^R{F3$u21Nb`n0~Ruj@M| zYSx<#W~13;Hk&PGo4ILjna9zrXqokGV2f>qt+cglZM(x(+n4s0eQV#@kM^_uVSn1+ z_FwEyxIeraR)^2R-{BvE9(@MPWG+jo8Kq^&TCBr*tj`u~$@X;Y$RQlcVI0m89Le#V zz$z~05-#UTuHtI0;aaZaCT`^p?&5wP;6WbZVIJX89^-MI;7Ok1Mc(0EKIRiX;_XTkp2JU2d7_nSP01?pOOQey87uFbIqAh=3S~i+G5S1W1e|NQz`gjua??QYeS=sDK)1ie_kz z7U+PE=!DMbfe{#m37CjUn2afyiW!)V`B;F3ScDZ=jWyVTJ-C6}cmaa|4j!-Y319II z-|++g8Ih3~mC+c3u^ET)7@x_QoGF-wX_=9kn3-9agZWv6C0KzKS%dXhpN-jq?Ky~J zIf*kki*q@j3%HPrxtu$=i@Uj(M|qhy_=u1BgirZ`ulRxA`GddsoBuUbBWhGlq1m;7 z7S_sIMQdm+ZLZyPpbpX@I#kE%RGp^Nb%xH?IXYjL=rUcQ>vWSI(1UtdkLXc7rpNV! zp3*aVR&VHC<-_OZz46T#^WFS1#)t_s|7;8!-=?=2ZEjoCmbUF|2iw_pvs3I;JIgM% zJM1NU#on-Y>?8Zcey~B-x4&$J{cHbIOiD~yDLdt$;#88#QhBOGRj3-(pjuRq8d76w zMy;s>b)v4+lLpXG8ct(q0!^Z+G>hiZLRwC%Xd`W*19Xhe(nY#T*Xbrbq-XSwK2a!r z<~W>>^K)S?!X>yQSLQ0bgg5Y3-o;1wIG^SVe1q@tLw?Gy_$|NZPaMnv{=z@_H~*C= z5?vBYGD#zuB#Y#byi!<7NqMOv)uooyl}6G;T1ac@C|#we^pgQHNQTQu86)Fml1!I5 zvQpN`7TG0xQ|MxsEilOCOdtJP)t#mKO;j0wXCTK>` zs(ptB7YQzNK&J;U3SK++ERzq$kMwG7GcquC>AM4m0>=U;0uKX0fv@-i zL08tbbZy;Ox6mzhJKb52&{OpcCYNlrUhkXps6MSP>j(Y!I!J&2V>%tKBaCOFo46*G z$zqC`(x$ShYZ{owritlqhM3W243mow`v$Yw>@vq!b;VpWx6EVn+`MBeZFsw=HqKV1 zZ0lav-puifQy?ig?dtN8cf4!6iuLsG=-+qJX%1DX(j#Bbb&7a zIuGa>y`+y6LZ2y|Q*eGRz(u$ym*i4hg{$&1-o)E^FCXKRe3mcqEq=g{nfh^Vk;k50 z@<>4`CFNGtOj=6^=_=i&4;vaS<7ARdky)}*R?B9_PtNVyRv#o}%b%k5CjSu+36Xrx z+{lN5DEDQWpe5R(Bl@5p24N`1V=|^;Cgx)q8@~x#u^oGG04Hz;7jOwzaUFMXpRHaY z&@;P*yQnTkKaor7Qp_6<#rso#&i*-XF310_=KqKPKluO7|2O`>hCeo83Y0ETv_Qf5 zE#8k8f}RaHfmR1r`Gl?PN# zWmQIraw^9W{GKx@BcKdQueAKdp8$XG3qSBJp8$Le@DU&K9@IMa4EpW0KnM*XL1IoaWue@07n2EzVv_g9|CXy`vdI3ZtTj= z?8tU(!&Yp@#%#cftbm8unSj_y{Fndq@BR${eA5?w*sHzD%e}-4y}(mF+2cLJeE|1# zXSZ@q*8p7FrCh?rUDQQf*o9os`JBhOozq#J$r+u&X`RNYoYKhwCw3wybUep(9LIJ{ zK>z4>{m?gk)fau%TfNkCJ=J4CXLSP5aX`la9R+kmhjmB?wO{+RS9`QuyR=g~v_+e= zQ5&>gYqeS{v`jNKO;a>d!!<~K)knS5U0u{pt<_vj)mRNxS9MfdHC0uWR8i$sPGwb6 zB~)C+R78bUP+65l8I)S7lw9!@N0t;6Ly;6w5d{9v|M)lm;BWlJ@BGSle9PB-$OpX5 zTfD-HJj2sG$s;_(1Ki6U+{(?|z;#^8C0xXXoXP2&#;KgdF&xcd9KyjI!~yKjp6t%9 z?846M#P)2*wrs-|tj}7k!D_6^Dy+mZECm1ni?Rp{vmgsF4>L186EYTKG8&_Vzr*k0 zb9fP+hbQ52xD~F3&0%GjALapJWaxzm0DuPo02mn=85tQF8UME-HCIawQW33EZx!GL zJ>)|bQXxrFLsd`}wN^2GO@K;h2s4?@bY^OS7H9=;a0PELD)J#8sxhB)3%BSPXK)pl za26MFin^;3r*I1U^0lUL4~H?61G$Bpc#o@ePKTJPL!75=+NT|wrg@s78JedF+`${X z&g-hGnyRTvs-h|kOywz_;4w8;Rn=5gRZ=-s;sj>;cU5O4Hs=u@;SuiQA@1M-9$;s7 zMM3Rb>&C;9FH!F@C2r5Uk6fJY2*@JjU02?kD`1AM+!8$&cQZE4a*eaDjLA z&R*Z^`Vw#KnM~z6uHr22;VyselK=p~pMOqX$A^7lw{Qbb1ng35UB>`#4EPB@19+(= zZnzEbVqR!_XQW&DzprizSpA=O^JGs3Y{DLPwp5!ji)HWi2EN1A*oO74sBd@d;TEoF z4cNKZ9h=((n*dnd{e93VAlwJS<&Eh2!y$dMSHS+;?|+}t7k_ML_iz(i09fY<9~m|S z2y37$IkxB<*fPMbhu47318fcu+J^Q($X%_rF~_(}8v)p`Fbgm}v==>{^!1ltU^gUd`9h6p2{<=1VlgOw+W17&E zW;CY-Eont-+R&DEw5J0dDN&}1oN6ir1)bvyx=z>YHr=j!^@yI-n|end>I;3P-}I;c*9^_nEX~#&&DA{3*8(lnB5l@I zm%C?P$Lo4MukX#hrML2m7v9M`dl&EKgM5e&^67$%`YL^weor&fg0v=W&gx~=S!CU_K3TtPAoT!xB>?cPKp7|l z2IN2m5YPqa3S>YpU@%Y<7zXSCGzRwn?OFf_xR$_yt`%^QYYiOi+5m^Rw!opT9dMXy z4;=0~07tlvz>%&59OcTu(XI+O#^u1Vt{OPbRe`$ob0AC8ZLsKA6&{W9V!UcIJXjjNP!@hvL3+zkCyCOA({C3!rkbevN z9P;_FPat0a`vCIAuumah0(&3wtG*2z8>~5_4th*8l0 z5M!Vj5PQH+hu9N-7Q|lgvmy3|p9--L{0xYF;b%e|47(iS5a>^c6QCy{&X9P1rZO)- zNBB~R^WZZe&WF#2xBxy2;zIcU5EsG!gSZ$z6XHhbScqGotq`}vZh^QH>0F5WU^5`@ zhk8Lg0L_7T5Sk0|5Ht_sVQ3b_BhY+^N1@pek3kC{9)}h}JOS+h@uYJCL6q~D>a3(uga2d@J~L-+(J8YArwMH8g`plAxa4vJ=C-dx!A zP_z*9_98t5MH%TsD0+hoigEBE=po(Z#C#QHyZLIOvjE+-VI19cB;@Nl|9|e)-Sw4_ zZzIx)bkBtWbk9TjnC|)Le8G(KptCtM&Wp}w%sd>OHJN!5bdF}`NyU6xk=J78HAYA$ zQT3-9fH|m!19MT0`Gw3)wK&z1n1^aZs{JrO)v;8^VO^^8sV>0!R2KmoQr!p}QQZO? zQ{4lbP~8ukQauctQN51-^HY5Qn^S$h99vNJQ8oRwx}MYJ*qVAl>P7l#b-jeJ1NB<4 zBlXs>6ZMX;Gxe^p3-!Uvu`Bg4)W`O}>iTq-V-M70o3opfz3O zIdB=xwO`TYG&j)PiYsVtr?~^y(cH~)+(2_L%`5%fpQw3_<}*A=Q_*~Zw`jhi`4%72 zd@p=T^MmYr6X!?T4nCtDNIM;VrtQ+sP8vfy7wzh#Nod!k-GHp(U0g)I*}Ma4AAa;g&0VTAe~H%BF2)=CdR$cS&SGD>3m}R5%LMd z1jIz73y6t9XDwn9bk-#%MY@=n40&H-@)6P{#8kv$q)Uk<7@@Nqu_Un!>0x47kPju6 z8!;b5EKjUPdYo8;*tjoS$0j1ZMr?}oII<^-E5IZ1!Ozij-^(S^B zb|Za4>_Hqxs)-{QAq8a*}!zm-i zsOu@CD5J@1QpSkRj+Aj8O8Sm6UPStyG9hJB^4gTikaPuQ@`$_vWlGA_qGY_OHh_0 z?@3wyRWk2SS%I<|`2flqly%65(?il)bQ7etN$VMV?~pbiZCua$m^LMCMRy`;FVenr zr;`pR9YJ?C=_t~%0Z(UW0(@@QPOjCw~$^Uy-ar>>0PCJ zfb^cl)AyC`LDC0ShWhg%>0`QwNS~6vrhAn1J?TffXGp&(-Sed1EfoJj`jhSj(qA4T z#v=Vq`j74?>Ey2l$ehEpqW0T#PsBc&Gb4`? zPx*`TH?afdU!*^i^1sAR)IkzEQ%9B9g*v&!uGA?dcB4*J5K~cSr_T9wF0XT25PMPQ zqt5>fF0TtJu`hKIiT$XHN$gKuLgE1GQW6JJmytM#x`M>P)RiR;p{^=%D0K~q!>H>P z#NpHpsT+-Z^SX%gL4J)U7EQ$5OYY?nE3%-IcmKaSC-$BI0!FKGgk)GpGkq z&@Dtgn0m;)#S--}>fyw>)FY|K5a&}*u#a&u^(5-a#3j^Isb>R4C zm)Y^9Hz{!)^^VB6o_a6!e&Pn|L)3?#6!)l)QXeO7r9P>|9n_~I;!f&I)R&37sBh56 zxQF^S^#kHw>c`Yih=-}4Q@4ygwbg;(PkySqs>Ny zF=(^X<|2Ng%}s>SX!Fn(B@%5h{;l-1#c8Jzf78yOT|~bE?GoDM^lQ_uq+LV5KJ9wi zZS)({?x5XCzcuY{+P(DK((b1{M85;=5!zGqyU?DYy+FS=?IqgF^as#hrM*sn5bYgE ze<0_KrPEXE2oJY<=&PrTB&PmQqTtqHJL|jTP zMlMcVMlOTI73A^~SCT79Tt%)baW%P?#5LqP5pgZK9=QQ=9l0?PaU;0}xg~KExh*B* z7IJ%XN8(m;7jie^4ss79?k4w=xQE*SLX zZ;($%#GB-EPF8R(S@g6DhKKX&f2joW*ACjMxjE~4K$S;YH$#2Q; zh)>BMk@%eaxnz7n{!acud`bRI{!4sKjz;1;a_n~TJvreKUX1vG>Pz(_exe3g5Wi5v zsNv5=@|sAA->E4j{-CCfh(D>Bsac4>sM#nP|4?&Ma}obii&0Au|5HmNQPgr0BdL`n zMp0`-#As?AYCU2MwIL;AJhdsc88Ly{lG=*aMeV{2w(it!)b6yP_M-Nt^`Q2p_M`Qp z4yTTw^`VZYj(PS|qK>1ErwyP^q)w&{qE0uW4W-Vb&ZiBdE=1Z0>hg#-5p@-HHEm+* zS`L9WDRn({BW*J37V0+Il++#6owRAF2dD>W(@~F5kJ4tKo*<&lL_I}4O`Dl|mdG|M z^*r?gZ8qu^>eXinJ?eGpP1;=4+e(|4dbgm>M}0(n`fRmFeNX*JTbTM?X^T;RMYP4K zf2sdyOHdu8Ek#vCTbde8jiD{W$ZkZm$EzUMhYL*JTFCExRPNL?5GY1Tn9oa3(QVqHr#;_n~k;u}e_6gy2`s{)xiP z#GQ}A9mIZ$!res2qVO28+faC%m{}-1L40Qvo+R!&6rLt-HVV%W{e{AF#2!T91>&}$ z@G>!8N2Bm2u}7is7I9yp@D6diQFx!Y%Tf4{xZ6?qjObhxz98lr6uu^=KMLOw zHx7j#i8ClfqRUV?M072Rj=0xREJfVQD3&JfZ4@gI_Zf=SiF*shhQxJ7aWK(mD2^xQ zG!!Qgb2^HXiTwn{>BN;oaT(ExC@v@VITZI2_W_EB5$91np4iJ!JcZc#D4s=p0~F6E zIv&Lf+;E|8xJaKDE|-{YC|*g-6DVFw^e~Fo6FUsWn~1$0#oLIVfZ`nli{hR3K4!A` zpu~(r@gZU=qxc9hbx?ei*pEKPGlOik}eui{humk45n_Vy;H< zbK+a0_ysW*#czl`4#nSyc^1XriCuzw1MX4&$|0q=>rY}m>iFpB~8bmjs zRFjx$DAgk7Vw9Q_eTq^`Vk)52intF^YDe5DD0L%d3QFBQgpm?=6-uLtyBno3#NLe3 zSmJI)X&iBPp){Vjr%;+e+(#%)B{~nKwU&Qb+Mtz>ENzy!A5hv(%*QD0BzZ>?NK_;%4?QRkl2wZok-jwlujb%6_m~1v6chthRK zH==YsG0&rPgVo6_-7fKKP`Zm4gVH^|ez;dF-&wj(tHW8kUt*3&=|N)dLg`Vf&sln0 zGfOXOX6Y4u{qVNL_CVhu>!t#5y-^{F zn}P~M++b8#;`*S%5!V+Lp16LfC_&r+RFo$6AykwhZVf8R6W1RVm57;+ikietKt(O$ zCZVD>aT8Hdm$)IQs7Kr|RMaPKC@LBeR}mFWh+Bz@7UV-r1KDkeIR)8miJ6J)cElWo z?DoVQk8FdOA!M7xw2)m)ygiX!O3ePqt|0y&$QH!=2iXgV)*yQ^@jgKIGGdNF_HyFQ zNA?=xJ&f$N#1zP0N4$rSy@7ar$lgTEAIRQLN;$H35brZ&?;+*}WFH{@kH|hq^e(aw z5&u?XA10=Z>|=xs*(ZqiKC(}d(%#5ELrjG1%cL}n?5o5dLUt|j8_2#!yuXosgP0AG zeT$eEk$s2gS7g5?-qFZ@OLRT5zY=op1mkYmY@ytb9+myLj51}UUCy?8K_$A~v zCjL{%^%1iGxvhvi?pxuwLPgWN8}KLNQtNof{xdy`TRaxGF? zjNB>2e+{|I2-hHYBQakhcMmZ?Aomn8%aMDQm{XB^op{$G_Z2ZKk^9=yBK#mRZRCC? z-VwPS^># zN#c*A(bI^3E*d?Zc(c&x<;1*!MsFm>N2AXW|7SG1_CG1+VoEU8)^fEe6OphX!;t4e zUe9{cMqi_z1$kOed$J(!YxJdAKG4jwteJ0~)zVtpla+&Vuz5CEAFTH*Y_(dg`jbyS z*&10sGBPsKc=E|7M_Q9VD2fwpmc)@1#mVGXNgQc*Q541Gf3u6CT@-~ni*`|TCvhZ2 zaWZ*G5=T-LC%O&5oBDU^D|XgxRM`Ti&!}hFVBZ!&x!RK@X{|AoH-~%tX5Oq1*2_UT zt)&aYFpk484C6QqH%mJ!mCiKntW-KvEsw)64C5){Fbu<3Odg`?NitoUo+Q)tAC1bx z)E8>&c8=o&m|jb>tQG`WBg?a9-b`!hlp{El)~ZQR?(54qg5{uGAFL1NdBzdkqCPl0 zBX2eu8T&Q!W|{_hFgN1qI0#fZxWlh>L~#&!;p{^mTvm;YnSI-q{3_*YwR&*%VBO|d zI-)tt;vi7WlPF#OYs{>dvMdg!c?+$$ZTa%$@4V_@ipo@5Gt}m6m_C$dS*=>lhV0=6 zeWQ#vN-N?Lw=7LT++10&v^EFjrZWGl6PIKrC2rm=i_ysR^S;TAF-7@jwNC35TGdtF zzFtw5#+d#xGg4Re$PDXhFcp+y3;LXR*b02H-(Oz^%f>;&SfF$xRf4&06gShxS^X~v zkTh2^=GczQ0v=^qXj)B-Ez55jOEns);+!~1(o8j_0%nwoP4KM17n~665za#CxI{8= zJdcT$R%b0qe9zhtlu{~kixBEE#+mIn#C0t<2;$Iji44H?2p21@&T5kQo|Owq5wZ|k zeg`IS56Vy%?IQ%$$e3YSF$b#@~T< zr_+W{9SEijz3g-DQ@WV_913H)nZ$9j`Ke=xUb9B9TrB5sD@@US^E$6hO?b3RNRBt< zYEDc&*r}>(Q<{7`M{5&36O>*JI3vWe*b815SeENC9$d|&!RN8LS9t-mEJDh_lu~i` zO~kTTz?pY=Ghv(smgU}bSLzSkb@As(?FBzDuP3b){19MPUqhVR(xfuaExb*tI?&SKE$>Hj(iq@tqn8UaYYwRMFw4&>k zRyzCCy(o)vRJ+6&mt^(C%lGs}S(FIx?HwNO?csl4b9$`R0%*044FX!EKKZ`O5wu#z zCQF+G2<2y21aC*j5K4-&Agz_vv`5t@N{yDm`S*^d5JurqfGy&2#*UBAH$OT$@0c-6 z5=#BC#~BN-McRvhObJhqo#6-}SpFalhV&6ix`dc=vS?V>Y0>Vssdi1V`9q4bDBsj= zte4uN_Rg7eTgNaQyXT{6-g}8MO2ij6fYx0&YP74u6zki7k8Pbhvvc=xOpzqv7aHBJ z){qXLI)*7aL?{{7##DoWY1RFBGMBW~3(L}!+XAJvQF>si`lvc`)1n__M5*{{Mv1&# zohnLzvN-ji&Pq#S$F><`j9|X@pmv%9q-p!<3vc(Uf>M|q-B-`#H7JWyr-~AyaLs0n z5y!Tf-CV_1{pv|3uTd%xg5?jFbC|$e&`E@ns%$ae2I3OSxFt9>?TMH8p7rM)+h&|G zcS5Ki&CBgEontx?A9xnLfHUSgw#^n_4W+!Y$emm%0Y)fVDdxP`1hZ~ z7~YI>bj|}2v%cxq#qEo-C~KX1s8GvYNAYi0SAzqsw95XI0{8)E^xhsW*q^h# zGd|zoT*zMYc*fa`Y1#oayUe*d$gj<}y%c{At#FB-M{XE=l@ zTF={~H|iGKXiYNmvjN4v=gmFF7_%(*w|+0>K2C<=V!|xTC7dz1l3DiBg_RoJzl#ph zlMsR;ze3)jtSS~~N>{-whGkJ!H5ufCiV4N~AyqvvebYC8%0_+A4*7~PJ0)fIQ$im$ zdsV0EZ35&CAz-stjjP@!2+_!45{6+bjsfDhHC|X8b~@{?f1ScvMs4=0(Pq5TS~Fdz zwGwZRCJ@K1<6f{8$1whx7|?)knmT*C7kl}~*mk}BJ|BQm#)JFi!i5XC6>iNy zW#tmbIE*XwZZGukIfGZ#Z~wm!F0*ZiFvb?M2q1znV~oIa8Dr$O+X!Qf*|szFlf<7o zw#{m!TZqQ+AsC|!?R4#`VIuX7&NBH%wk7TNiYfragp29AcA)2Nvmb_03l}hX#w>A1 zVu@oMoC-Zu2t8go(sFE@0a<+S5l@2gby>^~j8V9pLm#H-47!ZR-u?5;bb+Q`>Nu5G zBSiM9NILEPlS+29TuIHw>P`u4U~{m&D#KiNhR}XRl3bt6JKEi3b)^rn8VTHM#jC3u zc1l-5*f9gS(i0=JoM8krbOk*YAz%@!lvWx|jV~WlI{!{shcgdPX6t61S%x96Q--+?7Je-NT1H z?9lfq=RTPIeKQ`L{e6!1sTVHnojiH+eQj`X{_Upe(b3TnD^4L^wnt^}UE{a~)!C-;uI$f$*$k%W zRH8;&bLXOZpy0>vE`~yji*l|*Hk{tsVXu1d_q(170rcFtM~%~`JmRbmKz+vj-Scon zo*J;3p9se5385cx@)RDnS>c*;?)w0A3t}~!2~5!eLcMI11*wVlwr;FxjRM)Z0eIB0 zL~k|%n$2eMN(Qnz@sFyDKex~Frrn@r`#@8T!OlnJCK|yC^-cuU1c25JOkk|q7igK> zd~@D5wh;o2-V!NG)7#$R6KxqH4ZaCFg3`!W&Rh|eiP)|$#bqK868UMj>NRuDKf}eb zzQv=7Az^RI-uAi!A5IN5^?k0UHt9!mrRt`hr^5u*G3ubu7HnWt!BiJ)Gm=k-U|Hntn_@R*Hc-hdcB>c!=J<2 z`fzmm^k}%g1|NqUWvW+fZx_c^zRhNOb}(qKueS$-v#-Wzw-=hCHadgnn6WklcYyEw`zQJbd=fY5l|%UC@I6i`K&mqvp6mIBK*1@TQNqc_J<#(t6JT< zQ}ALgaf#C`H))q>(=xwJImUZ?hr9VD4c+UE4> z!vfB!2M|1TyS9NQ_^A!-36oCtg+U9vXEo0MVtBYv&XjZdK8Dj!4VvKuia;5?%?7GOMzS)Dg%(ynCpKl}BEi1~}5L1vR zt=pDk^R*KgnynSx5Zo1~>YI~BGj(hX+#LCm1e{QSG)ei%&N2U~bEhLt)i(#3Vw|{^ z9R%D@Q{Qn;cHrT>l~SL^0toeGxVFAIZYXAXgkS)N1)RH%jV*AAi#_qF)p5pJGjQxP zUy>8ovMd|7+n(ePk3WGa$}veGG7*KCf-uTOQ|#f9GEg)2HL}~Du;p7?nB&2r>5ib3 z%j0ym1fcdC&b5QkCD9yuer@tE<6Ge^K8UVV`b1)qIhjVwxTGkpN?e#S#@t(PBk?cU zlxCSXb!>;juQ;~D7rzT9xnnyHV{9==01}um#+>U+;*0Q>4r9MARM)#FpS6rjy4M|^ z@qjtceW<6k_t58#;}_d;?CVr%^5Z}5DMhc_j>F+sZO7q@zlTi)pcelK1$P|VVF6!E zGMs^4{8xrEnDBr(o^9iD`7iJj@MhoIajHzYwAs~K-+1t~^fJ$#x(cUv-9x$JVAcgMl`h(m7-&APf+(&;w*(N0c1e$%DEY`3U9GD2d6^ z5HklcMNB@UfpbS~fc7$i#@({%$s4CnZ*1;q#tiNnI-2sN(@8khM{vX#V{sm5w!IXv zbDQar8;)blw{|?-+rygf+V(Eh`W2qS*}LeO=+-$O@#l{%O=UFGv<_vqZpVDo8>u*%WrO+ zKE3f|K`DIP@;ocH60l^&Q+jD)H9Z%U#$*!j?Hyjwirm8S?-H`(5~pUIxI6lF#~Ho= zzm86zO9=I_bW3iunISKYjDjtVh8|sXFc${Tyg4)4DAJn(Di6$umK=|PQr?mF^~D!k zaZKC~?gI(}MxRBg=m<(j3>KeK7^^FPSTN(1!c>XwJOxU*@aGiB<9gAXd)e`TpFcvY zl&6;icouv!%+MOzLu2${^kgr9I}9{%xgJUH(Xu3VuFjryvM5K6_XM2*eK?aG)W#Tw zGPM+xd&$R}b_qPyXUxAL+>87D6uUL-c6$DxU(*+O*OtQ(jw!byd&l#r>?W z$wX0ujgYa=dG4?t4$HMb6xbngnb8lY> zvsLQ$E47-1D{sFFop%3i0Jt|edu$8itz%~gJq&N}w>z*n0xr@GHjn^vz&29BVGNT? zUxmdSk|Z2cIu4TrV){0I<*NYnK&dRJev#NNEn41?Zt8bBu^b~bn@?NTH~0L-JO5$; zkOxVI3-tOqbrA9pTF&tVrsx+a$%N@u3X^(Y&)b@^IzNm1@Oo{h4%O-e&fJQ(F81QpK>>uS&+Tf0PLy}h&3UMIf~ z=k=KS^Ae0Z5+Vum$n!u-@VqM^CCKQi1dtLyUR~Tl!s5nWL#f8z2gf2@6HI}Nbcs^I zVSA+o&|2BvA^7T;ym@&WWj`-r0?(HK^7)Y5cH3j?VKLv6oVPQO+&6+!aTwI58 zYpaCs(benSyr2}3QbG4+aOdHfm^huU_pfx-=a%LQL`YrLam} z8gnpEgZg0+T%C1EQ6`t|p}vH*-r8sXzNto4JseeLI2xLf8Fl3qILN!DG1?4AxANo* zLQFUBipN@Sf6i)A45^JVMPB5ZRA^Nfe%%~sm4OLehw;F7yiht$Gjn{&JvR+~5l|Lb z*mu3qvqb`^8HYCBPA+amOA0O4B}{P7%E*$X@<7)nViMrZ~;| z`m3|g?@%Ai%_?Qmqp+_H<#M%nJ{V)PDbr3!V6fi)7m})mciO4)Y$@Z&^|O>X8!aM| z$oE>Qh(i`i%M#Sjd^cg>U;ylr)e>ZCdx}RL+s-o2?ydPzBt#%Az&!Iw8i*WwDz$B& zr80;jA%f(%ofaP-4pY#LhK8lmM8#~sF|dL+E6+|P*tWw{;%7|6e&maUsb;{!IAo69 zP_~<7zJnoQ#0HE3uthxEQ>krtB7TzRn*>5ck*`{S2{|}s+xDTGkMHq(O zt<(qADDh?-@+ubyxJcU%!f1goqQ!m=GY}$4gaD5)$9eR4{CUgbo;M$ltqr-;Bd=af z1&5i_HGO=y_#({EG4xpUG;~|d4eGG4he9BMS&Cf(u^C}{O^{4~Rdd8Bqyl@4(eJl# za8TF1K|V01Yx-t>LSd{$92_Qr0HamXYUcLuc8mDjG-F(gB zUd2E0yH1=q51sM~i|c+E3T|Ql5g=tAva-5~c;l7FdH=WSvn*$L0yFeD^lJ2LXjU!J z8B%vLSKf8nt!RJ=Q_2mCiT%uz*Jd~xj-ulOE7H`2I#JaocGoo~I-)dXsmnoz`T@>O zyM1DV1Fhkc9&>u6(^mK2+{C5rHH_Ex$H`-FdOizS81gea-X_6>VA3R{*}A-YPf z*B99Huy{Ehm~Y!*Mya?V0py3Co&HdahUfUL{7o-yQ@`Ez3xM;_VqwTxa0&wega=%} z4MFMR(|3ZqJK>)Sam}Zo?|J>jeA{;eBB-F zjEI*|7|M%4`#>#xP*vTYF~*pjGF~Ymj}FmudIU>@i>XC^9#OQuvU21901m6obvrvs|B{hJDSERKTkMgXG*1TD~bS@gAXZ;)nfvH z;FStv^~wMs_+^E$`sDy1c%{Nv-Tm0(fn^bxP$~qalvbSaUS53FG~@N9f&S&om&?4{ zv@EYaetf;>;RmIh#&=Q2^`Dh78o&5{rjsx2eYUr^NeUPaQs&3w{r!FFZKa&XIv`8(2RGmS4~s;fx`Km)~1{8s7!Ki!PuiAe0ou)!>hti?TGtl$AeX zJc9`b#HsU~7+~diY3kM_0Kjtw>>}qb?)*j7Rb3{Mh4@M$S$G%bcj=V}u)qtl4{w9- z|L4{FJNBi<>PG94E4ReSS|fioymaw8c=6(Oaln9!^x2P*Ng}8f$s`eg;j=7EGQTZQ|etj@Vlr&k3~;K&qLP`>giJGG>N70z$l#?ResmSl!i19=1{MyoriN=+I$Kf(I!a! z+Y7VZ-CgeNL{141JGSr3GaN3FiC+Bup_c>JY(>ouIrJw}; zroX(~?OH(>%Qi$ph74}VBxGB=5`UHMQ%{-if)%IO?zZt~6;X%%b1JD?In(Rtzzn5m z7d-+!3!!>gYcuOY31OoyH8~ioL-h;r>!|)|(7eX6sw~N4f@zvdQ{)A47|;xrVx>30-ehS8z4>h5M&^i_24FZE zcQ~a0Y~)*pKGP3^V6pTm6&((uzQ9Zz>@4S9HCzRyyJN=0l$=SZSl4BJB$dIUmAH87<)tze*lJ%NMZ^mEl}&$>>sQ8q9m zlb2OKz@^H-Fja|I(NNU`Gy40I&a{7hJ+8Q9riGoUJ7=>#<*M9~(3JABG00BYrcMQ9 zMVY1~AtjIU&Ai3mUB7<)I(itNf{c?!B~1S{aPHsj#^~{AivAOQ4SgTu{mDh%CE)K< zg1b`H^bq3l!*%LzU^AoY(r9~f%xE~O27OcUh#I?Pw;Ya!L?|esa3RaYRWe~kmdoPZ z)U=;+T`4KgnONj%MF^=0NBd74nc>LdQUEP~)tCsmPBLI42Z(35c5DG8zU zDTTfzsQnB4|CcUZ3WB291UCw^G)-8RucZlyC}L7xwj#u*g;GW;?L6N^y!#U2%v=3R z#$G8X{VfidHrCPv)}r-xS?uoaVoZu=6Rg;4T9HyBO$3Gn5?6Dm{TT+Cq~gnzcA`zY zb_;HfY~rV(d#hkkmD00q3QbL*Y=VS&P*R$ClJ6Y|Wn_O6kfNe>5GNrF(P~V}3>3Jf zGBt&fG*t^!N0+vLfWlI`v%8~hdmM&+;3A#b{j@1#*oJq&tkY);ZM(6(+tJFhF=OfO zZ(=_T$I~Maf{le3Ga+Eb08HkeUiIDWECt-LtZ}SVynh)WJ?)D~9IJzOZ?gZdcDH49 zFr-=gK*ce6(cW*`<^5Qxc)Zo=bULBy2ID9i`;Hr~=_e8co_7=7j8Km|7?cKbRBMw0 zW)Rd&dQQ(JDF-LWYUH|(S+tu>d!uUj^ySN!*V=8%YPZ*3^c0$$h^?*P1ux%MUwGi+ zi*RA_MGDYv1MqFn*?F3=;*&AGIDsf$ZN)KsNP-o;;g=EKE&7>rKl;&+EFa_NJ{MyS z?y2`h7RVPMWTB1a45lzcJ#-$)@^c0U1LPb%tbt~8Hy4n~Dk}YE;0M9-qj3UnCPhEV zQlL86G@1Ah69Bu2*qBf})hN_=kxXui6>S?k*s&{LuhU_n_Cq~TEu!g< zhEii@K)Df^`M1~rOtZ9Aaa;h`sce-@6CMo1!>NT_uG9CA^yyHpC&8XGwY6g%4xgNA z@f8Q#jwUIJq&YTzJ@^mG+%R~#RH<-T=9NkbiOAUg2K*aGqy8#%8`_wuVH(LK_!hwH zqq^~O+=^qe*6(>b;<>sI&+_k;beRzO87Hu1tYJEcw|GaTox*jqQ zt@i(W(l-R5M-0PF4<6jRZ&uT0_w7A+Fm(-s^zLb?+i1zHUFfpeBxg`QRnv9o9)!T$ z5eAohhH`<#%*7`;6+iIur~5hyc&FAs+v#|nx;Z%jp$99JX3}2q`Qv!znG0IhQcZLSZx>g<(kbxA;NB z5R8z9if9hqpo-xnzvY60OqN|Qj6m(ds~NBFg3D)Bi)B{B$q6L3$Dki%3Fj^%hPjOn zAlE;nI#kM*eb90w6YBwF!&f0czUnsONrfZA)TR*nQLNmsQ_F4D>%!q|nH37P%{__f zav4gktoy=#0xEpt*UQ`H@m|W z-=n|b8ytWe{`BWj1I7RtgV$35h;50WH+_Qxk13wBF}8CXd$9-_&CCD15XBC&A;12+Qfnsba}+s4>7!(dL5 zuvX^^&%Cfjv>UCX8?j6nKGw8k>(t|<71wh@%Q2C{;nSog_WK%9lGNAON#eu{lD}+( z@>XQ|AfJUprpn~wyfl7&i7OGl`s%A+ZOBYDp*$LmVs?6qpOV|c8$ZX{AARtaW!zw@ z$>{4`09TuM@sv-Qsj|^ura&h8T zROkDZV!&l}XW_$1(uzH|cm7faaS+StAog+I%4=jyvY1kEB?;s@_y>#89(09p?{I$&jgxMtm36av zA`^sqO7>jhoFUn7GV5kp*1u%@Z}kJ;JGqDMZ50X)zz-?}s{-h}G5)JoX$P#0e+TBq zzatPriB1*O{uj$I6peqrsj?4z?zGbBoJg?!K{(b~Xw5Axr!j8NkKg*w|NPHhaJC#3tzg^b>-?Wwf)1jq(ai`S?beE;q(OX7CUKejC=6O&jq zU!M~n}z zv|50RaMU6%4XQ5gJm9d3}Ta2d5Wx$i6A2tFXaHIAjRI3XPo|xS+ zgjQ>%h=KpYh^gg?^FQ8W=soBI+AmP85!euEId)G@Jaf76&5}zLd89$*Kv~8$b!{p( z&N?Ah3r=oUj(B-tfcuIKFe8Mus*)Ews}yy;Mc=-<*3$K&LaC~2Dy@VyVU53NSW#46 zQ&si7G5gE0_{l5PS}1G)H>2zD2~NEh3cKTfmuBa4qaHh4fs5UH^(05om64^k>rqi- z-Sb!@anL1k)Laq=4q7KqsMD_fBKR;ijWRpz1_Aniq3ZO}FVVp##@hhnZNoo%_WIj^ z8}0=^@`fOTs-4T=YXb|%J}-mLT_R_H`|H2{>-wtddYCF02Dank0gPfx^*5rI(Qhl8 z5u%B!ThU?xUoznsZ~)o8Z~_Zjk#IX~&1WzO0)50H6+fa&Y!_aGwn`^-A7fvipsKAhgho9-xC5Lee_NUVmGQEoss-(n!)LFD+}r68LhWv` z+vv%c=RkrjhP;^0YJ8AQRGV*y2;?in6agrznF-NvoS<3Q3rh*_2uFLsZH;^)tUA>o% zTiI`|tgMt47fXkPxc3{av=z{~D?!B|hEXgU1~Doi&E98VXiQ}9YPZ{}%GktTeEdi4 zcAGI(-Sd@apMBQE*nEX3=OL&Af78&`G{ZV7yIcpsBt!;~2{E6%RX%ESZA~-2`Ap}t z{qwrkcyOz^_7h5Mi0X9byU>H^L+Do#a)LPcJnaT@g!hS^9(K59^r75N#fmucQ_I@P z(j0`;xJIKOwUudY(cbe2v=H?|ND8{?!!04&LwVYerc6rNMa5b@Gd>q$(klxhiawe6>2+eu=@-#*YZM6-P{?YY=iWint%hXE}qm zEtM1OG%Mof)Gl=5=%||hOVxFC@btlvW|}1q%Y&~LMF5VdT_^p|8Tcxt1IM#`^p$z# zSB@UX=wSTZ!U{1Ef+A0{){FjA&5Zih=z8?(e8@;X)&^T6)?|l#`)LfCI zQ5qysl8ve7DQnm5OIc5MYQ2J|T~qc8dt6ENUBBFQhjE_gVR+rSbLaF;UDGD`Y|Zd_ z?Xs+IlV4tF{%1dX;zS;X*wC{;4kL6KI*hJF*Q2{MFa!}pgDyFdj{S^o!cxM)8Pn5l4sL7b7*@t$srCkew8wEE>XCb9LY`61!e=cvg^ROY% zArP@w$e8)ijDOD7GzGlj5Z2n&V$qmrwrgQ5x-#hhB{@Qsy#G!LId_ukV~MFJ&f%+W#hXc!1n0?j@YSiHIhX9iIkd2%==GFbSG9Ub{TfLkzlrC4)! zkZnz4%v8+{x-f{tTQ)W}HtUI-oiLOK+!G6Gr4lzMjAF4`Mb0vNYMOn{pLxh+EF9HX ze?PCQcfia5Mrc-NSS88YlDZ>8k|%1(EZ(|Gr6QG{V~TaYH)gcXHJ(!-F+;_=^dMbSjz8x()LJ(}pCL%Dk#(KlIlSIas{dY+)d-msP#S zIHig%04Y`Jeh3LD-`>JG3{evu&PXnk%s}UaE|3w71xRzW$`H14bvffCMluArC`-Dj zA9xa*?2t=H;@^_~Vcd#Ie*1#X@2aYL3~Z>v0CxDj!egq|c&MTMBN!)C5DH$k>baK1 zxf?AdlLbLiVH?eVI(W2JtATBo0ay)>zE{<17}s7l08~jRc%I9-Ww{N2MnM4eL_QPg z+Fy!rdjvPZ2(6;SUjBN-clJy-KRxPX%oBn-O}vg5#uv@k`G@A_0p{mgnHRq}$uJPA z>C7z_U8ZZ40A`bm`AoS4P%6*dH@if7x_sowa;isVpC_eEb&WBM6@&atHVI(Hbd8pk z=$4=T>}RY|Us$L&80P+LTNuF*g(yQO&@Jd8?cI`P%m+J&x z$1DBVEd{=}0w|FNR6lFujYc49!{!3!xYSvX zo*%IYE+y5COQCe-s@$lDbKT4ssDPR+T?|p0s^6Fx*9f-;`||WO(!tjK&C~F={RLAHt*X z=pfJYz`A_^kHVvbvT1m9xOMMmwzSUbBN(C&Sc%*x5*|0{5;?+m;2dHO;_U!xb(TxN z(f53Z5%A8|An{W-bS7KQy6GH>ax-Gb_#7dli%25JjkwjBg9snq{TRi6=oCwq1=PWS zF<_x)+EU4~zy?VI8!W3N*=C3_1`OC%!Lmvv8)O;Q{W%PQ{ZZQ=OEqEk*8+AZSXQa% zfGh*NXS?{*_WqP>!rnStPQ5Pt&?|leBltE-(Jr)s-hhyVu2I?v&`;QHgad^gwo;!< zSSD4^VIi3&JW4q%fbHyfU*1cVrwz}hNk{N8BT8aw)q0B3)3^>ou;ae2r)3|b>po02 zyJkMuLpT1}DmOl55Q(l4#>g6#h%p7WU4E9jPEnxfQbot5&vu)WFa>1gRa#jw8gp)4 z|K2E^;wo~0F%B_^Ubk&nYeR6i!Fj+oh?VuKWI-uZ~VrE>E|Mx3#Cs z<(dpH9+#p^S7H58Q=#a%G;W;=<9fNkc)DDM!*k9)4CV6l@`|brH4Cmn_!)FNECs#b}kstr~$F=S+td9SF6~%|& z2MH+LehF@dZ=tKvjkcvn1)EfEbAiw5DRJy5BqL3>;&zl#on!Kj4ngjp3Z{kcSx2es{B&`C ztCdK$thrfLJWi>RMpR{+5XttdK=Y;X$QXuAS4}KoH0?xzOkabNG)X}T2QfT1v;n6O zLWHw4{JY3PvuFiff$m0c$kZy4EJRD7B!zHtU=0x@iEhCqwYx2O=xNg(6dpyLQJh*aFV=uNo?RHm=uQ$8qcR z_|cuMsw!j6CS$5PIr*go&vn7y66Y=Y;^N-vNepe(JhkNnKXjZ%qt$9O9Oq}3yS9OC zTl&Ov0~n%%_nII1^u*Hjk~*D(Yz}5gw0se)Rttfa-F1mi_2#K3SJhK<-Yg7Fv#h6D zx!dubl8U)4gMZXwp=pGqdL7ak>m>Cwn`x(BuSZpk?==)H#P?| zx%5S^+YIF!#qs14RF_CQs&@3;BeWA8LcM)Xg`~*78h_5fa*X9lK7oUBb?IfX@RP$uBp>3;*`D>PFeD^ z>#x7QQ7lls2ViOSz32qGZj6gDnRr;9rP)V5CSAwss^w`e;TTUXN;(9aCt$xhitF{d zA4opbx*0Z&S+4PGS4~QVDS=Y6Um&I19nR%)O;`Q>CuD!~x30hbdh-qsOFx0ZcmM-! z6q+i*<591dhf==5DFvpPLqpdfxLsAPV>F&+=92Lh!eQP#&XxiOYJ&Ix3?+ zdKf*95J)xUNho!MbmR6mQY{j-R$M>*<0+6y_u}wws!#A)1LFbo;?yl?!!RUFrxWvU zwX8QPl!72H-1C}Aw>y0c=a$uBjM5u8x2zi&qx62xE$jaBV_~K_-E>`!S8Y%j390^J z5-#L<7Sd1yz##2*Tc?D*JF3t58;K|~&*k&+;o0V7P$49~#j-f>P|Da1mc{uElrna| zWpRFg`E8G%@}W92)4myusr4J-x2fJ{lqzCxPm_i+N_{)@(_{4mMq0Jn1@bby8 zNiPU^79b6_k}#_MMhY1vl%-P!-1g@x4^54*pE?EwUW_}W|8#tmOmEC28xR}t0~oC& z6MBgSj5Hu3EFZ0|pa68pCrUKBX9y&fY^*G{Ndg!U~g~l|U zj%L;OIO>D5caF)nt(z_d_i)t@&cn7=+=`*$&8?_0_8zQa`#k2Na}{;lhDDp4Qbkde zeYwoc#DppkMc0`suxS6RCFU()m~3{V)&l4*v=%W`YfA$2V|9QL^RAS*(d2^8R6!tw z&sTIeCvOC}v04rtXzu9_pn|D$b&jnrEEE!L;CKMTaj_hZUqr@^k$7{y-m8lDO6Q){ zDQmUaOUcQX@FS_#(zTyA&yhLnn3$thY&(K2iridE;Mxv#sQ|M9dTtpF?M;Y3SPNG}$v+Vc+0Ux@4X>dM6HA=KTYn>#OC;^miJ(@-- zOyY8A33kDSsp|?k+`&@vh1KQhv9v##=jA+)Mg;xoAUE1`TjXJV3FE9c8Yyf_OzDl` z`zrk{Gk?Oi%MWN!6`gmjm3+88J$#n_jd_X6A?rTI{-w0hfXz?3ZtarZvi_5?C1&=c zqv&#lm7a-z00hbFDL8#rDR%0r6vt@_7Z5zmeINRvb#A$-{5p2L>nny~7!T~+xpOBx zWlKtsr6Hj!de4m{k07&n^xNZq@%{YtX`J@Ik&Pi73Hqpq7tmhxDt}C=;`639m6Hx930JEnylX-z%Y4@#w10Q@}6w-?m-YQ zi;|@28k1LSr|G=iZU?3s+JJh!o_O|#u-4{$D=q9&yKVek2cDYVJs&mffvz!GlqBt4 zm+km)YZT)t4g1@v*1}ZGBL+iG>5vm8_5m=7=wP;~ZG|h{7w@#?MZw;;-;>sw+(h=j z-eTKp%J$#;O8H)!e-RJh0#X#I`jMj@)0ZSkv1_FMvs>?3h+;1tcSNepfe=Pm!SRr4f$=IDo0?n$Zwv*Aa3` zYw%{=aBXdEZ7l!@93g8QVU$H^ z?CE#_!|XI+AR(&?3Eil9=iQ$_A}Pxd*b!Lpg@cgmBxorWOGx~?%XuKc*s4@0BTQFN*RRk?C9O);hX zJXxlXRo?~OLH(frq;o!W+(nlwu;%5MnhrQP*0dd`ar3Qv?8Fix3Fm_l4Vud^H(bwU znz^}U<|FHF9p}UO{JX@FyZQhP8NizN2+zG^SPsLW9F3fnA7|ioy)?4d(eJ+RzWWRS z<32rm8Zf|$_0#{*>nOAn?MG+PTVh$=Jq1-rElvuumk@@g(xnXM)3Etp=0GwMCs`6m z8+kYhfn{#*A6u=KB(*q}B+MV}@(s=-?m7lM^6}coZ-KAgamO8nI|}1SQC2)bQ3P)X zAv@lkdv=Pg%x?hvN+#GXuZDkZF)hGwLH@y@G^W4Y3Mk*4O6tdR86W@HCP0pN5OFOSu%d~ zh9pW@K2WKuWl1b3H=^$p$AqMyw)rXKBCpf;QDvHwdZp>s+_kwN;%H`?sif$J}xjY@|4fx<>PH?{wf}p9uP^G(BRWV5AS`SArz~F4$?OqWk zmB&`(ER4Q$d zOMh3Aphtb*HBtUc$J< zo$2ZBmFAn6aY{6BrTtQ6TVm+2dUbmQeZ3UjvzaZ1MJ%-96fAYmm%3RnB}`$}&}UZ! z(KO5*x5Q1Xo?i2@^fD)|b!TB-mchWd?7EWN@I9cEn^RY$AoBw-YBc7LA0vfKVK1=a zRooO45G=b~C#B+K$Ul~`Xw6MN=O=G3d=uTJnTQb)_}-mLm(nGT9P!-bj^^d?`JKtf zy`05Ks-sxfm}O$&y4kQ`F^xq2HLEeJP{$asX)z5T6j9vkAchT#X&SQ%A@ZP5Hf@UH z&|kx`-+OnWHl$AZ9wS*2EwweH?V~B$R)XC3)|#wdy^Bz)ozt|-TMuK6vgesU-(ia3 z4PVyi;VU(&kQ+38Ki+7D`bD3*z3@#Gqf>ht*7u3w_{c-(Apr+j7)+7iSE_v%1xE{jPM8YTd5n>`rJU1 zeRK5`4z%1hKLNypF=ax@+o%>{_{x4Lrf=Y?aR~~X`=|$gU(s4rDkZ*a_h6xrYb3oI zGLHDAT5v*k!A41bi(*FU7Rk%Jj&y9aHS8r${ zTwtO=$0J3dU_gGkplunTD9V;9jDOP-1k0Ey76FRI83W<%@%Gn2fbXFP(fbi{&c4mQ zM0`az^w5^NX*-ZfJ8{*~%0V(Vrn-voLLSPkX!J_T9SG7fWhas(jqqCA2R1m>%7WIb znm`dV-Ff4CkiCNKO(_N^zU2RYsG642om*reXqpSqP()34EZ4JzX-5G8W5=6TxnYPR z#yZnPQId5*1OUu!n?q-Akx8m*6^W#(uH|_)o_47qzzKk9k75C^?N3>hC|JNa(x7iC zOa?&&Q6{?1m?#J$P}}Q5wNTIj+!=sMA<&pYBw)HMiQJ%6!lEQ$08x^4ooOOqkYpKC z+gYv@ecfwSN=2?Kimd5Gmh}GzOqV55#k-saBEu4sc}sQjbK zwhag7=W#fM#vS`Yt^0T_kX%m%(G8?VB*3A3F}Lk&B$>#QP#e)3+SLk(ZD)`M&6$1l zF7!*vwA}5}HL3DkGM+Jz(8KK-Arwln3sY^3C{`N)_2U7ONUTd>lT+$gs!tVHR!tKJ zh6(lMhU)3g>>pUJ$2s6|x#7Ctdy`sXog%$9jwt1}s9f_}HBpo#1q>sI z43AeTO);)iG%r7=9a%S=jG1pT;M@~K&Q4XTx-a3@K8;rKLiC77Igz3hl)3k$819~^ z6of@l6IdCHp1}JoX%a?*m~c1I1c?$5VgN=c!3`l0gud; zd7Mbl-F{&ZQCy_llxz;8JdBI8NMb^w3bDwjh>hk!)*0?M$Zei)bJz%cXeHH`pan)R%=0|YAE*d+ZS#YMnKWBX z1M+A6fMV3*u{*=RU7+K2($q_$RI5Jl09Z&z!DLe0tW9Ifet?69eD*%)GaToe=5VWL zJlG*WXly<@D2L1|cOP{WQ2x&vU_2dB=AA@g%<-B1)z;V znAr>bAdZ8W6ob-V&XWIfo2&S@8DDV?QZ3~;L!F_~k2-%K5a!qgnM7rkWGvxV6Zizs zp`AM?8;!}GI}dIBdRdI)SXE7~>qI1yWJZD@X^z7rLDC5mWFn{{u_@JbPiI9@*z5Z~ z5d~JIWR?MDv1gRPFa=r;YQR?&MG-ld6+!2+8-^ics`}#2a;P_U?cTqC_pU}Awtw}1 zN!N8z6eU^GWz#ei3k!q;a9IMZ8X_q%8Zd)vhDA(_e?JHUg;Ee4nW-WXjq#reyBzFo zg+hVLf?nXV$a`TJsw!jeHZI*ixfHFU8_|OU-H!Ym8NX1Bj7gHn9#}775}wXWx*4Qb zKm2Z(NDTKa+6C zF+&hO?YpPhdE!IUXhaHOn7E|#nGcj&&jQe zq5hO+i1Bsosmt%Z=P#jLo)$GlQ4~$|*U91=oEf1RbdY}(rE_YuSclxe5A>UXPM48tviWASP>r7?4D!LCuY6G0i$xpLLbjiw2N=jZ?LsHeYY}ql^*WFx1Mo3mCkly{ zhKuV)30CJsC+TR+n}}&NMZAs&FJl*<$>q2eDHW3N+TQTX<@>ZTYKTaq?EVY&?dx|W(Mz} z0jG2nB{XQlEcf41FL~+&iEJyt@BO~TDb1^EIbKGNvbIW3d3Ar&kAJUK~ z3e2)NU$H zS-q9o%-k0WgISazc)@}`4GNH!Ib*DIM2M~-!SS-1f<8Y`NJ6wxHcUWG!Vtm5Ond}C zYDp>aOHscO?)=~X{a*}OC8rM5O8B5Bdt)_`x;TKM>!O5u}wl%zl$Lsgf?GrmOr@k(LP*4!?&`Y5P0z;FEn( z$pbXRxHpYqX)Aad1yY9!-G(GCrPHni&(9y|^)#*FI9!jDGMH_YUyl>2y2jX)sxnsD z%^14)cyD4E^18e{(YejP2<7s$Oo}?yxe}2bQyVWfds2jnoye7D8Cz4;DaKTFVwbAw zvN(D0Z-mFu`@Z(*@ZG|WY)Yonfc|WBvyABaOUZ+BS;fA9@$!FMcYGns6=x&f1fxIJ zJ+&f#efTN=SZ-{|NJhFf>9(xO@iXj`oH^<00@8b-0*wtCo++OpXWF`~X6M zpC*VkVP}Tp6^Mi61P^8lBc#NEC~DYjM9Hl}8l`~982Bn(rKTrTU03^{hT(MUUG7gq z?dSRd#2Xv+Hb(Ota%cqdO_QBDN$JdeWx0Ic3}j-D{i9XLhOP<-Nnv}u6$UUuSEF0d zedw*|JVHT6vSj+5)k&7KBqDJX?I0g{zPG1LyK2 z=RTs64_^0vX{Ioi_4{XjqBHlIB|p8lPzqU6X9k-q?v>t%Tm3H=@^|$I$cq zAcqQS%^zI4DEZbCSO_aR9qvPBF$gq=)b3&u>O;$-raf#KfC)ArR0=J#+zk+d&Ruiq zWKJ|HkW9&hy3QGm^un0mw}26vM?Uz4MdN3Cy&k2#8c;6Xms#ceO1TmNYNFnRbcxoc zf>1&j55q9B4Pf-Hm)kgYf6Vyzd%a5sy%G=VjK{bjX zn4&0(rT|4l6$sIF#sp$>t$?AZY5FHaz9wNwH$Zc@Jpb77&%xCIxGrJoYd*&zA2!~E zKIl0b^kKJni5nypJ4!W`E@fT+lmu_A-KI{cvZO22;mjl60%^%iNwVyv;F5aDa$T!Z z{T$1(WSTKC*T_kdM|SXSC+jo-g6fO4b4*ildhQ2~rn#;LhAv^OYIU^B(|M<#RzLfg zj<40U4BQ1F#1Pv4IzA+OtM_X*Ug}Qt0D3F>1@sJh9+v_AYeFrtdVqmQB3$Ij4#zHr zOW(+)=k?YM>PB*}W-fl`KBAodD4l{IHV0%g&13G}*^z5Y4fWOznjafja)0J7kSPY> zLNx9L3@DBTKWr&0KT2h4v*2PtEqjZBh+r9OEq`X`AJWCG@ewZ%ZgYHDAS+Cb; zjFx^vCiA}4qY>^MTvW3rs&)F1H1aFs5IqFBXz=SGDDc5_+PQ->xaXR9RDt*+#ozfv zI0IsBj}&@XI8;}4T}7)Xs;k>0e2$l*W3OT2FQ6Z{VOfs^Bow2<4(-mZ2KSOCr63N- zQBk)t34lg6aDl^r6eKJY6aUo~0g9=&r1Q0NQRi;0=Xg^#eH>U8=ecF&oLg3a{Xf8d z&t2Ft04v@(eoo^*D3{Bc>(EJz%armqr?iamBz0Wv?{Lj^XiIawf?ISOum6Gt&(#`k zG1m&e@%QumPWawhV1)g10p|zrAh#^egL6Lq!M)`AHS9HR>d-ow^n3O+4&ry1vKLbY1iI=(^?|{<;n7d43OaU+gOf>xRcZx*F$O@MuMx zfH75L-%D&aIAb!0rDj@VH!)v7(ug7YE+kzFnwDinDgd;q$UnjFQ#$iE(+=txh+SqBOF~1Px#dx{HKg1XfRZN)Ti!;s2qtlgJUE)tQh8 zlDc7ls*=X;)23-yZ>-nrM5O@6hAIi9(ZP7H01;ffckf;SqT^lvG@f;!ANUFO3aepw z$tPHyIcQk-(sdL`7U?Z6xeg&mj*_HkSOo^$SJ92J^y12`_}^582$O_QD@~SyPD1`G zhlz)dj3if69~Er7JhOWIs4@-HC9QA#&rZ&LnlRbymTep6)0?bM&dhOdqm6c>d(ja6 zF0#8Fb&5ENOEjs;2Z(eVqBIIbUiGjxkR#V)&ys8-mTP)RGD*V1&YNzz>4UaZ z<$@zFq!Z&UU?Fa5CrLE)~bAvzLJ$K%DC%v4)h2H4ESI;>Ok!0xt`TxSW!Ig^W%~JHDL0I5i58+R{jqbSP z{-%f|OEY_gS9odi}_3Pnw7 zbrQv10qeD@4hgl4#&Jxeljz9_$h8so{^}Wql=5rxJa=P?oSWMAL>PvCT(L6X8Zf;N z;esQ7u$#t*{_{)Y2S`o+w{Fkf*xxm$G#kmYD+!j#HNo~b@uToEDxp=hhHgfWpbsJM z9fe0}aW%){q@~dsW=hm7D$3e8B>L$L-veNjTG|p4aXAAgzhMnO@_7VxlE`sgl0n#{ zqGZD;=duZ>ZVep2u_Hh1yRxCA}Nz!bVin=7}d$wt^{B>l=W5BR1&iAyUcw8Uv z`M=TL3$!ep@7Z&uN3ksUeCXHXuS>P6?*sULwN}!!J-OpBQ>TT5^ZQ?CTz#T7_w2c& z0nY6*=&sFO%PRrzpaJMJR?UDOKYXU?Q8<-JdC6~RzU_fT!7D9%-zzgi0qGo~r%PHB zl*`jk&sdSk^*aDEB#WTr^DPZ3*4joPS(ib4>ZFlJ9U*c1zuJ)a0_s?Uir&_1pxz4& z&PofYQ~P_w9vq5U+7u!13J3l)XsWG1Xgc_L!#7xZ+Ki4fT_4~*5}7Zpcv_!(L+KeD zkIHfIY~tvmh89xVPdZv>8&Sig(V3bqmw{@LyDjqEs0T`(2TnWndVTg6)%k}3Q7$d^ zsR4c}O(@a#(}yJ5LJ<0Mgo3of@iY)(L@C}fU>a)qep+^~Craoja;~D z5a@Vb5B7RJ9}lp<4kg1c9WVJt3Ba}iJny4Cv%RwsaxnS^yfEhX7Y#-qM%bU4^6~ka zrE;!XwUTL1RBgLjwe4!o%D%Y?+bpf|w!;l{NJox&Vh$aXeT&;^TlT%W6j@)+^mg<1~~o+ zgK7Qw#e7y=4|t|kSCpS>^=rhv@$W{eb3Qfsc6Im-ldAeWPoppF8E}d6OpnC2xqb?* zqnof|9O`VvOUxh?1cW1DS0o%+qm!WMR_l^^vEpch$F7yBwN9=eY?x$Md2OnVZ5$&w z-|G$jV?gP5ARict6GQOMzHk7&>-+;+oDO+!U%7^8FFJ~@MAxGS&^yz>EYbcu_SCBf zaz?%6Y4mAR8ca`j_AVyQia#h)lr9B7O*~gF0mN$w_u$8ugowJ4wEca0fZqLeHJ9Xv zxZHFwfx9ZHQ0U7FEkgJlxRx$=il>$Vx?2*z4WS<^4=%M2 z9Ya^48_>PzDV(i*9aTzrl4Y3);9x>vVGY5*1feX1lG`_$Xar4rNgD4Bq>&=qHrx`o z0INV$zalb!hT%L<^ygg8x@jlmeS8V0ly)rygHFt~vU|dVy27%04)MdylIX^{E1JJ-#+0ZO^O6h*5z4!BmO+B>~o)r#eU z<5V<7fi#+7%l_ru`MnV+T=q!CaX?YD$?+fY zU(1^2QwP~y)OB5GuEPM~LwQkbZ7QB!NXg9*>L?+^9k>y&`zox-R$t(`K-DgE6Mo&H^Y z=wWz!ygkDE;k)Q+F7a4KGNEaupc+NnP^|AG33p0oL$z$c)6#MCX1Nh}I7(Wv6!U17 zEE^lM#j{}w!jT%*0sY@H5CO?-he`K;JjO{k{#3nQCyvKTCFVJ#VG>Iuz9%T66SfRN z0pFKo+itaNN0NLW6v1ePj-+7EmnDmsaD3mseJ111vh4Yy;ur>zT%YQaIiWR8HPTGS zDB5A8#tqr=R9|8fTG`Uex?!8#5EQuj$dMx+BhJp94q@IAW_z9>`%^Xqs%WnG5_nTm z4da?P<%8s}n4%hhJ>|=S=h^H_i;IhfW3lEQU-WyJDXvc?H@2DSGEtkiO=i}lNzbzN zmZ!>2$6#gGRP>r{>V~N}7D5P7gtq0Fo^2JKKsTV*phwX!pwFW3YtY!bet`#T;?!OU zK^9t)OCMUWvN%+Nd=%Ohp3mfcSg*NE4uVAL-pTp?78%kXS+y*j^1wVT2~uQx18y04 zZ!lov$Ope@OD84wM*l+ZR@I3!27qy9;=?&sRCR^1*}&7iP>iDK<4YE(v5-n@b$H<- z58D7!>d8Llp0n;Y8u$#`&sYTv11gN&)MH*zRa6#=Uq)X;vxE)|Q)J7Qn5o0!C{K6G zSvm{1sK4G&A6lFcPX~wO%CAQ&>zf0fV#}%`@1;L30c}oaesW7g>KV{aXu(LYAz}Gg_?s<889IW_pu5n+ z=-udi=@4@FPeREEGfI3pO%^(VLB*pj$E~OZkiB=wpZWz(X%3&FH_?!ivs6*?m-)}m zeMnvJ6TaEPv*K#R;$&W+5?(d=sEYU{cMtypDd3lyyu$74d{T-mTxb-Ex7e;6Y zI^u8Fg+GGci=IWFpQRllNfbGSQu5lX5Q6eqr{`=PpQD)o;tR6&ED%!bW?3N7v9+6Z zv-#L7G1CeS0ME~;+Crz8lV$pwDJEjys%I&EkOOF}QteXOtyY)@z#qK*#v5;3@;%Il z`Q6D!Eil4@0s!+XD|6x;EH~KUK{dB08++p<}oo^G`QQ4CtG>FK&Dw@FfRq?pSG8^;`Q`gJ?f{q zZM!gkDB7Kx4)SoGL0=vRlYn8EQ8RxM-aWb%aI*r;0E;@9kaK?!U8~wwW!5tQZ4C3T znJrZ{{>V^OD3#+|<8moaf5}Z}a-w4Px4yPFvhBYxkICNnn}({Ys&W0Npj3{LiN>(6 z-!td}`WE^f^w;RWKm;3VZK)ud{bW+QfK3kzDrABs@Cu^Zw@%`Hg*PY=DuMLVwoa5r z`_}e4!MGph?mp*vC!dQC`WN=uX6J-&s5Y8f;>v#zT{~?tmam(Tw-8gFkn74HZ(~VcxWy_N5uQqf> zI=7hs=A9)`W&ChMwk$dP1pFG+xb=xJq!Ytmq;Z%tRV>{ZpWQ%q&W07YNAR~WLJSpo z_wlRI>;50U*^nxL@iu|a&N_Aqng=yDJG7|256!Tn1rrARW90>LAIR2N%cO4hn;8NT z82i8atKRjlcQMhq=!ooCrBVqoj2c)sGGU1GN#(OS>G_IJQyzcx{nbxrlt(~4>L0}GqU{V1e--jKvi2A z7oed-=sn06LoN8o15P^%Q#M!;3Y(CzZ3|kVHK_HXnH=yE9eC~m$l9q4pJ6>yn)tGp z=v|%FNm}s*X?vsCr@nTb=$gg^fh<>+wdKk(5d@}bnxYHE)SzIRZI3Znye%deyDS{c z;enMqD$w0|O)$WKHH7tzhidNE@I{vi0#P(gyJ8Mt?g~v)P&Mva0S^jWU>I-C;6QC$N!ne+zJVfle;Nn4xXY1rDayQc4Fy$Am*@`6M+Jwh3Euidd?%dg2#jV~! z)0IjMi;`lCBXJ%Lz~gbS71wq$ZqNkz+9Bhx*Xt2kxuNMI%}`xqCl>$~PBKk5N-^8K zL6Hd&!t`EyOcsxi2mNJFwUwV?-=`nFHO`GPi8>kMlu7a$-X8P?i7~2qtvHjq9Y6dV zNemw^TNZZ5!g}E27yJga+O%yxC1NpL3$8cE|1G{C$xTY0f(G%tO3+cSe)Nnbjlc(G z!1Ne)o((P@RoPKV`YkLDd_N}IMtu@x=q7mr8Srw>fJg<3(qkM3k9_Wx+z;zO;#vqZy=3_o|ZXxc9v}mpyqh#rL{Q)_)ZzCo-22Q z!l(etK#q(@krPZQ8SGQB?!zhll%=X*Tjfc|cFXOxa8eQ}2MN0}_3(@La0nqJV;i(i zq9wEkoj})mNNbj)BR~s6N(eYDNl$Fgx|smNnQIy#_uzBpv%||Eq_O8#B?z>jQT03- zr-GdxH&iZ9U)gTAb=Owgx?@u~Y|}lt-#?hS?bB)}@*E3Brcn$aL*!;iO>Jno4C6oP z^?Ekt&WrK7?(XQNer&ZrA@V`ktgy4WlY%Zo>*!W=A9}m>d2*YH7e*spcUw+m@I&ehiE#f;G8k{P-{2{dM`JDF^m9dY~xnX+=d=QzhqU{OJ;`t3&|!Ct}jN1 zqGDYkgv;QO$)hajEjZd6xmIuSc|PBccDhFs1UV#{u4P+i=$e-OW|$LE%AF%B%3~QE z@DOXlz?-6?_g;anT3|#}FpRe_^vI|n3?sHI^_G3?dKo3?xCJ8=>Cneb20AeHL#D3X zYGS$F&+2;8ikCCeVDwBuIF3r=JIKP}I*{;0KmvQsfY8Lk!B@Cw@!e#g)P z|D>kPE>x-#1JEw=~6mcIxAjZ1RL4++^!azveJ2 z6vD7jPqB=K8(jJ8y=B)sLb3f{hOldMnbmV*MeoJh%`=S|Uuj!f7DEeOR+g#|$1XOF=KRi`i%JTt>}Cd(k?G=>{TiyG6ulO`6@37G2B9V(8L+q5E<2ol zCm)T146h8%P>bRBLdg;{mJX>pBo=PRE330&5%(lByh+Y5Cxt9QZZ>h>Mq^T;n!3PL zrm3^0xeUNd9+*uV-B94kcyPPzX#uCw*V+Tx^$Q zy1*#Vxn{F)V16ENOcV+lAzGm@QMMh&F0YB8FVAD}43_f!^}M>b;SPmqou@ z0r)i9fsU`1$-AB&5q=JRU!-`PmFZ%eJzCs~^>K*tsqct?OqO#FU_B_7NSc9BQTHdNeP_f81-Hn4i zcgu4W*lt&G=zwY@s5`rKlIT|r!n`M}GTTUj&tOvSZc{=kug=ELETDyL2< zbcpcw_Vx%KgAoeW;d*9+$bmDB8}20CxE0ldIEd3^l{2(Hg3G`*A|0i;wU*SftpBz2@SjWRzk)5>&@{ug6rU6Cwq^W#|L_n0@DH-_{xZt9IujGN zrr8q{PSLUeEUSoQM7F;VKZmcO8FV!opy$zVpnvZD$WxWFOuU)ua{~mxxSK8M$DXm= z>1LU6)v_PB90^O4;(!*pGk!$WAgvfcASVMqkb}Djf_mBW@pCq!RxEW>CurP6-UA`T z+icv;n#+q_0nOOLJ)+w%@nV&zx)*q`M?vc$_X&Qe%d#Lbrq?Gl?rYwpO-0#8**>x+ z2xZ#_#~o7M`JAqkl5rXb5QtIgF*B|~vs&5l>?IKZDO zY1Mgy=05vx;e9Yf%jhz6A9@6#tTjT5GB0sN&kSCqsJ4q8h=o-i-NY1{X(#O{Sk$t0 z0$_Q0O@XRm)DgQPNVWP(fuy>>XFF-9ii!pO(aCuS5@(z9(fmomGX$8vQoP{-Zkj5b z1NeIQ-Nx?(xWr9Uy`fAbFq1gmUwdE}u&!c3Z*OwbR9{sJlSA^S&t{`0>Y=;P`_bpo zchK+q#NhN|T?h$_cSC+nya=_-MMLrtQ_Nm19mzG89RlZ2oUH{vJD z1CWR7XtMHcvkCWMgznetv~7vXzZq9iNG(ZA+ywS4gE#PM9Js{PkYlz3sa5us5e+<; zO|%-4OlKVlkdcJ2d?~711L0MH#{9I%+Sh=RQ4vpj05bm|(@k|kabak7v6vSZ{TX{mQ?_%=o*VmMzrgbe4K~>2A`EMGvBHcob;y_8NmA4qVdEH~W#gICqNsw3`g}5l&5X;2=yyof=1#-(r13j>`b>fnxh1@30TPu?Z z$19hkMZU!Ij4{ubL`9T*ADH79x>>HaTGa_dH*9zOqQx{P<(3LSjSv#178=Enym+Af zCFhnhXw1TGi)m24#VPHlginS=BLt_pQfMjws>M@BV-}2nGYO9J`|6Wt&YbZjFN%DS z{4q>rWJaLNy zI(JVk)QjfLJ_}`xP%U-JQ@tsR+25zOb*_YNM4w#w3EETN#Q(onNEvq+S8B=Z4UQ|I81ed9+5R^9XJnom8|0cFoH|RpacB*Gv3B z>b623wNNqag!%p>v>UX)^rbIprm5iy;HBi)>?AdJfPeuq-G>z~WC zt0?lCBopPAwbnREpAYYK9n=qOKULPn<}>&JIy#UVT@TRl{MvbWF&!S+%h9X8rX1L$ z(Un`S;vswjA=)lixkzJF$Mg1fx%xvGF-3p=(!?(jnVgWjT%|!#biL30cDb_@JgCfD ztje;Li1p02VZ!BVDo%8<-Hwo=zH8H{O?|@UdP}PgxLgft8`viYk`Uhh7L4H~q@yyL zMj5&Tt=t9^uo88QU+WIpF_(?5EhImWG@Uh?{w|G@4t!(p-o4uH zU1xUB&al0E_wHpgv%Ami+O3VnBmF<7!~1-HM}6zeuH9N-ouBz=+U{LvcJuI;NxX&m zE*{%n3c(01q7`%+y`Byp$qmP}AEc=lyl@!0+)2D~_*bgsxD_QN4&p3H6HidFI2dfv z{rhg--|dDWK(Wx;FdidxYEV6mSML9dXL$1D$#5Z3rZ~V>f6pEaVc6~NUuhMJ@CIV2 zJ^0lm{b#{9j~qF&@)+z?n82Js@AZl5Vr%F+^csYkeqAzTVO>1EH8EF6)&m1svyE;5 zGc+khT|9z+f3qdfLgtvCe+=+Ksa7qNO5Vgo(NQ$7=xLf}n%Wf+<~$H&%1SJ{`%REd z6(_aPN+}=T;`ssgs`;hcT~?_*{-p&A3qd)G%AP3dnrkb}(M(g*w95-~CD);hNx11c zAl!LmdHf@|B38GD(K|QZVNy`9-<io@_FMQQ(MVEltZ%#B_% zsoSX=pHNY(mwlS?w?*-tZj;JzRk7V`%Zf9zaRA>$tLSt;h1Iu*dMUul(LrkP>0vKP zl0&-wJGd>95Ob^x`8eex2cNqB!TC9iUqsIFV9l5i%!}pm^LQh=`)iK@t1FG8#tsz9 ziZ4)wZC$z!2mkc+pp?zq^T99eI!#=zQc7zSpz{UR1`PmuNk4|mWb}IU+WZed^38bi z(Qylg zy%LU|s-7fKVg*N}a|=VS*9N6LfAYyEJ#Wh-iZ4r7rg^zyqc^ z;-0&vo#}7R!c7PX^1$`)p%}f&53KVnOMbBQB%Zax)SLP5gl-%QF3#A~O-Jv3TaY@= z1xr?ZQ^3BpzB1P)FaVgoB3l=lF3%p(*UdyOw|0fEYubSO9vlJeXX`5w02~3&=Y#P@ zNvro7`Xl;ID7xPQj`{ja_!jtHE(zwYM^B?4<=rI37?$ZqqL#_*=#R%qEB7AGc|PO^ ze(;rOhezD^g7fzC(2Bjgt_xu&A3kjv%dwVqo-aC1Xd9l^%A!ywKFN;La~zLqo%X?& zESjeGKV7}T@?k4l&HHDYlLd+SW~*@lUsZE{k!jk_=Ip0^u%Z5}>PFomw{14vw8t@Y zJsKb+xj{$nh&Xqwvj_8JLMEQ{bI0sgPQie}_?N!h|BQd7;kr=8o0cn27TO$ekf*c% zUG$?`WujTMn}bW%Dy=eW*@tdG8wfQOsY&u!#c^nt2ihP!3POhqTY?d&DNVXrx=czW z8mD7@%M3w`@eCk$-DvzDAIx)P)vEUaW&%gdka zOlfu9Ec#^O(n3_P-qvp_vEsnarXjX2*)!a77hZ1xjrssimSb<5cBeKrkwAY4Z`GVq{_9UZ`6P^A=9GrM zLd-4C>r@Yv93;1I(ij*p9zj0jlukYHzysrtQp!i*dVFM>=h>H@nrhnY6h*5YIYKw1 zH>0n~>t>cHtuPX-(*7K`w3N1##&HLD9&vsqdtS2S(dEjmY8W#W9sq}) zYrNcU)MtwU+SPq!Sq~@IS9f7(Hut+eMEcm)82ut0vus&VEH*t$(==I@M6DX_p7~!L zFrKJ6T>T2IC;(dDar!B#w;-##4k$81zY8$DvU2)Rr(??$-DQd((!j4ai$zdmDG@Zg zahP&c^9=ytx@7%|E{fu&sjg|-+W0FZAo$r7A583{cJ6q*61K4C%Ofz{+qlqsQYs^5 zLS|?$)^1llVT8j3kby)` zag8*oFc380aW!9ePA|Mp-G|OW@0S4SE>!w3+3XHn7_zXvCwC?J^RW+C3x5Yw->NN2 z-J0t?%c$uPSC7u)PFZ*kJ%v7kK7*>+$4`PV7zYg}fv<{yEu2+JTNsF9A)9VexlAS9 zj94$Q8W7A0$wOH!jt#lN+jx+g*pSETdPzWQZjQyR<@6dXS#HH_jr+H+1)O!e&)43i z0bc=x^G;v#Vs;4o8(R@&O<(Hu28#&BoDi_KOWia-c;P}^+_Z5rXqRNZ4u?st28mhN zRI4RMqjG)g1`|W^t``E-M1mLIXEEBqNPg{~!)(p5Slf{5xKG-pR;kVHVUV#G@K0&DB zjBj@oN+wQm_jZeM_jXgozRQ_)yJIlxb~wMexmmHzI1wdZoG`qu1W?K>&H*^LGO#SR z#$8igNMm#-m&xBqv0ghd#kDTK|oWhuu4B3$Btc}0Sc(v`_GKqWl%~~yK^_`_6BT%Drdn}b^2qOKNvrUYbgEdO zWA{xv%v%_Iul~~!;9%Z59>Jh)5y~tIvY_1W1N~gQg{`{pL4PapKI8)4a>J!BCaR^> z?_-+PLf&oS0en2iNnQ5mr-|G)}x6-uRPeR=sv#zXF)T8rbd%DGzTntH!z z$Mf5hdbJyyc5${^C5PwdFz$A*m|tDZR_dAt_Ziw)@dk>Gy0rcC?LYL#gxI#?*P@4P z?+AuB-BQ$#T6UL$);Xo96=dp`P4~pPH>#(V`-)mCj)IH|P^cg?QIaKr*7^~|4YDMP zjax(B0RHEudA!r*+{kBSoh#G6tn2iWW;({H*(uF(`0}x;YP!7Li*?tgMXlin0QLIH z%0V-=ENij4jxlc6t*;kM6ReXh_<#Xm7yt$gvF~F%zpy6Vq+?*&l8!+gUvv!3e6^K1 zT(b_%I0my09k8kH&Ve5^rVg&G)a!75YHDh#g)yFvF_>ndVBXYTBN9Jms8jI->gI*R z@z$7^hfdOa=o;AkpMfF7kBZ)nzo~O5V$L1bS6A_`S{COuhu!zo7OioRb6eh01Cxe z;~cQRaXTq-3;v(Tm{>W^h%8O*qgY@kB~Yodx+>FYaGm>ITI@GhnjUBO+^kxP zpZBX>%MGG{%WE?|4 z!XK&lg&hYrN15ay{x;*RpD|G#t;Xr(jNpLvaglG84{l z3$~Wv`e86>C=JF(;M1@=9(Z#$qfOWmr8%%JMujwX5S@%mKYPC(cEZFxojnqT^hyP) zz0&MxAase;I=i3=Z$UncDa(l})}58E9L+p#PBjDk0kwBR81y5yDjq%HN>`5d!R^BX z*>kz3ui*iFAC=J#bQiiGJ&Hbz5QuOnKi|%BmA_(@K+w4n! z@{^y)zzzR?*)=`SRJ2PtDiG{Z`0M6dGbcd#Ju@e&O5ih0jxPkkp63>C0PJA8Z*VC4 zhT;5P*|y=>Z*zYiAn*zC__}%6_rboS(1Wde&>3_V-Gd&8B9$~oC^a8wjZwo~TiB4S zps6mv{z?FISUo4F{jTp%%z2rwj)#q=WEP*Zy+~C?`+GCNbML&Er?Gi>l*4 zd>`taRfZeC^u}ZBjUV`!>rK8V5He_R%_)+DX6OLA65WhmgWiNbh(3-!i@t(J=r@sw z>!dLt;&u-oz{3^D1(u)%GwY19`i#|6gL_=D}X zb(ta94>vY8e2o1cM!90T?c+=LQ07_Xpy|c$L0jSAyE4%#*M9W2QH98j9xpUIJB6|Q z-TmD8Cvh(`v-ujx=aY3PXI}e24Qmv5jp%1*--VFWjqX2)m3+c%RtLvxUiWKa(~?(? z!{nA0p-1E%Qie?j4*q0dPB-&UQB$|V5bZ+vJ< zQ-T&VZKF*Ue+m0Q2>5u(S7_UC4Av?r+VqDSz7J3;N0ZgJ?UQFTZkuhnKq=>x7UZ^R z?=R2s@gXl7z)-I!ZimM>)(4y~~S!PM53F>iy*~3<>~_ zTN@^dz$>!ONhn}#{=9!a<~1yYimuBQE-1;c;few;l{ha&%v7gB@MI}MfYuO)iVoKp zpi&T+yhwYC6b;l5=UVqR6|?=%Z;W2M-EMP5-#tgfUBRtI0N-!c0$&Pdh&r>AE0Uhe zax)GJWL8mTNg-&pWI5Mm!gtQ7WTqgMM9D??)o>{Bs1OQ5iHq{=QZ0I>OX0LASis)taAgwSv1TRh;oJe^AX&`d^(kpRSB$)S6Z>* zLdOQaHSQ`G^mTlBvRl!V$*;cxYs%AkdzgDbTsw z4-J85hP$rO!{;%+xNFz0-RF%Cb}XaHbg1SYDY zP54!|JHkwV*LHt#-Jdvn_G~`=(3}Zij=yKw7Jy}2urp3~{bW4qdd`!&1wF^W?8yQO z2CLvkh#GJsZqFm0Zy76aaQq@>k@3A5ZzY;W+HYd6%l?3ZVHP0t2QYj*1j{N}#vd?t z>W9y3!L!^fm_`o9>igi^=*$W~gwSA5iQRYxaSlnaK9wpfSd(!=5<}sdWIAHBlQdBa z!}87NBaR^>S(*e%giSgzQ8#oV3&p#M;`<7JEB1YyAq9i6CA@@B?7ytMuXL<44VRY$ zjp;_+G?kh0HyC3Z?CtQD&@>fMWK6#<)c#T&NtXD{74vvp|8FDw(tPXBMsRoL6@UfN!DD@L+H)uQz(-KBnlH`CZ-Eqx5Ht_BP?=^ zgZ|Zuf6uyyn;V0R1zyMNc9!EN4|YcIgHFPin(B6z6J^-aiqEU5 z1q`*x-t|{38^T&UY?d$8056oAVY?QBZB-_NIpqM97 z`S#E>O*7ou@foXFw60p3*|!{fr?Ot=rE()Ai6PFPc%Bz8@0)SLlbC6gOT2u$L0Eg7 zi(GZpRaUWR{T{h?)c8`-y2>ifT;DX)D3|!`dwI`n9M;;lX>KW^V{<`H{bT__8i3nX(sO?RqKIKr3#`*^|2Kk*H@T~@9u2^Q?;-d2Q_Ed! zV!4Y?`e)(J1kMqyWyCMx{W1Grz~ktB=vR=X_>c@?SaHb)NrQ3%(;XF(Afba~b}L>^ z+Jf;-S<+6IGE&*OH;8ElQOlw{WD5x?-`oOUeC0b`z0Li9*0|WWjgQfK$pgIZ2jb_2GAja~~9NSC$1)l0hkH zBINbb$#q>OnZH_ky+*rx15@EC;d=$tX3ciEXf<|4G&ghUvdb=8Wq~(eM(E_cD8^Cp zY|8csFTuCa9&{X?K~H-^z#N8^6XGC9@)E|&Xjt8pBZ0+a4dTgXo;2w0^$L>p9>Mc? z)Ka-U%LvxgiX-?t#VYHr3Kf~kb(sj%$sr;eS{Yw;Xbgt~bRDl(evEBCqhv9SEwqg3 zsuh67Wvi{iY#rWKb8Gnu){!Dm4?6;+Dq%_ds46AIXxU zGcYQPBdl>@?NM&Q)CIacDem93buH`vl%dBf-j9ibOf+2^Um9Q1H7!Y9XF=UC&HgL6 zfIAE5vj$+LlYPYhx?Jubdnse;x*Yu4a5yBIrZZ8HKk$KotIng5p`v``J-f+z%uPMR ziEk$fqOG>O%5>Pd)Vjv}5z>%{uEb_sBmONkI>1*RD(KS+mbr60O zm$1_;^f+yD&OlXpc`*fxOSPLkUDI^$rsOO>nZ=i3@YA3E6t!LO(_(gHukYsati(ad z;XzU&w^{CwJ^vXf!{z&(Ka8Y@OH#_X5g}pw|F(ZEj^$WGr=#3;`WXuRk=+zvvM6F? zb6(7X<>l)R8}rx7xBy zRok|QfE9^gNyYo!S~z(;s@A+zz#^#kgXjAmY`LMLTxI;&sZ*y+j5WjlF}%8JS*AI0SdvuL($k`@X?pR4wf3vuoT~_Z)E84?ZF6fzTMbp+ zgGCinuk^w#RbQ4MIXFQKTU8DOVEQVCn;qBqZ`XNWFNLZgEfrU$B$=?|l{%LhIK}Dl z&uUxnX9ay6@Z&|^%MnpxriNp%*_Iwn!AB>y?bHV-P6`Acq z#v`29m+^zBpDheEd+*h8b7avseKMh9KpAlmUL8?Na&w-*W)q@HxEyMfoSQFb0#;$`?l)^T?uPomd_k2lP4_&kQ9WTJ-8kpU(rGMAdJ`sOQ zXtQtkL9Gh|wm8bn68C2ztd199iu#uVnEG8)2@8AgfHA8lwCA6i_q}z@wZUCddRslKu>`YD;d+OkDg$BQ{?;#Cci#BOR zElA&|p$38e2tE^65YXjRjFMriPSWIgOjw@aown@@k(?)1TMZI@uZ~$PR<&xeXm-+y z`l)HOuqY|t|MhcO?5XhPf((t)znLSla^K3F@GoNX z*t5G8_^G~GZQZN^^XhIOoSuxzPYHk>#&qc>EEe<>FIwQPRtu-X+z74q_skO9K1MsY zUxGh}5ju*l$t@6;sgT?}tTQ9Vh=C|Nl9wS`l9d^11LO?KVHA>b^Agvo@3^MG=fccs zZ1v%KBH(DTVKMDzz7{dr){pd^$ArXN7{42@MH(ua&NM8L>!H_*^ZTN6yl{9& z-F1?pH3l22t#;liRRCr0cTf917MxmO!=xZ2TWhIW4GPSeikHfBZ-f6#!vaw>oxL|r z5W&TF+RzK4Xj(cEy7Y*Cv?GfzDy^LPWQJKS6 z){ehl=H00J=g_R>B$hWUvLqI}M80ct!;$$JD5T{1`l>uxoVwR66iEQ9uOzCrs1sV$ zqYk&Wwx);|8GNS5Ic%dYFx)Znhu8Z3)OoIH7OtVI(Ie=c@%~sn(o8%ex%&%@Jk z!4(ID1?yw_IKkCWXE;Ix8RW@CCWHe-%akrRar|wS3N?z&lpKoz_|&k!(45vxQ)w>_ zEt$xi@`Pgp|DtvQj1}j^uZ#hpE3)3^MAqAk#uNX)cAImNip7!Jd#?AmeRXwphGD_z zryX|5O6Gv=Ox()tsKV52Vfd1)b5f9XT`3?5v)r%5@kj7^d*Rpn!B4pU9bb6< z`RAYOQp4niC@Lqc^0)Mxt~7!#h+crfs2QN&+%Ti^xhsmIY-r4~n0Dsl#C!hv=NGOu zM3GX%v<7Gg?&qxQwUcqjpOBHm5P+}VdDfhh$Wct$KkE>CA2cu+9|*Ahk&k=?m&i->SKV&+bH$0J zf`6kXE1YvpUcpQh$fSUeQ&BVw!=R^$KPjpu^f>pwEXf7Jy$&DpME&;}IM|^m-@l*6 zzj*Dn*WQcq&zbT`s%x^MD7+6u(^)P`k8#tWR1}S6O8LDddAMMX{}0Q`1NVpiHThnx zXPFS#O!b?5nA)|c6cNj1U8~H!^H>1~} zM`o=l#-oWy=&oCCYGrj@;;1F`38btdB)UnCFLrhI?9{i`LAYpH zocF5Z_TB=^f{VT0?T*v3W~S{i(l-p?%p4*U#_{BYe15%5S5j=J$y$%wiO!CV?pA}3W~}+S+U~I`DwaY3N#K!9l?2=YOT3+ z2l$Of2n{g6^p0N!)n3ekxd2%BncbK*QA#A80E#E2hQgZ>9QjqXLCEsr> zf4V;SmT4w*b|6U3qI-wmPdm$TeD2=$Lk7|NHh=m|z3Z!J)}Av{tbF;OtUiT4k6uDQ zIzPF1frgprbf_nC7<4u0P-|fD#VTtn4*G8JU48J=Z-%fjflOef^BA{CP z?Zn5fXstKddb+2MyzTBQm3GahFGC5Xhfud%g_RlvomFP>9@Il30)1rQveH5A5gj|Y zO6x_A8=JNKI*$cag_`|1x5^V@{^?WkSxZn}fFAHu6^HgdSM9W%qSbH?&q_+cZehO} z^%+H)J)8*kJ}TXD)l*PUNtfk0!x)ChMj={2tLPYd71}_Lp{LPTw2h%22nDQ;Y8u@o-Ss!WPdlhcH?UDQsv{u}T)rzn7znBmWexMNoaA{So`vQ|D&Z=g)n<*GU(yQk>RNd5m?M%N3NFs2|wpN!F z7yBZ|6d;OLAe$V?qevPDceGBF>#s#}%(}DT>g=qRm55>cMa8ssFU(t_kOJI9;hRKv zcM0YCwtD!jgI-WNS(@wOaQN|$?VT;fRk%3JvtG4RhWfSHRsYvPgVf9vB*5}lNonI2 zL2x(J#WZeJlIr76%l&VzAHD~?UlVPIe0N1Hl=G3)c7IylS@YWT-?#joVzMK@PB-Wj zU1BP?em~LW+n(2-;_x*nZFNHOFjB#|YsOh@EktvF8H zFA5s4chX(kn`G?jC4id?g@Qn2v9L@qmRb}fi7YM>Nun)Dz-0O6QWjFQVCxWNNysKs zzxD9WcDpUhZ3Sc^H9JHUgR#HCkm8t zh%l;>PR@X0l17;jZio_+keKsG*KJNBNzBX2U+xAkA%HM$=IW~{<#7MeqetaykIL81 z96fqey6&ia?NF8v){a7W_3f;CQMs5g?Y`qzJ`-|EbM|HS<?L1LVM7C=#2;g0-LNwIwP^tJq7%A*<>?1c#E0odDGnkF6S z1KLKRhDeK3SvgB!ot4M=oz|V2g3H}PDL>!q^?Hd>|VjckR4Mj#brp=L9f}LX=y(hPVn7mAM)* zP45)A@Y2kM!H-p-dQ=~Uz^^PzWd2ml%Kn^M+XE$!|%_>#g1LHiz`Tva!S>PExLpY`h zqWX(ZZe8SCmTAI2j{l>uvd9K$yc-?LvjO$%*1zjSk+rL|d(X69uUknyu_Vh{X)i$p zh5(QVhNP(<6vA+&5PCT1LD~9pT!-Pf55sXEn>QRD77C${VNh7__0~sIN+@X*Zcu(n zKaTML28FBT&bB|lttrIkpmagItd9JQn412J8~w&nzmNDWl~V#H$z5) z!C+AmMfE$XC`yZi!C)Yh>dbT)M#PAm6~5^>45w$RSP;eP;^HFbmbJKOS)A|R81xTQ zg#ZwR9u7s(ZbaG272!yH$*x$*qJ|Avz!w+qkH=NE-FE*j+KaA4uSO4|r_rapE+U2i z@n2j`5_`!y5uO6R9r2u3c8L#7kVHY0MM3a4#gbLAXAR57%r2r>shK|FkU*8`0=UAq zA3vw2SOPO(=q8XSokBC1^p<9`SOjBk)xp?#bnc#e{3mcY9cY}<*4`EO&UI?xmJ?wHFu6r>f(T5Bez^)lR2eNmTynL{A zR#jQF6JoE_bKd@q`VsWp*AB-}dRLyk_T8x6ZI|xjb;1-*fD_x@e-HZ78GJBV(4qYV zoy^pv)KA2DO-*e%d|kaeCi=&@8@&;|%V&vX$6FlWSQL>2B!JVdQ`x^@S&7mTX7^@| zyI2ZV;J&1y`mq{>8yf)vjGsDls}1b%0@hxYo~P2S*V<;V#i~ZO(dilwVPkKr0prc< zvV-67i4x^N2$lPhXagG|QDJUocr#`z%bD1RGaIeX+OYe>;T?1sok6#vhtQMgS8?iD zg>4lwQZftM13LA+2b5M^=*6T!i@hSwWpYCfM-yv?BYps@P&j;TFO`nKLSy2DlEhV< zmr`|nNfCei+~a>**3L%rrKM7#P+Iy7qn~tj)c`J05515&WDpAnxiA?m+D!vVHJ!VF zpJ3p+Uq27I!GY#AJTo^pgSRFhG)ihrqb_hT_+n)ZA0o>p3_f?g# zkAJanI2@`fW8267I2h#UQ}01JqP&}>`9RrX3` zzJ!8u4btosrLPy9|FB`uUdMbUN0sSe0Ej?$zjw2s5O%zqC}rD*IhT@rC3)92XaYXV zumMw30E@=K7`&d;B~c##wP1f8EDlDs-7D1qW+Y7%t8Ys5oN2TCsZ2f)uXN}6hmU^> z>48Ga4_#`u=lFF7kMuC4XJtZwKzqvVD;c}eN{#-bbw>%Y-#zaNRlUL~#+NSsWa=)@ zzi@;g5`LI6oWA5Vws4ygsvR}IO0gTXFBTeG!fbhzzTcjWwY7n)I)c;C1Xg2Oa(CHj$ zVxyTn7AKLnH&{|iQx*5)NxLKHeb`Oh#6xnm`e%FNlG0OxebZAD;?!Iy246fY05%MO z*b%3A5}bEk()S}UENMbFBU1}faI=WvG*#fEchwxcPm-hu58ZP4%{lQcvp_m7)imKd z0%PRZ$x?~jXuxJwVbf(K2B0gHDmnl=c)yZQ#$314BMX&rAAPSU)uq)6S}D@aH<&Id z=PK;_i#-r|BDHG=%RZd2Q;0CVqa2tcC|I3(It&{ptYy(q1!}AW5SqRPouo5VrNR!C z5W&w>HS+~R)Exy$U9yD&(C@CCnR6`~U=Z3f0H&IZnJR-oWb!FB1te!zMssW_+YGN=R?v^42 zl>$53t`T0ya6B3e27_qN`P#fx4&OyC+J*L`(?>}4Jyn9jIeI8ikd}ya@;L1d0%zeQ z34uZs)}1_l1~zQH{q1j83yN2(>}^%ok@2v|OGw;#L$Iu(<9FbpaKg5{2UgW(utGy8 zn8(B{;kYt?g!jVtP~5f(eY7}q6lhx%b&Hmw#@Lh`u$yI|JI)rvAyAd~CR}=pAOpVj zeQRFiDuocUE%>^o4HaX&#Z?B@4`iTU;gmAffRHOgUrAu6_B6*3a%g~JA(()6!J(Cd z#IFONZ?Wveb}4fZN}^*P_7mOcnP$(>O`BA_!<214>^SEcE?gGkUFFL-C}4Kp>GgWM z1(^c$Y;#T1)=b;bCm-@L_8*$mjezq3(N#7e=k}I{IvI?=|Bwvy9I_9%B!};#S#%}x zfikL>Q5-`qiJV0Bl8u7k2B zb$DuC@a06-uu2Vpjh`P-SvWx5U8X@P6fipvU)K%WtX56i(DA49ehxmC+&9!iwP`x z|EC(bZVij-r*-C9V0(o3*i1GZyj?9D*3yFrg>*gE7kA`*W`6t1lxuYz z=+9r-cDi%DPl?yBeC|2R0g01p$Ybz5R786b0`S94(7aQp>5)lVkd$GCpp z70FY-9-iZTI8nQN?V*Pr66nAzO&{T@>iY2TBf+*^Y@xZa?qCmlqvaSNVH?d@%pf9A zdR{B(BoQ&9Jc58DEiM!a>>GOPt3o&$;eVs>*pe5Fs9fWBreO@bKs_n9p2#;rOc?`fjhBKx52zfpU1O_MxN(llL@ zB*`=-Nz&k3)7CoGU(pl_{4!`yNTQ@^nkIXH;>nt(>7pb*QTh1CKhEFIFK@M4$6JpO z$76=UJclp_t=E|Uxxq`KBxy@KcP?p?B+5+FODqm$?i z>Z99G0;WPi``*;T^am9X{79aOl19>g1rACP)Ui<4nrPoy>LNbyi zaS+5ooRC=c;&rs*=qH>HHBppQhf*$uVDnAi2r%{w;R|;#lsKi1Dv6>tJhHmFy7T_~ zB}v+|M|wSHj4ed>&d<-Y#yNvfum^lYmU~3gG#jl*RR#RQj40(jThlbs8}Cn&BpFRi zOmsRESMJ}xzg4NUTCM$+OAQ&JPY;YxjP*bLpZO5Eabr4KgrtcH-e-cv?q@E95bNVD z9J;UdET&3zPN^CBKHwT1(~5iIZVR&h$cvN&(~*ye?b0Fmel#5r+GsWi2RG<2nVv_e z$y%$4-hc45QD{zVlasv_MMSdyQiJugXV31_M9JJn4%g+LWZC-Ovy9#7mnS^e?K8t* z|9JN7*}b}L$vsKuHXut9wS8xqVX(gIdJ|>;M#d0ANJM%P6t8RGu<1zK0%sDz*aHPD zM%TjxZsB)HrL!XGap4u&GAYC4nqK1d@PdCc&XB^oPT5JgmYXNr1;v}E9okaTfl9(SX5=jyy@gcYL1mTnxQu9T^?QQT= z#q}&rvpg5Ev%4l^uVWKzXTq=7eO>qKb$`OKXH6-+0MO8w>jnT{Uc=K=F4lVX)jOP9T3a54cY0s_xu@z{^`@FXJ@x^6=eGcT*Z?Q z0PUiO@Uq0?Rq*~i)o{?yNw!0Ye2Y(^_4rFBHqqy(_)a7n7B*zaMT-&xnv7^?7mlUL zR;S(3XJ2Z?ob5Vr{LkqGaLtcBP_fs1nSN%cx_V9c@y)gEvIre}df9GnK{z`*J9~P3 z>4V!huu6X!u248UGnqbmtZB{=b!EH)01r&!D(?6DcazH_@Id@9h9UTmHGcpjv}^c1`^(wQw|aqe3P&jPIi(cIjg8RS@j^x*Ci|FTJ$Tk6fc?M3j=ngkN^{Q zQ$HveCOodlM0t2gmWguRwYVaHVHVbL6@Vr^grNx$*6Qs+sMw_+%kXASD*rH zx#cjzl}hQZ%O#pCl8BpEOo>XUNRm*k5~Xf6#4FwdH^W0eF(!v}GkdfS9^i|4d^Mwa_X$B@3tD{4}iA6VM8C)(lrqweoJpELANiI8df> z^8G~5%`lFWSk>lPk998Fr0Wby4PhUHLZbiAYZ7ZZcO_+6r=vD=Jr1969pyBq6n?CD zZiU-@6>Ie?ZcA+mcE3B@;*?5uThseYog+@~{o470=N0A`3Z7S3fcRt@`{VZ10IU(# z38tFiR15qnr97+#0SnV*KBx3D-Zr4MJD+7Nasj{jy}8`73Dx(9ZQ%hthcr}29pCiz z?Gl}?B1(yPDeC%p1Fr|H!9vju)>4r$QYD#L;xx@?n{%55s^>ss5EDCJxDHEomZ^s^hv{aY57$A6kf7Nhb2 z^fPMdkW*Uq1C2fjC@z|(V?j{83=4cQiwE2M+r9c;M++kr`{^n}(muzoqgSH`&^ypC zMifN01%o;|mO(~p4Pj!IByvzUK6n};2Z5zJBR9H;e86Xe5NA$OK!{R?PH09_oJFSj z-M5sdEk7)d;(QG+tgWrNa4}_B|94^Hj5W@2Z_V=f?=MljD%~wQ8}s8N0+=mX?#VL1 z6rU_}UFYSpq3cGu>>CE);)-lpMaB=oMiGNy_=h+vT9&-hd033V*AC&(+S=OM;Z?`{ z7u4Hft=!KqqjHadtbB(I<46Gj+N`F&=^+?~U&JNHQRN;k`i23r>Nuqp^TWpCs!g=UcC9qCMJ6?z2j`A|oby}JQPuMQAY3t+r?7E#(ihUmsZlV*M zF{V#iZE-mQ@RH}!_C!;e!ev0+StZ(Jz-D;)qp)qimy=ZM;3KXX_1F!kE*6P%89)cupypfqr{ie0{J;`~ z*PRoxDp>f!hEwf0)q&;KANc)dI(jyi4HCDn(&PdZ3Z+n^Z1xb5p>SLIOJWeqiI>|~uR#D{X-XGDjgk(Nr7w@n zxa`P9AeMW*Wp|F|JKF&0NHA$UC3YL$vGaA;a%Pg1R8 zcL||25N+N8s0J)3mRhIH7sh=p=(#Yipu#RXzi`0|P%cj^*-zsQ5pUsl;1O^Ev*D-!T4nGHXTDVvc*7Fn>%+lcPbHjy_ZgS20~zbe4!UMmwMe+ zxLqXb*?{==2ws=gwN4?+jSMN5Auiu{#CUX&7&$t-ku+i$R=nxkn!W?kR@I$wQUb}2 z=PEYPw6B=1DYjKA)*K69um`5^(8KBw)6G<-z48C0RaD)~1MJV1HCraLsO}KPJ zo&QbkqXxeX`aJx8_<0!G9nEU#*(4A?VeT{9g+)1B9{mLCvD`y>G?Q##EC8P%s0EJ*tzh=(duuSJJLM8I4p z>7xD=FkHKN)6`7c3;{yZF*MUO+E6Y}4;Lr)$+8SI%R(24_f0Hf^Viqjh>&Nkko$P; z?khyT5o;;tT)ohDgmJ9OL_J2ilwli>LW)}^t|w%)kIe7)JBv!TH!J^rPN+A|x1N6b zX=ptSBXAt^_rAFE+x_D2nT`XmJpJ_3Py55T=-X%sA@DsGE!1S;@#e-(BGq_hKDmN& zo}?SVPxXG9(^$qfnzuLjkQ+LDT{YLnKT}+B(MI5d^wf!qADe%9El~O*y-&n1w3TE& zp`~>iDy0)>$tvm6d4HDdcd7XJ^R!zS!G};2`AaXII*ESLOJD%&e`zry0K|WA3eU~$ z4g7%om#n+nFXFj4g%Rsz8Wm&jiT{&962orlI%>Y@$TGjZhz-*oJZ71uP`F)}h_ji) z=PUs0t-fG8{I;hYjnM-KDASy$ZsU$E_-{oRhChWR)J7=NMnV=S(E0z?1thJ7nBL`jt2fXrTGm}-;Y5+{NWvf;VS3bII`_UM zwO)_pon7;^3|n7cUs+#YU%#}zKI1ml|89LfUteEezql}kAes$HLO7UI-+&E8FC_hG z@3Sx^!0Ni9y1se>y^2&hcPQ3Zcwgp1f#~Fv8Ei0Y=bA)n{H) zlDGttrwgiJA{l#$r2xK4={8bzgy&0l{5|4I;WfCLFn;r;JO2Utl^qbE5QWf({&<<$ zNd7pu{3KBNVCrTpgKPFAIKLF$IS;s@BrcVAyU))LX&1B#awmlnAW*ikQ+iQIYy{Bd zXHBJZpRj0sW!gXide(m^Q(?2~=2Tm8*ScxCA5Qc_~`3 zY`GRh38L{j-P6t6i33`8|3gbaxYNXSyQnf6+ zlpH&@lwixM@PwMJmph-Kx~?(+rLovvc|W#QTWK#gBHg(bU8jX8nm(2+VZ4+an~tIa zPfHwbk7D^b)<^SbKe`reSdw+d3@@`B>XCm@GCn^1hWB_ZB9N?;O>~Fh75BDahU@c3 zZ^NagQBNN-oXYLHnTwMXpY1x~@&B+_n_cV4`i(om>z_f$yZI zEi%JYEXz=1{o{Mp9q-$r4%p9m&tYQ|S#a&BqglMXBT*xmD(NMK8b0RsZO?o1LsH@s z=#eQLfKM1Ot`{64uLIIxG)y(4qG(db+i-k*fVXmN)dJ^G&)~X0P z64wQ!?oeruOz|sOpP{X1Q}Vd$=svUb+UaRdQ%{MT-X9r;L@?{s_=I25kSDn9vne;W zgCkF*?BFS$IpTJ+s&ef0>IJu0a^`P^4WB8C%c1b612VuHs#ALOr(+u;N;?a&B%U$v zIP)t{77ITXT_4TFcQnKa0xJ!{a_ONXC;7LV%Ju2o=Xyo?OJC>t2&@SUeqqcI7Z$hw5H7*9IL#}P zgWWJuCx%c*L|;|6(*A&rI?>u^yD% z13!>jZ^E>ubql<0baByob|W3}F7zlG=BT~d^b;EHhZQBs0@%@K$;2h@W1nJi;xtFu zoN8HeF=gKwX_17Rg!@ zuhN7xZIMVNQ<)qZZH_i!n7ilP#XRVe310FE5pBBNZnsD5txMYxZ>H@L&8Dd1kAZhQ z9Cep&#AT|07_^;`q@!%$hVp#%;E{$4z;%mGwpvlPdQfO-EF53F@NRHm8WW9@PQ9Pm z3MGw_fE92;O@5ARS(y9>Z`oOhQdQ3v`0T7Ls$_#~P_P0v1! z$-SQoFA(SxV7`B_-9DM5>qFCmJ${|^D;QU7UDZthQ&)8xK17}DAg=!I;?mNTYFGyI zeP&pOD%*AGU*G{eg$gJ}`_Sv_KUDOAVF&`OI&m=ae zZ0o8%o6F^Lv%0F=yDGMA+ZD@DjW)(@Lp7|O$?MwBgde)_k;dANjg1{^v#Mbkl~!wQ zt<|a+mZ4r=tVhjeR4=N!tH)QG zaXyQR8Eu7RHDjyjaM#Ghp0et;gWT8(`U$ZX5%#j`XCn+f40@K-Z7|YdnKOM>F6+ba-$2q&E$@}_ z|c@wOVqaN8pJHCClQIy*R0b{0_eHt|8JXWwT9M;wg_K-fwZwlUb@4ffVb-Q!7F=m># z6~nN2uW2&&)yE!tEbcgSA<5e=*o9Z67CaBs_n|}HaKDzeu+0$#4oo-?UI~HVDVa(I zAC!`NakhDugT!3~3YLdyWgsECk# zSquB2hYw*f!k#3)|NWvQAIol-UwxPzhG(+F9_DYkg~uK|a|1iF!i{58LJR1Xs8>rn z4xt>D{dkHmM-9E#wv3N}EmXsQ}E+Qc>PXD5m+9kX27sTQq^`^lh zEQb_3SW?DG_aog|)UZ21uUFc?zhIZ_Y!n1Rni_^wnmpCXHl94c^fg$cd9)%oAH=G9YH_aEwYz zIjW7S2~|zdeUK3!HJL~s=WI8M(zJ}@BNaJj^2|8X=T7N^FF@_(9nwS$apL1b8U#Tq zc?Sye4h9U`9EbcXH^%=9Gr*^ay11J!_s9KEJB;3y@pYAQiDf-_v&19UDMHeLwmQB+ zD#jom&d`{%wZ*a1W0Dfe5SL%Im%*PNew(-NlO!&?p{q0YP;;aq4MIKAxhiR#LQ*UW zYEl{&`a@u6V(26-TO)unh{$!)$>D$5pG}Nvkqs_%1H5ZcFu3k*^L%)fc5yE#z32n- zmUYaOR+rNzU3?nq=SGJb&C7X;c5xhR*H%AOAF_AH1qC zdoUOEQ2Uf-LkZWV_d30s)4ZREjO|Mp!4T=FhSud5_3d?4mw#nlzslXeTJDkr{59^& zj~f1Cms{r#S@bnngGHHYUbyvcwm^p&RT%!5@UW3bmMzDm&$;t{Y>%A9TPFGb{_TIg z7xMd2A-51~LQ&Ple@ypQ9ix#RNZDsP8sgEq*O921dz}=F3x+5d^;<42{Kep&s(O-2 z5`ZMplUE#)@6JfsuX=^2HSMU#^U8-6o)?eGoq6)*?R(3IAW@bNA_v)|TIQVDFgSVg z>qkLVzyIZr9)&ONU=*mQFhKd5TRIAPrbh0f*mX(G=NU=d^-+b~QtDd6O9l*U^%;!y z3{yXMw7u>P0h*%rHnJ&|Q`sDta!RH8F!SZ;c8eq3Q3ZldeDP;$MD#USy3*j#i|%8< zdGlaHQZ;hCASsHJN&?Y)mlzSrUB3|PO`#3_wcV^JZ?90gscIT%nzE%xgi<|Ne@#zy zG(``#M>g8#Q0ap~I2x_RD4H%~-0hCo>lmPBfEn{EtI7I$^Vrx$g#Bms87>rQG;YUdr|SVm}dF9vgLV}^}nfhf+;dz=IMp& zF8nbJoR@4X-+ONcs(4w%+q`j%?oBtIfbh^X$$1!?BP$WiP3Jx9bz(|SgiqLL-> z>)&AV8l|_(6Sb7mbmkvL<=gp7+v|R`;0qf_{z&&{{Y6OOfx9R4_!<4+FgZzmLannixtMEJ04Lb>bx4y z#?Dw>;FwNiqPbky!;Rh8k2-Fr(|nW5hDIqS{9#6nKK0ZO^W&WAShr31wZkKGbB|oz z>-7dft}N1Zd8A6z$@}JKVE~^-CjKa(FUojRe?E@nayfupE(d`J;AIndAJf=WHkx7@ z+hWCnzV)nnCls~ml#XE0@rCWBs`YFksncaV+Z&0Eh3uMlKJi<|2nNTM_UQ@39KMHS zipOFhI#1O^w3tNF6H{(Xl22^z5)|d8s2+7X#%cP>Vm$fe55TmIv7(HP51#9lMjAxg z_7+i~z+|3?h{|Qk@tS2DPacAExz?XFY!hB|@T2E`b#uZ&_%Lg7=S`59WW&H%xo1|W z+^&QPZajhS_^P32vG9N`QB#RkXj8Y%F$P;zk*~vv%eEXvo6bgMrZ`*7orsLhz{nW; zJ%WdFV?FCZlBZcp7v6Zi&_ZP0H9^YeMDm-0mZLfyG_TITeE)yCHuD!l=-PWt;OoZV*)k1{<>%l zLCyXZ)YhK%Y^A#R=~2V%-)_AG4XR5$@fZeZ_W5Vi?tIU}caBEi2<@a-vLE4CsYeg6 zcaHdnaB3yHm$G2=nL&+~#p&NTgP?Z5st>;Kq3_Lp^gd|c-^aO41Na!l2Hzj8El2M* zyRVk^P>vhkyG#DhP4bhj)uzjt{lTL#^j+Ujg`kGwC*`4OY;(|pJxLGU0gyhB)G2wrcpb>1L3GxuC z0fh>;$aSLewpCo$xP5i%5!%s~2~1o$U=*$jX|4c+cMyA&_$galW}v6ip}vPFZvl`g z651i!YXV3_EPvj?lUJtK3~J_VlG!~W5Lt=Y^sWR7(z9NO-0R& z`I{P!0l77|X$)u9*Vj2tJq|dwXVL2hR-Bt_b91xv0czgKRMd3`RK@)67PwkZx_6na z*hS|T3Y=aj1gD?})k;!8$omTm)GIG8t=-V&tsOHn08zAW@5}Zk zf>1OJMBg5N8wGI)54x=KP%|@KAJhNP2zA+BrVHTFg-o3^@ZVnBA7N~^X1I(MTu(PB zg;Ke_yk}2)xm*G~KYwU{Etr;wC2M(2)hvstYC+W?2x9oMqShCEM^*FF-K8K#yL)$n~&RRZ7lR;Egb^Mysjei)%TiZ2M`S!YDj4PpK# z1PydNaCx7MiMt9UvoFIpRHM3JLA0?6aDN@UTCJJT_Z2}oeINB)yy&9cd}pL2rui+A~RvDwd-*ANGuA^JT@In z=L@94FpDSNSu^2ru0CBZR}b`680x`GGH%T7lI6=5XG$=}rwdP6|(6a zY;*E_M2Uo!7}0bX#*$1&9*YH{D2#JvY0hiGpNBcK9KS)qH&0+QXI5=`<7ra=(J9OP zr}!xq(8fUD@&(%bF^th7+5=HP^8xfI`gBA9yh@4yfxB9Xk=$A66{)CU!{rhr=oGi- zdOi2IGGVQREoTPN&ZVJUoq30xG*IIpHz{sSw%DAJ0Wfc{#pth}U0M|OCs->e!C)mJ zJ+PCCxZqeK<2Qk^$wDHHe4Ji}%UsWp(O7`~O?!H-iq_CgvLLM?U&2@9$m#M3+a&QL z{|d7n)ziyKBD}}c5u=?4IeZr24k`D+d(g0MATZ>)#&mfp0Q*v-(U>kTJv8fvJO{R~ zSr09h-zSHcm}Z0lCf6W*7!&JC+G=O627ycetJCSs)o|isZS^a7TVqiBk^h^vLhVNy z7FURJD}i9cQ8xnMsKv=)CR0&8iT^dziCb~&X%Hq5O28(|7$rf#kkytQ))!=fBXLmP zYzj*xHrIY}$I{YN5iDk!Se6L@<_Rm*Rt_v%0L7`Lr5)o_z&p|bU5Sdnj69G;ew>i` zkZTO2l|;RT!ph7XiYmO42{Jwy3`FwuLpptepw|n`ze2dK_JWfoCk;Ft&fS9v^iauP zwlWlwi`-k=^|#}{>%j5RXS9bJ0~Ejng7NEwzNQU3Ox||4+21aWw_ijEAAkXBqF4Lf znM;poeW@*#k1znxuiWal?P@;-mi?u`-Xy4zXx>`sGf&KB(yR6r@< zs|I|)Q5Alc*mgdf2!ag(kL)|06m3UX0ddj8eoD@RaX2K-VOSY--F{z==_N}5=pC3w z9(01e?C|QYXc9FRaNZHBAmp1|C_dkCaL9wyuls@=a>U4xZ9AY~wo%%*X!$%)4#G~= zc!D?JCQlzDl#qm&F||de{pcDKi4VRZGGw$5_Din5`s%B}vWj;U?1EkNP}2djtr@#0 zTQIa_N$Sg1)}PpUa{l~z$Q3OM?1GIY9Za*ps~F>#bT6=fRD0Pq&XXGI?vDNBzTnYV zpL<%}`N~PMPeAEh_ME?ct5E+2f0$iTj37n35UTlu+u5z&D`*4GfLH)t+HU{T%SU>y z)^7KZ7;iO$<1=@6#Tw4I8fsaH9_@THRG$#ol*(inRN9IU$5v9@b1~7(mYv7*@`LcP!ybulfHhFE4-Q_R7LS<@SNj z{ts|ns((h;+5xTGIg6GB7qHeDuO1HN^ZA|+;HQLS6A0{8Ceo2I$=?+ehp_Cjnj9Xu z;lu)3^G$+4ajK5bK>!Usc}2eG&$m@Gufiq^n8wTFWh{M^z$`A_@Nh#WmBN(SI$>|s z#dtZse5&wKB7-+Bg1rW*NPz6!VcgoC$M`OI23?EZjDC!M6Vns$Kmqr0Vn7kb2IsI} zkqw~?D2eW|Fc%bxK{uvIsh%(i?Kn87kdOX1uS7}2h4QcprUNX8xNGlOL@Q%eq8FLR5ijQc33kIK3okR8R>KiKNSptSGW0>k<*9ufLom zL*T}wHD5(zDsB3oWn`Judci2>1eI<%+nm{ob@=_t9k8F)7 z%%I}=K^K5@1D0{1qxI(ER;L;ds-LeO)g#Zn$S>;06C#lkkplpU{wFRR8`?Q3c!hkiV@&$_vLWc{IaYs=rR>J0HVkMiYWT9^{UlXx)#m=_@bx) z$O-}A1gfxw^+Rz?q&z36JKJA|&+whnd8+sXK8_b=+RN<_0?v&WbPfXtV|I`=tvyB&#IWB=naFBa7H!llKImE(PE z)4)IRO|QSSJ%;=G<=u8+D3kKp&~FQv##B{^JYU6-8Sv9=z#!_nO3IoC0+o$yY{*mq zZ%r*TisBu!@Pzky&veS^Ow*XorPJ@>KgA>B1wGskJ&SGWw6vYJjvq3RBY0T}IG4cc z9WY~QfnkWtR{U~@d`F}B8&@EW2_R{o(w~iEP;5A~@uNo@X#F#ML2JX)!1g=)KB%q!;EI&?B4hBBA#9 zg>S|;h*KCxZ7ev&F9%N%F^Zc1UFaUQ=LQ1PDD-rvQ_3?l1t%bpMT(?STtN=f@Z ztifmK9Xu**{Jwo7@w;c<)x+laTw-pz)S;OfnLeOQ{8Y$!M-%!8muIgNW!$3AbemY z6g10A&)UhRwN%}sD4Vj`JB`2mU;f3osiglY417PIr{EL>iFiCwjz`qL4j)6y5j28K z!muhl2ZsKKgj-;^EOZCs@5DrjZ+d2}p%Z`c6K|<+Br-KE+gw{)U6^-l#`B!WExO|0 z&IQw&z!<03YRgRERLA;8!igNuGuxS8SY2B)ZOf$c*Z$MmbTFrJ0$Z-tbdE6rM!jT= zF?=0GD7OkCap^-6g8%wC}P;T^Tr$BT9R{|DC)hU-RR8C=3|#O z;M2M&ayhy5)*El!xf5{lgw8avLESh%JJ)I0#rhL8YC0cDuY_7rc&kE-IYT(=!Kwl9 ziJ|aiD=KXZi}bp(@v9Bu2y(vAbt!8xPUZEHq2pi+YPwwcT8_1t>~srxS#XH4z;ZWY zOy||{YvP9-CHzos#<0^6783rEkwc55Us+y|uf@oE%3k-m)gD71B z=0CBfy828%HL9xBJP-aCwBXj{&*0cZRUdF$^!KJBl{Bbm5@|nlP$db5lxw>z6^ke^ zax6wKjdA+Y7??JlAZw+%Q7LUvi5MDLlVKZ+tkfV14UQS;*Xdth1G+4aMx=dV$WWf% zz>y$Elup-xH6>m*j@OP?l5Q-X=1#xvb;;@j@ghr0M>nryluD#r5Nz(<8`amMp+?k- zMAz2@tI6wLceV+XByJEDSjQ&tbhP@MZZNged8H_I?;39Q1IGyI1Oxq#mfp zEXNQ{!?hT0HaB+fTT*JbPjEs?7wN_I@vTlf4wSDuPH#!f@HH`Wk;^Kh71&9NH|WIM z>t%-s;fdPMBx}&wVQ`=%ii;qwWi8%+Jt_4cBu|lO@|>~-5vxEx9CT;<>SBkL7rESx zQ@o*pbli^D_FUI!dpT*v3L{T}30r+tVS-rcCT-fg?7|XlBJRSomZEF}-+7wAbJ`Zh zZZc1vTgK%O>GW_Zu^p1%upn|fZny1kYlcYiP##0G>}y{zy?-`6zijV@n;}I#{N5`_)&E_BRkj1qu4SxO?c-JVJKyuRF;5{z^*;QL#NT zLueu+x+I=L*Y`;Pl0I=2(*sE=Kf%&sYK2Uf!(ut&RAhD(N@DH8JF|}|8BM3S5CY`$ z*!ov7g0IC2O3!#zEWQH0+WPc5syQU&a9(T2p6k|D3Q8J{(UV_1W6$@blA%KjHzvwF zEv6I*Khc{yD~Vr^20AKYB$t1t9@V3^!ZiPhk#v(b^tdl^+?Tl0nKNf3=?us9PoF+* zGXzpwnZ$ES949A_3q%T`^wP5FacRURz{e|Xw zWSkyX9JyAmq%U`@Ob6F>&8V@OG(r%~9$_O{ZA51Gx?rsh+l^$6;Pr38Y7V_u-wKM* z5}nPEI8}?uY{JL~A%N(>+i6>p*lRPdru5Kwc|=Yk;*|nf(?PgW04%xwMze|K&pTk- zr77q0`BHWX3+v_wxY^wBT?vDDg#h{*5v~-8#EH1s+$a@3=S(^7dE|4H2!-vf(U^0%(lAZNyj{9Bnl*5eJshy1q!}JCehY%_|K9!$7IXF_ zi()UytUgREz|9JuyOX!+d<_+mpB3vZO>pGK*lF^)b@WQoIYPTMm{{(8gZ| z6{_vZBhh*^Rzwnl4ORNUv9Sl1hdeVJ4hf8ataj*vu|AX(=x;p~o;*GnkDZ>F8Fwa! z!4OEJK|pFWf={Y@!%bnsQYZ8Lv(Z9K2nORMbUe9hY48~ng%!=T0O?`EFRPc3i&!r% zC#|5}r(zm=;%}Af;gaNfSSiOLNox>2eP`a`{Z{s<>#_aHV=Z*h&z25AF<*f$qNX6# zCPrP)7Y{jFkA#d>Auq-$O1&VAdcAP2aHVI4+_pfQbe>9FkA_VF5V`)GYKCHDy*r-$ zm5t%iS&ZG>TxhJje$E>KjXkK>SMOL2nDuG0&*!oEq|S6D`(?PScUZ4s+pVoDiX0;A zu06L3G-WTY*6R>C>oU&M&0fmd{waPl{7^ku4q;ElPtXr;Y?6`U9z*Maj>CC~S7%LhT;#n=QA zkg%aJ&GYr<+Ky)brT=_iy$K4&k)FcWU%gy^=)!HEZSGiW)&zbU;>CC{%t>Hk?B;_? z?l#}|N>h2Pf^*Sp6gwrc|32- z%(UpCNp{=`7Evm3fIZNdaRFaurSepOE)iu9t{MxIxZ8nX;7Ps`ebev19lGWg+QcP6 zFSWxbt2T^l+XI%qVdq6PV^6V(GJ;g`A9j)mA z2ow~>E0w%I@b9?e4zE=56y<~IufgW7Ml!Tr-)1&P4e5j43=4{y=DL6I-aGEN!*w<7 zgX!NqDqUq>>~p7!dPNg_4@&h&7gyrVWQbU{su{a9#Jkop7vJ)hw>WHrdHRE#ba1-O z&CS$S)QiHHZ7}D5JG>;e4AO061*+mN(a3Lq6TSjpkdCruokVvT9d%V(Sa*KP`RW<- z62dKZK5su01eN*u%6Y0NRFclO_wH@CTF=y$mTFkgN{^g~%z)h*kGTWqkN zsm#w;g5bO)QAMHWTdnrqz3pehu(q^R+j3pMRPu*E?qsN{vb<&I^ESel{iBMg^8RChrWBUsX<%7H6F*l(k}c9(w1ik+b1jT@&2V?Uuln-xq(Ghv_7=&?eh6h zqsaV0MZI|hf*~9)ra=Y2!d?)MwjSI?JqT>;SJYtzv~lvLU6rC5?Dn}m24K%R1Gt5=SL$1K`pzvo89!_}iF4_fs^n`Mxm37;D= z57)A%DI_4k-)o#gd(m<9rdZoy^u|M@FDic<`|%CjudTjEoY4v$`b5>nw)Mrf7m8)b0+!A+Z2oI9^e~J-YK>IwVQPhP{;_8tYg<0qt)Enj6MIOLaDMUJB}RJ+AE4gQ$1HXknCVQBl2qt zhdDys@XsKb)&v2*B5-`UvdSFC1`dmY{YE!|Ii9!kIr~7eg9*|VG7qhS#w5&hsboWu zv%NE30M++XfUv~mBDy}^l4Z&M8g+{rlQ0np00v8xt|R~kLL^T0a_MwYl0S^PSRagH zd_uPtc=UkhM&ae=S<5DCpbN4Gt`f@_B(uWZ!E0LNOkLBcL=fUz#yD4xOJl+K%0jVBnTeom%j~x7seMJyOk5Z+#$yn(!wH z&rRXEkFEoj_@!*T2{VoY7qxeS6vEv#=mg&pg6$Vk3I_lon|?=BG>CowVa&!!MyQ(& zKKl%gP&zPd8^E@W!2p6leZMyT%mEyLG-vZnCtN?KW}u(%gh5Dt2fbtiDxxvmxF1fi zHCS#?j=n2;e51TmMI%pLPTSVo#Wzi}9h-4jNs^lG zCX3O}L^>M5U^E(y6e8mpV)MosK4nX}h5siaWg+R->~s zFc_3IiSy09x4gGx#|DGaXZ{BG{Cm(O^2N}zuJ*_I&tcyGZ5p;;MEl=~Yv4ZL3qG_l zf`NZ?Lp9zg&WEp=)>&zK&U9V#IktXHIra%v{e){? zr~4N#UaXSq7eB6PAJ1Q*(P!ar~#@Aod*gwnpOX>#jV4c43MGRyvg#qBS` zC*eD#ZsgatjPKSPUBl!1{t4p*UQ2D)-bMzYZvRSZyEY($NZzeiz`K1z(dOLw7kK4<74&9c5v2ITx7 z>z^m0LS*`ovm8h@O$0c8ELoT2|2BZO^N*1KbwzQ*ej7qGy& zg$Q;en2yeLP`$}aXN-V6aLI_Y{uCl2_oM~g)Vq1JZai?1QJE^1WtUx7nT2VuU+f9%IkOV+;!>e~}aX zp@+wKcFEZ8S^wK%8w}=lB@P+eW9Y*eO`{gt3n?tE#Ju`)$snV41dp<(jWY=dC*bxT z@#!Qa@r8yNNKzR{Qd*!aNr5@q3^Co~gXdQ_8AmT%j6hN5*9cge*X9)&q9tcxL;PX4 zO(xRTSYR-#3sGk{f;nbf+Cv@*oR?iPO~pG*$Tch3A1}6;YT1;WDv~BiaST~M8!aLt zP|8lmp)ik|&5h>FjOUkUi$xF@ zN>f=y6KBJtwwM-dYBi|?Oy2s;jKT8=BC+eu^ah4mCJn1g-P#O%+E`RF;Pj~IK@Vf^ z!H5B*a~vO@J-|^ERc(#0meIql6LJ&@8dbQVTdvCOpT4hjI zJaldP@DR<&KoV7oWZEs9(<}^qL`c7-=6h1X<~4ox{1hir(-|LtF#c zD>B`Nx}MY4nYjLNHN*b;theCn=ymaFLv|MB$%qA!N1B8=G8do)%plxwb?v|fBG$7; z3KVd-By{8j=XgQX3SG{(TtO2Bo>vvE;HWBuGl?)gz0jHt;@7<|UXCw66vyIPI0LHc z6aql#`hjo>JfSHnPw%EHE8k7sh`OJ^OT;wXyVvLEwE5x`?kZr(rV{BRMJQb?7v&owA0kK z3k3*UtF6w@uiJ+cD(!Y(%hqZ4h`l~PuiA{#Y~9F)Z;*bWaS1Ja@sL>9=BbINJGbWw zguoDX7l_g+4o#efFrxhEH_62Tl5Ig|kSP*M0eGzAjsWW?I3i7&al5u%;Ld}ON48A~ zj!hn_N8#96IpLB9dbHX+o?%Qv)BHt<@NLO z#a7nM=%J79$-p&f3Pr8iPzrYKQLtf}6rOs{_*>CAc+y>|)9XV5ZW5~8UHa38Bb(qn z4sE-8G^p)y(_?Nj{@Xf68j8>=I`ZW3;~c5lE{vPncH?G z%lsa^qQ0Dklr#dUl63ykfxJI8r~CMjaxEk!RF_uuX-mfRI-aT6*cN?)z)_&Ts6H_s zj0XU8oM09zV51#|c?Kno5%ggrB1SFh)LZDI2^TYzMV83HHzT%_ugW-eT2)Rm0CrMQ zPoFYuraWhmP|ap>?wx0AGzp~S0)a}aqGjmQ4f#6JbyDX&jnEQrwq5aDP&N0M zDK2|JZ*_H50<}Ha!{T&8z#*3m!1XF8ecO_2RXD;Wd;giw=XvR*3g<5v=9B!wf%}|t z9_(r@JiaP2|b{v_Dlb(IO z-|H*0MGGYVTJLPCtV@c&k1zYKTmcin?;lnPtxIc&YB82gTqT-is2u)oavwZC^hTr6 zB8Q)~et6`!L@6jLX&GRob16bF+0P^{*LGjFo4^am>guiozuwWm%53yud>mz|NA0HW z>l~A8j!4~X7nE2GlUD0LKHX-V-Oe#_zhis?qw{TzkW(wo(xr#+Zb&RbPvI&Z+s`ik zZz-+~LM5SV@*Suh*&#{ST=LaLFqKgR!FJn`9kMF^-@Lz{U=K!+@}TSbz3r?6$x-;X zecXxnGx@9AO}|-VC_2xzU54LGyL+UpL=x-51nKt4m7j~y~(tk2SooCIXW4<`r?uaa=gO6wd84+B*8a-5!S`$Oqg-b23A zPkT@+**@rx==nyn{omVP!pQe2aURum5ID`1=o;h$m7LkB>8;k?F#@hMaVp4}GXfiN z2WUMWoG)kE*Q2@j#p`&)CQ_l`*n|`djzdVX`1Q*Dxl7EY!~D-c!v$}xrk1u7M2ObH z-$Q|HMw71Z7Y7AGJxw*LM-o>~De5NDXl~*)%=0T1-(!j@Q?pc>oh_A2swj%%I!3zt z#x+V60T#zcr8~0tONJH}>?T8@it0F2@H~#HQ00y)sW7N}obvcb&_)N*)xzn7p~7mV zWh;$;4REcXYn)cCE-qH98mH-kTquNLp&;u&n$BpP$QrhpH8pgB)5@p-BjwK>eL`fJ zow5j;`az_Yz?5)&UHnq;SooZR>Myf-jE@79YJH zIoED(kVQxPNY0aeTRo}>m`$EAi6?eKP*YQn8PZSZy4_=drnYS(XfZ2vy{pu1l8Pte z0SqL>ilkWABTM%>TXgFEl-egTLP&r(<2!jGxbo%vQS>zW2KpuXEA$_IuXg=wO%omio+-TozcF>FNeqVwJ8CLOmUhQ_SFQ30d$zoe5U z7=Tkfa$r5>)k;w=x`i>3B2{zk+?=9RrhUg%m08EqG{?@TfC@P#O--fK71#9;?yt`< zS;4Y+iAq-Xyi2u0bxJq5bi|wcz}(nh*)KE3_OZPxhLqwIL~*~$!68mPNJL11Q+JD$ z;KRIdxpCxCOohW(#8*9b=+Ghg02YMt9l~4DCE2LJptD$!xqY`3nB)QN_hiQmzJvd8 zcOB|Xg;LMbr}AUjkb~J59vp_#FMs~mk%v0y74$0!UMURy?yA$Tx5gs6RIreoKJ7wy zSW^{#5EE>gnAxQCnrpgnrjI`~ko|ZL)>#5yqgfaW))1?P44pzb*OgN#|mLk}5dP6}3uI(e(IkFPNzDHg_p z?bK?1Mh0K~R|sj_e-EF7N0E(B1&Y)kOdI})FvHG9J}>6Ca4xS5;Rf4aWT@nGcq=dR zKJ?n9XT1U;Na73$v?)j2m`6aCbSibnJztg}G*>uOb8NymNs{wB+5MNNf8JDKjuoWy zO7UdDUEm>ZtZ>Ic#>9;DU9IWnD3b3+t zPW>?Xg7{rDU`#nhBi7{xn}ett=aa)Jl)_%VJtQr%Vb9_7=sDHwyIYI^Wry;~;X+TC zU|B^1QVxmZD-)LHz5{-i{mf>1!dN+SQcuucbc2=!jx-t`lSxPODiUcX>m>5s>p$g@ zh>W53<`KJ=Bfrz<2&y@bbFZdx{YKFXEJ+GxJDr7hf7j3Fow9WMJ!@-g#I1L*Yxxw{ zEZ6JxFUhSr&Ree`QvF8R%g@d)R3eV6-2Nej=j_VdHHFpH)i>?2yK!uHt3;{tE?lGr zG=@(hHPZf0BQr;M5A@VcWDUM<8MEB+>` zL{==bLbYti0p8N(HQTZBT(>b!om*Z8Z=D-lM-dsf=EsydmlDV+oSP zXmku}R{>pyx(e7`i{AMLmr#ieRK2fgp%-2 zlko*A1>-0j;QCM5_HWr}wd4rG*5p=eLmiW_fh40Fx`$gi>XIN0O;iA_I>a{(7!gLK zW`pqbn@AKviet4OG^LODB2lW$I?mVei}1$z`FYFf)@EP{4%Qc7*>WR3g6W{heuVY# zx^>@~-BhhsMVP5|X^rC!`w-6%*oCRfnAE^@vk4J&l#3a}iukwy**W>xL;_9X<^sRq3)mDpto%|Fp72xM&8 z!A{AHtTU~EA^)vD#t6P{(}cc8=heuy%ZRBNWfy7mJQCRwTlfm1VC3N>?)qFXWl$Sb z+nm|oF9iNoRaG`}guNxxnpkFOwV*n-WL1JWt(ceW6`T=Me*p|QtCKzl*p~vnDC<34 zrd+_=AuANC!(p{pU?r<#5!I;|3i4Ft{tR+9A8<5#1CEFqvZ~|lm$v^G5984e+LswB zVO{n$Nsa)J0L=%!Gz3A;M3S&Q20#3OeQ0`mS`!5FMn*)jeW>cg`I^0&k@0@#qsj;9LFDtCYWLjzN*_ONnv9(?%~NQ9 z((--F_!9Zs;`g=;yW=gJwBxNo;_R39_`CUzc{S4VODLPPJl%3c5rDM3+@a@-lE%)K)RMUO1S{6C9Z?&!0nIbL3&RIgDQKPjb;-EDVLIigBz~P7XIB6 zHk%vE#%GH~huC@Q_3WyiZytE4!4s)$)e!xNO_uP!SfAi04isvv0!LpYpt#mMM3qb5$)vUuFDC+&9$@^3m~etwf%T zNUJA}YZ_JBNbqwc)oj?#?R8*W$TyqqcC(o;Or=z}W+R{`E2~7ouesn@Z)d!+uuv!z z3JVJ>Q;GLkZj*&o_lc0&EW6G~-c(&e0~kkBKF*^$q}?*aDc7EnOQ0bA5;*8DH~P-~ zsq_7EV=wwS)4A1cu-vP|&!Yj1_66qx=Ox#f;o%L^{)gmFe7k$>x(d+Mo2#Bj3usqX zMxb4sgoDBIbU}OtnzhjFjD+=>30I5-YN4mh94ChFGJ2f-x5 z+bzP;lnhV5)L&6mO&u!AP}QErS9}v;Ta1Y01kp6JB>GHFQ58@PM) z*pvxMwWQf;U%~aHetpurL8SEkM}$B1mmS^dI{xDXeg560#Tk}RsCp+ zrTw-JLdm!dKHI?~E2^X4u@D>{a3LKJMXpSNEC#94Ls#q*6pORamPtY+ai0A_bQl?j z1(YUUj#rL0+t5p<{r!;v5VG&5%a3-5v|8|NV;5#ef-`aGR9QZJ>+V-q5b%#n{GOElSp?` zQN%hH_xCclFve8F(A-0=sy_O*8rHG+Ubh^hw!WpV4p&r~y3%a3Ou4g6tjvbOZtS8W zU1*zM_Yh;I`Orn(!};>0gqUi5$TS&y=%k1J8LQg-y5;MasZ$8mu>EBa2-O{`7ZG%w zT?VJoTBDs#?N&vdBT6S1y9a2G)*cjki;Ih)?*nJ3fDO!Zil1{`k>|rVWm(tc-&NJ$ zqbi@dS9x6=l}BWi@HoHu3HW{(NJKLMc*7`{mv+rI@Gf1J>01=TP~Pgxgm54mOWxY8 zDnHAmN&W;1sD<|TKrCaxK{H3=`s+n2!qpQSDtoox`ObIhvdpgv+Uv-ED5ch0VXrT* zuCDU3tiMwezM}*0M3ct7_N;ys4C)J^V+~We=k?#fH(-==5YY!@PFMi$tnZ~LS57YRHH_1Vj=LqJaok#0DYh^W#p@J!-PK2oh;E8P91s9_c?vXL*K|qZ zrI~7^>k>n?7-Kb!o6JE&3vOq|7xtkUM1Lg$aL{6q0<|PWmRvgElH%tN?PN0ol zHBsPz!@v=N$jz_8KKPh*Sz+Bx+iUs$_2!u)6}azebFV#zGjTOqH%+Jf`jtye8PbDm z%4c5B*vjikQlw1Zcq6>aE}h`q^~XSZ5ij8`sGL(XGNpR{d|h83zz7|R-6tTA4}^Pk z>caHq-CeeaRv@pjo6Iq@I_f7D+Zw}R65eg6l#P$UvyCuNh&r0+NNtdh|AtL4hL546 z=vC;oyxLNoJvdZM?5aJr;z@F_A+wfOC@P2wnNAewMaxSRC6yzK1ilKSdLxJC=kPjX^r8_*~Vei5P z4|`9X;_xx%hovVjWH-bmpLk($F`b&4no1WJ$E)D0fM}-Xl$tNfPRa8yB{;x;YRD}Z26K5%91D6teXnq9; z6l=!Z&`}1aqngI)X_{U^VAIy&+zs^RqWUSwAXR!{OrrIUACvguO z$Fvo-Nyju|csmOHx0Oa^IInvJr_96eux6zHKA-El?gYI5_xENmy$@#hP=?t*amEgr zPQs_iWILreZ(;&Ti1O3=Y-*!rbR6A=-gqVEf^h(K6+W=NB`bKMK!Z;fLOy(X@8P{S z-nH4@#pp6^U4_w%9#A=U=4xamlkw1&2gd{3ThdlJTK-+b(Rhv1^=6-r6|hvS)9!^C z94@4!a!Xmf!1upWd+RT-3{Rs;bR1nk5BmXwHOZk72)or_7uH-aS!e@uObAJ|6ynQR zmIM%My|9VHCY7S@`=ZLKKRk^0=io(d#1BnFSmc_xr~jBuJ^S;^c3RPcq5mF;m718^mTm-l3^v2d?A=x~2}5PdT++KJqVeH)BlIH8#Gd z+;INIW2yK7D{~(b+r1+6LB+$zVY71#$L~4GKj=Jf%Kfg_kNM=FmPULDY=%GfyG{r% z>IEH=Hp#<_4PNG9MNX~At2G^O#5XnbdGM0dr#U~CDLko|`j|!w{=s^zro_mFPUT(B ze)n^#68XGJ%6tod6y)>GMtP5#!Zp^oWdla=G}>V&Kf$s?nut`0TW#!8lAlnlQFJZ| zze?C2(K(d%1If5?FB8ngu+3Ff5z8JYkJh{es%lYO#Z-C=-c-r8CIKSaQL$*6ZuSob z^8E0D4~eGfY7-IY+%0n+*t#yLysk--%XKgd`q}d@ex@yTx?XJ`1A6}mgn&b@k){=zv~S$%k49BIW{-GAS6mMR zo^ZvseF*6KT9F*oi7#KI+B^wNQ802L;KDpN44%K%*?ptiUM2UC9iKpBlE z^YZz)&NaLt@_) z`r!-PT(SIFUAOdfPh>O5GF0><`g=+LI-AJdC)#Y*K8uI;=0}^m=DU@0HbK5w*YA`i zN#o`f^9j!7CloAbOo0tmiQW{v%oGI#Jf9q%pO^@UreKgcSKmN+G|4%RX`oz?aG1!= zZ~;_Nz`1BLAvDx03RQq*qz~yDr6303s7GHiKn)lf@`2?()=)sWiW-qFdqy9dd*#{jtheE8>fW=kbRw;OwhM& zvIH72oME46Y{?)4%`l@Aj{a8z!1z~E+qLeWjB(ghxuH;mU_p%A=R#lTln0idUWFGk z6+t0_zZ4chA4+|O)KD7x9G`jt_2{R2v@niRAk-z^gDEAqb|13;XSkonvi(jsgYHBg znq#&J5k>k#BRh;#g(DnPux|!B@?6kO=DQ5UJXAZxo|EGbr@B_Izi3+yI+Ehel}ZxU zhR!yd9&=pHDCBEQdp}plt-T#%Eb5vaa9s!_+X^Ir8x*M_sjA+GI?Bl_uAAS_v>PZm z;xgLSUuC{a?1_m5cCKGAKWMuebBd-3VKr`)O8P-XQ7lCT(+ckOIS1t?jYwjzX_oBE zagN5ENAR5w-)+vwbVRsUIz3JgWXm^Z-45J`dyyyL0s{bz+Tz`n*b3GlLDJ8}u@0CV z9%RbN119S+&QuHj71%%wx!Rd)WM~8NO9$^JVx6vQY`cU93tHR88`TxLGZ3*>>Zgow zaruz0M{o#L3nfv#*Gm>S@$dWa&fe{t_%Ck1{r28Yyt#!~69%%BbjpvaTVcwb0K(qv zY+X_6v$MTyGw8^$p(v&(8mbb8gQ4yvVVG)ALt;&}%yrXJH7<|OV+d8pndW@jajI}{ zKA$%fMLaDkih1t{uQJ`q50;b=UP`~Xpr(<}Xw+(xFgs5mG<>@bDwP{~^2sNkl!}0g z(vyN92<5lF^{rBth>cVVFz^*$K6mb10pr3s3{X6C<_x@Z^bKc79)H2N|6?zre5B<# zpMfF|rvc-Tx)m{nX)htsMT+ssv3gQ(f{Ke}?blsWhCqw{eRFfu7fIf{kKOV_^fz_= zRfEC6qyIk#V>tSs8)$g72woTthiWdTn(8x=aW1D03HfvH{*%N13&(<@>Ub;L`b!>8 zdf+#XxK1VRqtzfGRbgF?xaEPTt%cEm{)ozmP7~>!3NJ0O{E3%7p)k<2?8gRxv6KhF zQFKWV4NnX;QD9 zl%|`GO=VdyK@jPlN#D)SHUe#@sB^k3^CfZZhVx?fmXa4L!E==!>k`s^OqT`dr94|l zi)feES&4g6wx&lpvM#Dtvn(rvh{yrphGIXn;Wwl0HucQ|T>i}(bQoHe>~F?g8^K5f zDR1$Te(LO`pzkS~%=r4QZXr#W{}T_OE%Yu)*QEh5Eyk{3%reQqb{wbWKxpG>)5h4w zST6v!4OulEN=#d{7%@f0G)?2&aMWnU#@JrKHVi&SrXaTLH`*2g6srL0y2Xg;x+Y=P z`nIX5sj2 zzYV*IT8GVS16ax{4YFB^9%i^&#osG*WwmL@xd;Ji{Z`}l!$B$BDA*Gk|^qPPVwM>Iq`5LeG-4TNZW!JxJJ z_4;c*3hvlf`afI`r*PA%;?cl34w-N&l}4jdaVCZ>#xzat4itn*3^$UEBis1CaV^-@ z#SM4GDbm!sZ^6)av`w2B!^cpp!1OKNngK_7>^-2Q-RNwz;j2la1%PxW!#e2*A6R+YMHc6q(4uK z7gy0lo_3l(eIlK6LnEHem)lwmrY?`8uNUb9ey%AE}-oxno_+KIVX*G6 zM&eS4uDtTftFOLN5I%_Uu#TZN^n<(a@q=LqxIV-f*N3h)>=kGmK#Rv zzG}64CcJi{QmIs{6@YJ^J9jP|Hogf^sa7k$QLop}h1W?K2?%Zde~iX{<7?4d@9xM) zW>#kogl~A(^LsR#5{&mH=oE`!%){f;0Mk27$`s?YE96AeVCD`Ez)NbBTlS7EK5Q@c4DOja3AwJY~9UVm1`cHU(rWr^> zdAnr~iPD%ADo+_0Dyq%Ht(BzRXp&kzH}mE~p#ZId?0?3WZ;YZy$>*w{spj%ZWc&0a z3=OO~f5;hARogc7JUpf^1BVYEK77~(a97WtKOf`61?Q=0y6bq#g0Fm@b=NfMb??uG zo6aGAbLT#au0waChx9czaY~cXH5ht5M+_z;hIdqyrZcTPprpPtn9s4EgWb~h)d!Kz zH|C6$gP?eb29Uq%krD*u2#(z8XFl^8UoK=nEXY1qavly(=#5H4%a+P59K{f-Y3bj1kXMF^wX4i_z-0tzLqc#-&t3}9wXOckI_TeWAthI z9;QE>^Ux&PnJ-D8ld?z+E3~g)3@8ynYZ^)KmGx8m?tIN?rQ&%%m*BSuE zwP##W_BTv0Z@kd}FgJWzbdTr5ap0Y_r$7z-bjR{q&NUj1hT)jX+?;AT#&pW#crE|E zu&}UTsfM#>k7KCT7kGcF^RYTRF(cDnkAX1Y1E1#X zxCG1JV4sks(~>AEr{BUPQH&+VBt4ccHIJP<&l=K^EHhb_S7q5dKvBIU_f2DhcA~@R zrdCD!XUwWih;%j04AyKq8>G)oypX1rX)%y2&83;y=td^KSVCxy=V%TKCfb05;2kON zzc6O3Q9PKSOz9avEz4KQq%5C4z5ZGT;QPkA(CK_1YrCu{ZC$8ckYp+#z=>?xAja;g zm8*Oif_J1s1s;k^mStHboe|Z~9){lw)CkkhDvClIH3Pt?HK@|nR0F_JHHFDCQ;Mhj z*B1EoS}daJO!xdk!S{5gYoscraFvc~^|xe1g@`G3K5r{b)nr{`ntHGFwv+bRTSC4- zx7aPy(V!h*IdA71M>dMalgdlfZW` zGcmd#2S~Y1Kbtw+7&P5%H`bBbqg3T=%(xM zx$e3>zH90W7%y;Z-eqQ&mX@pqS&mLh;?iSv2z+-EAP1Jsvv zeQBfe)BW!Vs2_DW`tH)w(oVWA*l_rU>1VCkHlNSlmdA&D&4rBN-YFX@Sgg!Q~OqA9|3|=M80lwG){F{jfg3kE^`%KpX@sl!B zv~QfYz=W}F!gH#d4Go!+-D-yBk%@TujiDTsD@x7QG977NL}WLU=Ga{|IIjaJ0L0`|I7(*~E_0o2Pimo6ofA3uG%$ z=fM8pbl;ajjPaDYPCoCr>=!2?{KdS&6zkvRV$)OjHrcGJr&D<7pWxJ5;i-A={9gHc zVH#Ft^^~kq)9RJG+3u_BZ}4?JO_LrIXKb)x`+ka+G{h1q2?K5r6&O5MoI02CqmQ3O z?DGkc0yuI*(ScqYCW<#KlX=^2C{AqtcMBnNg*B%ObA)U{&7{>_+@SRQT{yHLx}K3M z{rzsWcY^jp&)V?5G?A-pwR6;z$yAYAkh4yluyX4xEZR0)?UZIsl$@*CG$w=gsXcs% z&(0aE9%I`@QJ8UH=Cm^-h)eT`u0IaZK(D3P5MJcOI+9n4noY9ye=($9ZQ?LAf%$1xgQLww2VfrDaGD zt)g=XHQT|nN#OTWt#&KQl2LxL0R=gmYEOKMgi=lj(A|ZA9n3=TM{m0gA3k{SpiBw2 zgF+#&F`@FoqPR@4AQZzmC9+J?I4lYRrpsdS!)%=XBR0-?tcwmYcbLQx_4u?TNs=fZ zAmo57N>ZoS5@jmyX*;u$H0!kY$W#{F3!Ntrz&;-b7>iVM(F8CTO*KLYF$7zium$TV zLS3{Mo$%+&h7X`;I17T8kp&8v=p`+Ff?jzFMc%wNBSYTiS_!fu?o0YEZ3RKVo$B(A zYp&U`Ty=QBb60zC^eA{&=Q!znu6PZqYC~aNfeU@vW%-Jq^LHjDCW=8LtYKUW8$ofx zBSqOyi^bHJi^RL}gZjeZq%A|ihQX$Q%zp4ySFaUq#hX=MlZ%9kXv0KatTI8qXGoV# z60LJnx9fxFxW9HHE9vH*q+_msFe2+RL;0H5G2)CBJxP`d=l-@psN@wFRL-3n9-kbB zg8>>W&2caRF1x)q6HV+WU|iTS5zX|rKb1l-sC!`;4mLJ6Htu2SlfqxCmh~p$=)J=3 z%yxX;>i7_4g*89Gjx9!wh&ey}Yz%&BozkYJwj&YY!{1nVN<#9lOUrE2 z{$)vOZdJ&YQs*L?uDWVjAugV8sh z8%yusz)z!U+4J!aHieeZ{;@b&pmqZr=BP&rHtr=PlXF5M3_nBV-rJn3bJq&I--|Q0 zr|~%4_4A+qTqOR8-w}duTkEm;AUNXhua7bA80o#)`7eFxOVXiKHXIv{O2h~M20ezA zh4c2}3Rv1t$ck6-tQidT|x0{NA=LABK6$ zUp&VwKyhIxMG8u_9mILK??QA|Us zLr2J~24}D;G)_G~UJ1r!7&B^VClpRzxNw0`Ly1%Ud?**n6(5{=0O{+{ zxCg$7EY$58GVwIak0=E+H*UxoBEB9r6ufx;3q3eKmH)7U1&v##@h?qym2>aC_s&); z03ge{bRoVB>v1)Nt7+rE4a?Fr0QI-aa~BJ@-FBN$4QC0_RTX!=9#&(F34m(Be^>b!9Zj4 zNwP-DrLwJQw;OwF0sleO4u&=gRb4R|B|V9V!;*?%y)M_=#eiG2sdqA$%Mm?JFO=j& zLd4O`{Kdb@xzAT`WUj;a2-CA<2?KyA6A1u|vgvw;EMfqO(z!=VvgKKtV;80UF;A-N z7{EX`BaeuZUJ~3Q4`JO{t)*arvgGF zqNiX!X}Wj-C2qAP8uaq8Y_Xm>BFOIC}1VQ7yq@`Mh zU#%1eZrdg*jArySD$?Lo{oD77CBbCnC8m!%^T3TgG@WUGpa>LP5RhG&t{yC)rU9n2 zr9L&DuYCK{fy@Pd7e-<-qVWV#ASlt4^u2yIQZy?XBxVyQXj3?XWXS6@^krFmOi>JX zxAqzww^EY6dG?U@x%+r83rDgsQ|uJ_&hZE~ES!%YM5Bzog2b30;E@$Z`e`I6!iXno zUy#fNsDljN&Nvyl;tmy#_ERZ)!xYur*nBtU{KE{m-q}VFz8VVk_Z4L=$We3Ce$g;b z*`3Ts5lwXYgg1lF$kkovX68hXhheC>4htocw$t=_4?BBq_wL=qaSDqLB;XvOvQh7L zyN4jW`vasb`&|4cZVZ^C)Bcpfb8 zxUj*f>`Ir`UT$ZMuZ_Crp(7H;?vgY{=$Aq>u#eddF?aHs)jKA!UgXk&k&WSygc)^_ z@*8j==T#pJdfU*)RS)!n&`bGjnQn9fooQ}2@I64}pkgzY<*&(ftAecdJ!9$*Hh9w%v6%pw}%i4Hn$ zJDkJ$3#==haVF9agkwli9$e-5*(ViAb^%Xk2Lg*aU2P^x|Wea zO^cs3TupPpILj@Q=5^#(((rSQMuXDEb=Q3(h!Ryz!+B~R!x$Ywukgik;3zana4SL& zhs%(WRiz<$`bnFZz!h!~V zfe`L%ibA785wI-5WyniX9yHbBgm6pMhG7_v#$h-fk4IrxPolke?yJ%5=rQ!sKi);b zFA7&})uq{ekl0ET+61&fSi+^#a=iDvSqN&E^&e_C7qp(i>!ndkMuU6WhAf#k&p5^N zoH327FH|@nSXRYoKbF&-Es|H~YHW^c!LC0jl}eiHuo4(~ucB>(k^M6@P50T|?3yOs ztJKro8ZH7jR9E(Hf0&U8L@n6&apl;ILlHok5O!gPHBqbi zg2Obo_wfLmPw%!>b?y}{%m8%F4Cj4<{h3^{oV_xY<>7E=MVBN|%*~KLQX0lh#+)&d|Clk3^Z#r2 zkHq&dGi6nGO;x3wIRFmhR8>t^S7no7&nHa7Sdy4*0a!AVB#brIe{T5zs-ZIo1?fhI zG7=`kC^bVM>AK0tN(jN>oJ^LK5K;ym^txQcX_dCX0LwC21V+)mVlYq>vF|H#|{RDc_GfH9l zbaepB7|M}91Vo(gUVh{*9IMp?H5M(oUme+G*YVBLdR$M7Z&3RJHfGRwFz0_ThI06(3aO{sjDTt_IclMma$3Qr#RQUU!5dFNn4$J*rt! znu#j(9;FA|yCwI%L~mP85}lGXy+6j{D9(PE&tSbxsNaRf+MFu;9!^*3YeBs3Fq2?f zo4_@I#(DS*%Cb|g)M4WpLse1t_(4;2Y0Z6ri;_ZX9j{?R*i`{?L)F*Xi)M6n2OmeN}DOdSXyK??g{?nc{gYvm> zSRzpIPU;jIYxf;{tnzex7s7%9k(2ptLOXf9PxN(c&#sdFP~@YJdTJ8SRofE$VZ9 za%w9Hen;Q7kSru667_)}Z<>}p=6w;n7);boq&y~)ccZP=_APoMoEN*_UO1c20EIw$ zzeV993iT>LQ7o=bG_dt4P!u}IK2~%LK^TR|#P`Em40*MCXIYg{V6gF}m05=K3^GdVZ&CFg3YnKt?ZSb&FRm zYgLhmxww7}gxU}v=0(+c+<|riiib|L3xY?o3jIi|smCU)y&qvupF-a&#Hd?Z&$cHQ zu%&oHdGHMr6YCiW0-h7T9 z_G2Na=+TR>D1Apx@p!&xsZQU4(|j;+T#tIx)${^&NV?|&PkjJX5Tqj{B(>R0Z4Jy1 z#r@boNjy5_jBPT;H-mWR49?hQCaZ@>CknkGnY-CwbSl_dTyG$-gxruzbPmzyh!b?! z%5&%@1EwqntN~9()_Vn*79j#Eh*X9s?W-kVsmyKHJsBYRoZb(jS>vVEW~0C#-y7IM z>gy4;OP9VmWQ2IyEz~R&$Sko@+RQ8|6q@mQYlDeyXN(UCn`aRWSYv_?qx0y2h*DUV z^Ip4~fa3O+!sjND73k6=Mbn|A5>9JdIAfQMn1?!|tBSo031V~G>3AO_AM~xqfFDt~ z#4-SJ{H6wm72pEsmoPQLc zgL*cCHA;qCU7xEy49D1#AuH%PgK~`|2lHE<`OApakYV{ng56?!3tkG8MU(Elx!ICOAo^F|IOgH6VEa` zUAnxKRK@qKYzPeb9@%(M%|2y|~Hn4J)r`xbb-O92yDu@T8AqF?|Kh5`4I}~ZB4n>LA zzDU7a$=Z=@F_;;=Cc4bJ4r017i^BJU+C+nc;W~Pq$|rpKA^$+?J$4VyyjlxSkZOQP zmTH}mf33ldJo?~KtzTGP{ZlQ!K(E^e^`;bZI`*5m>xmTZ~a zXtI0OHjIK!Cs+(+`)}n+9Mzei+$fu-e4`?;dK71GJap*LV=Hgi(iGL>II3H9;7HEc zgq8iEVl|I525rk>VBhvCKsDQZ-Rp+n`*q8BnuGqdVby&f{_J(Hd)@2axq1WmzHH-q zq>r+NK`Ho=5DdVUIO#n#cC?I?#soKM1bffPXfS8PvoO1F-)y~R-4Hf3hhr%2+gDVg z{1xDgse9)3?VH=9kf*!;lG$NZRn>{ozJ2BPm_DD&&+B1y;&-KVPfVl4?~ORA)ro%n z%CtE)H%wEa`}R?357_Li%GWAYzCMs6T4+P?_rLQvI@Y=KjpZr2Lb+=I7)9Bsw6?b=%iqS^jIi2SHAQuv z)*Ma$uacdL?7mrae2x}i?9+x(&3XLMtb#!H@;-|@W(@!!SaG=fdhXCoQ&Y9O_g?QD zPUi701d+_|4OQ?tvOb4rFJoaz)pXO;Zr{}_%Mqur)|wF#-R3esxeG%W!6RswJR5XJ zapXM|b;~akc+4qC>@rEPnUg^hP2dpe-%C2O7zzaOKKN8)ezvY1)&uNK|KZd0=UvG!VnQHy~p`FXAK3(==OEj?^1 z;*_M4#X3^hg(XQ!ubrb*d8Ss^j|rjsaL}f}I5Y%Gb*`vFOY$7D4L+%RSn+68|B8u$ zy<6wRf5dl)Ac(z7X@O8IPJt7*56vTn_21wMz~+MAaRKZ<;lU1AO1-O3EIOB~p;|uK z@A?5;o>+MddB6;zQIw3*fC90_=Q!(#3mv6PK35Zkqig9jHH~}M(>0j`Nv4yHMqx$4 zg3e7tU0eg@XKAsSas@92Ph7+c-mBpXQ&3Y- zUXlhVE<}I>i;i^BFgN!iUSC7@=f{s9KX6bIB~8~fsd%7pn_JpcCaYZ;8)lAZbLH}7 zrG$jpnT6#oYnwmy!V51vt!t7ZobWFI)WGyB9yy)2Eur6`aRlek6k7F3LLZ@62Erc5 zV}%!HsTE4}vK$-)r`=87G8w%@vh8x4*VtTS91RkhC&~|l^NS)WAg#~=^i}1>)H_e zr$Z*7rCz6wr#6?#4qJR@Q}RSBUG!r)pk+EZ?5Wf_X7=jJaE;n|cF=y+vc6N)_d=X< za#2^1izt&qNhgxfo*F$7rh5=%R6tdLfH*iE!AQ4u$I-yjt~w!v8B=T2zeHBE2lmF_ zyU!;$J9oqEq;)t0nbxOvwqQ$4vA)WKqv$?dD7l~aO(0w9fE$vyYA(W`9 zJW_tpwJZbB_VlF(s{$rs2tPYO=slN@_>H-?WlaZZ;#-l2EJ@F=Ycjy~vEh7f?rgi> zJ;Zi_mNhqr(EIrIy88SdIzK(*dk($hMmNbHC5G~dO%`wow30ff4N@QGJ1j&O0L=UNdGF+GlJLq?GR_C zv`9}LW2LGjU=aY>ZUiM?twOrHDL+=t05FEAN`{f0k<3-Nu2OhGAM(t{#bT$mLV{j5 zJ^l34g7EbHLf3=Ay6MFc+TX@!E&71Zu4@7{!qf|yDE9)kyv2krtdnCC`UMhKPg0;p zo1zEo1`kpxW-ONI2VT8KL8BXfVABG4kTW($`xcbTSt(Z4nD#`|CTA=R*B-$bA8Zbz z>xJ^DoA@g{fDa)V1*naX)DGHK5+qh?S=I(Eceb(R$@{FaXBQi|8?|N3e9vsy1okM3rnUoE!x z)4ODI#OuQd6vs!a)lVx!lTzl(o3OSQ0L_FOC^nO$crf73ptOc3y?#W+LIuMTK2dN* zAlrEmyD>SuOlOlqk1LGf4%dle5Eq4w?>#ufME%&N~=CA<=Rb6o9ud&ljzKrAzWQeav{Oqo4SUaz?hyZXmhFSnf(Nixfc z5lv6I8gqX4!kd5D(>d2Y6X)#pyAIRbr=H6GeK>43kp#!84drZe?-z=pl+==zaWO`q z>Qa!iX7NOU8Mrk<0GfpAV1}tcFHI*S;SbD5Je9gyzJ$w?=dqkXD9$n8mCCqO&>X6G z+Qih$dIf0B@SqDm1lzumGVMexH$5mmOH zhriV$bo2l^fyZILbOiUmE8GT1NL%?h=A+OM zIePr+T5!hY^Ece!s=$S}HsMFFfBoxYA9wfDyXUv z!Ljte*8PGe`9(z2{NV^8wEaE@Qklqbv+Jo&2H@*-y*L@Z$NKH??oR*zk&V$ zjnTKzpQ67(G8ZTffO0*cGII6uS$@z@qy4*gx41TgaAg2oVQ_#HFX4j+tJU5=hY&5p z%}y`}J2G6)*j-amcYk?{$AsR_iWQCy&~@+%KDfC`!`(fvtNW!xCZBuadN*ukT@^QT zgIAOI^iF;CRa)QO;$v}@FQM(|Ui4=4B>HXiO(ZS0CUHWE(vboNnES-!USHCR;$$JI zC-Fpfjy@$cmFh^qc}I7l5Oh&epGIl@l#7}!0xETq;`v>ukQ!TCQJvDA&->vSINeX- zT`~q810WF1G&F*A*JD|pZ^;tI0DuLBXa6oKkW_;5^otnPxtkj`#MKb28Nlx8{YlK? zr89R^j^*-*i+A1A}&2Oy-!yZ46>|efBWLR<}S@L1AdJS7>gFo zX&+-B=n1t&z|lgE@Y7w5o<`9V%p%$b-RLF|$zMWRG#I}MLBc}XP)F`^83^hpAd>|3 zJB+SCI!2gwNM~Ep#bhb$le-)~RkzS5CfY{hikudyD6B1O!KhLx&>*ws3nFz}S8Ib7 z)$_+Jr#gKhfCCkj2YpgM(BIT5C*gapy&L(>VwLrq4WKicrc)vanxme4=uA#mhL-ITZ?i-daW&dr{)vAOQh3m2mEKc_0 z{mjl-5LSu&QEg~cQFI)rq`PbpV;tT(F)?w)^F#7+2JQj=-1iqyDoyFK3|LY0ebZAY z97_(yu0i5zc)Kmqxl8h{!x6i>t=>1F?E^YnjbB75I*88Dt$@fSWx4=TN#iD9V5Hg; zw5*e;9I+bbaWiOVq!cGNaoP{!SvLrVxcC^~5P|!(?%h6ck#uSI4 zlmAy`yRF$D=ks}A$MF5{a<)(?JW^AWyQ3!mlNBC1bf_H+`Nxa#cO|H&FZ%Ii9Y;aF zBPaPUf-2gEb|Z`ZFlig_zl$iLkn~f?lW1J!%=vDdH04xIw%E@_T1hfBt7-XPFZB*n zgX5)K_P%-H;}<4M+4n#GY1BobTw7R}-&6Cv>=&{Qm6M9{9P?us1nBs4%u$5EvMeG~ z1}4P;0_4T?2%r$MSdMb0vAV{ImQT=q4#iyMw{L7{Rmb&bDwUav=T&C9hm_0BsbX=e zSuQty%QS4SZ2C3wY{RttX1Uz-96L~S7-n0rnQaMzA_|x>3?wCh8N>7<#f)J=5G6r> zJMK!VYl0*SPni)!MG)#XdOMDJ;tD$Eohpj!e9HG!(IF@-)lO_E>Cr0jy=y~NbP00( z#E8x}Ug6dk)##XDxX^_8T<_hu&VSXgzt`(&7^rOM?0QX(>HIg;Us+knm8NWy=e$=~ zb}4sw>NUS!g>brQsSH4WF#Eh7+hx_wpgx!)wDYgw=A%cC%DS#;n3J!P6jd>zI{&r3 zwb5u;JfF)Ii}rrWXifgQ0--X-Ot)=z<{0Ra#!{V(51b~x^R528(1?EOz&Bw(ar~I* zBvpRjPM_JeL~%V#$K&z%!@6y=kBn{hEj!aNp4g4$zp2}o&Ye4VuJt>L@;j}I%De2~ zQd@eTEre9PP zPtz2H+~|?%n+vMsR&)S|M%tKKZG#U&Bw!P>{Ury2eaeUh-sniWooG#e?=-TlL zH(SL3Bls#5Hkh$ew{C}_E4A=9Hh9D=m1feJQpqgN)aK`FGsWz-uIFYn>pG4*`zT2H zcLR5>>!1_9*?@3nCWLQRKXo*??jNXw@9ps>ZK?UdDxmk?`{#DKrn#E$-#8k*Da`8` zdVr2;sLWTD;xNeAk!=h?LW^Ac+k_Qxu1tJyd;3gebUii4)c|=RYY;3~x)ANmmY2pU z=(=}@h4Z(|-`i>2vW%j@d^d5B{q0dNs_UK%ApbILIe%DNPZuzjE8~tq z*%FO>6%eRS=;HE&DmmF?3WF>d$dDeM@{|Zu}&A1|GEH$9+Pm*i+xqWId`Asc0Nn zzHX*%d6UF-c)N7={25@{* zuVWi?!LR$nPF+?Mqa0MGz%&^f3Bp`=Ic%7g@f)AdzPYnIzh(MUb|BB_e+RaDP?YU) z{~#HM*2()BxLlGkw93@od5~S+H%m&VS|#pqs0z;bWWF+5xoE3!FojEEFZ3(7BE3z?-)*=YdQM#o5^$6L?-=x!KubfzqLH>C%5KbWM6*Y6te< z1C8d3Z7^zZ=s~0?iF7_nrk>>1h*n_#XhYHr^5z~YeMum*q0jGE;V=PeS#O0BG9kjhXF|LYPn2x}`!G3Y ze(kU!=>+Ho5;Yw~r%W3Q22aJ|+)PT|Kjsg6zjv(2r$$nczB6by_HY!gGL<~wJv;(L z$#Q|CzoY(k^gi@4zh`jmGbDU0lnNX65-OlVq&?vi)V0xzo2b~Ssn=}e?N>`Z5}tHI zYoNY`JrBp)Hd*1xc|;)6hdd9j{?+!rg$DCDk_8J2VlKyFlfQiSl;G~L)+fnUTpz>N zVI1{tg8CrcdqB{|vPt@H^P*`FT}n2_7ZFHNwP7D|77$xeMjfz^uoF;4@Q8rwI^^#8 zD5g}2>ybJZC%V4BkM?KA2$lhn_eGpAN_Ep@EZ${FuFDHKRaGUCQi}vr{MfeuF>~*T4@xgGv!%3daL~aP| z>Jk$r&*7|a#^xMP64}(d8NzH&;+*~A!PtRRK{*$bBmEU-_PtLdf6YQm$qW#W(X?Lj zR>^3@F-MX4XxK`U{JRImsfGEt1X<+!Gc&W*;@=G@wf_BIOjTxQW_-VBSrpGNOclZV z`;nl&zeDiQ@z+p&?g5C-R2H2Sk6g|y<4h$Kct8d2jqTJvM5-4wNP#b7M8vD0LqKn@ zPVB-+E>9PyF#bN0uQ3~e#`tom%b+-+VXYTe=9vaSC9Gn`u!d~RTP!rch zDI4@JGD;1N)6P;k%;kB8@i#Z&^SMx_XEjcHkOD|&82~#g0ni7}L)mHR;6AzoA;X){ z`OtJXftcpgn8j47p*4xhrgg|)TSH3MCLL*J9evx+DhcTTW3Fho(A zTVQ&&pKoziWegA7ZjuCo+fiwvx4IQhQ6Fu@S4c;ZatCqd7H>-uWZ+ovGuAvm4FYlk{ z&98l#1cBt>d2B!zDQOS!ny#{w$L$51eFi;H0u%vxfidr z`+LJZXU?2alru78vYa(spe!>cKPM8b*VIv0f>)h6b4D=?<%}%LZ1>LR+|6WJmiaU@ zg)>mA!n#6qejg57Bqe5566~ASVufUF>FK%C`$#?5pM9U~8UKCnZOf+Nl^tmIBqZN# zxUr<^ABN|nZq-jBT$Cg!gZ!}GWx;)n?nAHiQYrc03jISSyi4RU)(CAatD!j(#^1;; zN}UjgjwGn0PN;Id(;wde8WjaD+Q49YrjpB8rA-zKrh%V{_>-;~W}&#Q#N2B5k%9oe zKXc_99dsD=dfq+5n-qoe4xmTl9K10n(vcTg4_eK|>D;lqd!VbC|C)V5ta9Fy4p@g0c;<(DUZulu6I z`&Ct~?yjodcMKBpvdr%CjPH2=O(_-0QJ>>|(1Rc7y6&mf-BrccEvsP{@=j|w94d+| zKd3#cD9Rqs|2u|zwk8NCSr9t&A~$*K1==A@4-CePK8pQmQIL zN)=Tfq(!g|UqvRGfvxJ|zj`+Xj*8&sIF8>Dzp&{;zy0lR&!oXMUU&!m;T{LI3wftu zS%+SD;RW5hi^(z{68(jDlz)}&EX{Q>7UY%A8Ra-8r4U5&wT-ryea~be@#w;nAJrR! z?ex;VDBo%{8kV&JM$cTa3wh@krg_pd89V8fN}gu+3^1R(>||b}6btT=4`}d*jmBy4 z({{9bjGbi6G*7zjQ-(iv#1$~5TK?pb4`}ncC?3N`>{xCtRx`4%gEDsxH}-ru`eLm)ta5z7z6i}-q?wnt z$FM)0s?fcVxQ?faf98JoSh@-9K42fPGqhiT&{BKHSL+qjKszL^$FQL29w4!jHAZ65 zK2GYN_7xnlWZ*jH^(wFP!Z(V6ReO+6F~(H2bf~21hV~El-h1z%d$kpr5kcof_V2?6 zjMCqKoo4Q2tSe@EkPZf#jx?1Hl~na}PqjW6@3RII7~yX)jV@GlCCUh~R*0u!8&oZ{ zD$PDJ=V46VpH8?I)uZnkAaxNirZ$cqZBSajXZTCdIC`|9h@!E_S}A8eYahXj18JAc4(BUvW1J?i(})}-S58n=9@)H5^pBh*&#l` zW8C3jVhjE~vuAI(;RZptfyfRf)O&jD$mBJrwGq>_5cv_nFbEe|`7B6D9A`Ds7VH^j z&t8B1^`dzFt5~)1Nfx6tPOCpyw$>y8DSJ<2>lC}mGV)_yUefHT7`3rQucr6V#ujf+2<}8|6 zd3;vgfrny{*%@vYY}=T%Wn%S-IrR%3d;ms<%i=MIXdVB}?I@yLb*92~+)l}0X=pAL z8CbO39x}4w(+-e={;-QlWuGX{h>|4kt5AbYhGV<#>~9`^_+cHeOG#y)C`sasDDJBe z9Ou%FU&aW&g(}g*h_7+6VB zL{NZ+;Tx9q9l%FSX)(PEE1*CliW9g9j* zn~Q^Y5IPZbj1_uR1dWHPaFslm4O zj4vQ}JQOv8vLDaRA6=aZK$XPLlS#JXspYuU7A2`KPUI8&bxnSUsFzgImzC#e6- zW(*8{eSe!tm6;(C%mKF_gs^Py>t~@I<^Ma-zEfK&T90*2?@*9{Ye=q^&_kp{hw1V~ zi(*NsCF3a`2y_%?nj--!kyxpHG@x78y4b>cY!)FVI9^6Ls4?X@yx)R9wTE>NIAmiA z+Saian^_OQ?I5gHpMAX#W3)uik4TInml2dCht8F(mlrzVyV7b}ePn8gJ!}t|OX5z8 z#O;BuI3|dIOG;&HIN^lbGuDiAqBk1p!>OdEqzBT)JTWv3SrIUFokm&CfgG~cNSsFt z9D(qzcsjg6(kub6tYU{&kLhCc;JGX(&f1(zCKDJIY1ma^FocCih#EPEmr{Z*Y#-QW zPXb8$F-g_@+@=FP)1WmdLv{B#8lK~HKqcL4i=3i6 zQ{)3Ho%uH{`@gQq)r=Fhppo7)PHm{16x>^fUI999VQl4c78bR7vti*_c~NjZX|0#j zJ!L^PYVfW3sBu0EZ&(;xI-u&xOl=?+f-~d0mXYYM@KjfH%(Z{?}XqdMA!kd|Ja*w%1vU#o$ZB7O7QWD}ZNVB6*6(TDkQC{HMo zbica%rYoN;>kN-53{|jYu=j6hm#p*(8L==hM>{61HM`S3X=Gg~`@gIcW%p-rd*n>o*pgm{JYCu2G7c ziyMF9&FU)zxVgBYQA(+{vDmb4ij2+QC9sEf(bMHtFhbC0?C3Y-v1I~rHN)Pi6r)v# zM3^^vot6(~cU zQLz2Fc>!vo9Vim$`M>l|o8*Is^<3w+_0>e8VBZD;7i{(+Lp`GCwRWAEr)}dyjLkCp zM0358>4IN;i&N8tG5e5VpEmVcyQV8gRO3U;w%IJpHk+!6>WZ)!Br!%7!JQ(?cV|!{ zhE*VNvbrCjTguCAffgn`-*Ib@lU}>qc5qy+-dRZ+@y)K|SHo(E>0Vyxv|38_$apiR zD_)8bsBX~8YuiI4-(x#=yix@3Apkcp5DXYZ#r$#)@=P`aF4LU1)2mt1M49ieNjj&c z?cgt8lP3KYkP}Nw+z+VC1w&@}K$Op?UJwXi!`S0haWE*?rP_Wji`w?m;l(QDephQm zFh&enWmA0-?M8?ATplE72mN%RO(-SQ@AlJnTG!1-2NB;qL+fqgvE+{ll>#$|BD{In zd_Y?fp+VA5Z7o;orvja78nw2c&sMLWo;q>jLTJI2%e<4MsCr+H0-0r)4I!N{qh)$e>Ah)ux z{LD`LGEvoA4m`6w4XtmBNk||%k?iUHg~OG0;t$0~3H{G{W)3W+?}JVnJ4%9CNZ2z4 z3CWyU9J<{fxxv>IUbH3$}PO^Hp|DPw`hduqf3pfY!bC$twkCS z+qf=Ofl7D{8LroiYAa3KWiN;jT_J4))4EtSduO>4^qT-9y>-0s{mca$Lk;^F`=a?4 zGxXG&a1F4%uuFY7H8mxfZX}kbHr(W=rKl|0NtzmkauL5lEX!tau^&;RD+s;QsLfVv z9owRKjj>3`Y$Ww-MA61g+h#LcTUuHoerFyaOUuJAFDxu*th%yNW!l2wGUg*f{;h`L zIL8PK^p^(Kg9UA*SPPrf!!_i&C&JpRMo~(?yGBt7fAD;x)e^wA+_7uf0J(Fjswl5$ zHS)9JX`j_jr$ey^wufnF6FBdt3#7KR{ffx-Sh2`FH(HEuQ502u#ZrwFIAhty$u z>fZ_nKcqmHPkvfQ7VCAo zHh!fywHu+E`fBDAmZ}Lyv(2M|rdm&IIRIn)*-obeVTWQb`?QDY+shP>caJsIlIgS0 zQrS{9tx(Qwa>lZD=l!+tr1d4+yXkFlXQZS`6B9>KrH*GhSya1w(cLiN1_8h0oN#%* zOtpj#bzOJ38sr60m{(O9Z z#&}U$Ta${MvHyB;ZaGueP>OcO_W_~cwG2%v0AO8{ z^BaWbC0miFdk&NDfkTAU?@eY148Sk}xzKDiK^PZ<);iDrDIh)`u?KqN0Sz^gTb zh;j9;^*u>%t+ypa9*3opEIYQHBM6b{G9ECtK#y0P*tM0G2ILDH^GcP0*_do9g~ zerp-t45%2ChTP4w3+)zduehg2^8LE|(I(;QtZJF4G6~o*x<l=X=n4svKDKf1!$4gPSGg&B1cDnMf?YEmV z1yfmJhRy^wzqpj^iom$StnIWi`0Hy!9Z!7)?qgv4c52)E0qA9v;b6L662;X<&}@e$F0JuBMYso?!Nn~F_JEiZA@eCKRjyD{IJi<0OflZzE{zOxgve%OibXoH919Q|y)Rz#gM@YchX3vQOT3-+j{D z)Q(TPn7&aEu-H{G)a7LF&sw?g#>Wqlah);`-%<83t&bV|>-+AzZ*MXQ*j4D=f+*m; zs3nm+C%qcjrz^R#N5@Epj6nAtB2hQa5KP<*q8qVzurg zi9)ZbsJgB!G0v5cfwETtMQMNqcZv1TwBb78Pj&$QAE3K;@2)(& z;!Z;%^Jya_)K2$r-#%l^9=_s~5dN?jMccO@Du;)6Y_DCreSf3Z+gY7DzJFgUyC2$} z=8UF#HOD~+$!`ySd~;{fd35Kj6}szq^MwKtxG(_02vf3f3Y6mAR5_ic^Bn!cxX(9S zMM+a@72&Z24ra^A2e6u5KD?s8tiE9J>RRf7F$Og@bwqh*vJ@=(|=>Go$R4yWMX0@;zn#!sW}CyS#kQ zWg8w?V*|JME9cLjA3@>#`2vi>abl+&h#Tk%F4}J7GRehk4H*#?`vM`kllMDO62aSL zZ>}VDIsF^_?k^`MCP>>DJGy-9pmTRZ=v^iBh4St&wr!k%(8prDzA3O|*$Q|jg=^2r ztXUkYZwq}DS@3Vg?&spByeLpm+=~vCz)<^41#WG%8L4Ln6@5#hW+zS1 zPw!f%DQV!qvYI6VY1FYG6K*5hC{2kYy2?aN#ZNq&4#a&m&^aL3CZTnvg?O-udI>aG zj#3xm-f=*vzPKQOR|CMpT>_D1BHXp0;k1AC)%_H!*AY;R=SOg^!-ybfSo4#P3&Rw1AbZITzlm!HO>_cuwgn5H=TgK*bNin04@QzTnANC zxZ{`x2JN~pq-I!1xVToK;;T17(a>xi!x)`K7toq^kzk^V7QCjqGGY!?VXf{8tWJ7j zq6X15U>r!w8g#-@K(Lc`*H z+@t~)T(1>YGyoO@EZ_-!{pqcA)eSrP_kaKQh5|Xo`1_?H*^d=tu~ZF<_w|f_n`Rn~ zN~eJ}4FHRh>#3@QF>qe2R2q$$TLF6yHagp`n2k)+Gy$2WX~v%C!KUN7)w4Ql*gZld zt)oMMMUaEX`l--M90XahPBA}Cn_LTZfG|`$`Lku|AHWkV#A{(-}A+nwdY(fjS5W>Ij`U8gcaWFiM_+Pg+*4gmXsYDi$RuBvs|fSxZN ztp}QiZ5-XeB)eL5gc7Px_{w6i_kAm8F>O|D(jlzEpe$NEl$qRa%QAC+lUzk5BO~z? z$+pQGkmY6BQ-e#}CsxN}XMK=~>ULdck|-4lk|?otzSUN_6_-&%dx->$kOU1zR)bcU z3I-XtsJW7=g5;$_YpE-frbZ<5tVM6go#}9xhf0l7=aF*7vX~U~Y)^fj)bJR#rik{u zs3>CTKt)xp>T==C04v_O$)Z~rOQ%#0EhU|jiT3-3Z4h6DVO%xLA>TPRhr=n!lU$Dr^|n`OOh@D3Ijb`o1D!kJpBV?3`E!_#Z8 z;g6I<(LG)?pjRZevEn@1I}Zkw&9Xhc+GYU{)6@2Hz+M?djS8E<8Fsds7`c>U=bC`X zdc@<=h}14QyGk`FEON&6HBi2;5FDoex%14Z2*Qyw8dPYz#udK>+1nKWg?@4F1eBwy z$LdX+gX+C}_ZaL?#CQRH5P&>~=?Fb!46z1L@JNBih7R(o^$wHRTAAUEH0vxmg|AF4 zro_vO1m(+Z5Hm0vQOh7_So=!BSyG{|c=G_1$>@xn+9b4FOKu?>Z|3uJq^5>PXyjYd zqP=!~a0OP()No7O56g9bvLM48TsJ?_0Hc-T*>J-mmQ0`1t$y+f8QZn!S3I7HT4=j1 zI?#f5Y32$=kJ+9?q6B^`TIjk~rFzhxeJ(L|sg(l@hIDe7r{z#QMHEH;oD51dtpaG( z-LY^KQ55-k3d*zdYZP*$&CelQ3ee|eMIo-9qT|!ij*nv$kUu9Y3K_qKuVteZnz4!U zF)2pIa#1A^-LsdnNI^p)16dUI08!6aZtaoy+*#hq%n&^=3W?Ff79u7&iV(oRin8%s&l#H2fJ?wZ_#OPmT+Bu zTGw^g2}46YChS$fMZxmez;>U{^!t76`&bFjX;@`~|23pGZqhKQ@2?g>DCa&cS+RWM z8b(aBc#LkE@hI6h)kg&Fg>u4PqGhg;SfoLjL=@*cLd6b}w4&F}ibv(aBrvs<3-yj$ zlP(eJDh@99GLb=>f=k&E_9U9%5n$U3{uT+xy?#XxuA> z`8m(`FOK63k3J%SmRG>uh@`h}%A*zEiGC#ls;HJOd|>68vxa^E2={nF&+E0;Gh~T< ztujuC=T4@9K4^lag2^ZEeZ9w!_PgCpb1;UE$DeA9dppMbyN|Z8A-#ou9rdt|Z*FW5 zP{FmB8?M*|HdIyJKIPSBNdV1LUG6QP{OEae2l@&ur55Uj8^bV^4kF8GuF#9KOrGXcA#@}Nmh5_(oa*k9syjIE z*XE&FN@jleZJe!4O_$36lzAHd_WLm#3#Hn`R`S<<7Jp z^ps7UAw4t3?hMp6QC8-`-XfKDV*P$nDNpI1JzR_~Vav zhZi6oSDIM%lhTBgJ5?a?kWeighQHWVwbwNAuRzf8{cNEF`4CyXCG2rtfeBolbnjWj z7+>^jt{_E!W;tVTiQ_naV!IShWIjv5(f-Ur%oqok3M-d7v%UVeL zL8k}bk{YXZ2_@RAwM0a36Ga|I)S9;}VRIG>guR8~NL6jnGz~OcRUJ7cBss~eQ)T!u z8?i+`h7tC&Cn$7YYc`sI4b#nDL^a15WB%Q}x*YZw7k#`o7z_r( z)z#J2`irzd!xJ0`Pzd;j{+WFcaOBf;)lblcHaG@)S{j5U>4Da|v&lT!P%pmainq9f)y}=ZeP;YMDB7Y~A43{!e~t%T zLDy_U4IBU`Pz_tt1^3`CI*dj!KC~Kw-v?o&Zf)XM!-r56?Xx-RXpp2zhI0p~LYJwv zTWJcAV*+;­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⋙lQaVB~#PDMYk(T%O#@e5m25`=Gd7l^n;n^@Kib)rNxcQPo^+Q)0Bp` z@HZkS*mM@-Y~Ew%&%NQN7FJdk-uvUL4%OlH2r4Sgi|0!i&kfpddCOZu(1{b^q|!+f zXw6>e4#BZs3d7Kjn*Cov>6($2=nOA~*+o!?DMT;*TRTL&jc&^jMnr{)by*2V?kENk zG&F2U$0jf7Yj><=R2}tnhL~53%kgv&+!&=nnegB`VrK@B0(OkD>0ZRR`uo3-VP|7w zLu1fOE0R#_{9wI#P=j%zI15bP*K&!RhnGYCsIR@rX2Jdlzt*pTwtgo-*#OL!j z7}s-NUwPJ|L>~R1#h83GTy2b@+G24oEDL(ajTbovLX#B9RbHjiSZ}oen8P?@F2@Vl z5p;F^s?~#IY1=l&2?n=kVNj>*!(~vGI8*^`$p;a#{X50eZS87=rswqyU0$@<<2x*> z7xZoCfr}62PZJkY(9=!T>Vw34D$~X>TvmM_!1t@xt(&g;K41bW)8>rQ3KMD|Zw+iq z2?6%{z#aQbzH4-*fZ#io!1`?T{i+a-S)l<-DJu=0@r{@NSj}98tF=(~XfO!~;fQ zf+@04aWv> z9@SbMb?E$uN^9y-NKVEKFm;X&Pnwn?8d#ZO!^!#X^an!t_}gBQ%U8xzxMt`AfYGa5 zX;}|BC3-tFk7%Vy&=*DNZ65%@-yTS<_?j{RV8U%pC=csTFqUAPEG=kE_?i`Nb4>Jt ziYG@h7Fug>#~Dgs-RQczfJ7PMKp(-_Vo>z_jU0ljYHXuLbT@k3vUBL+vh=~SC_SOo zEbGV1jsQY{EVPggmZkt@Z4f>$n+=hALC8U1V9{YDdL04wf;x&o{sTS}gW*Cf;a~IP z#QZ07P@wis$_Gt<&+|P09*+r6+|VuxbKn7)l;7CZWqerI2HC5DG2wC!aH(0f7rWrq z=x(FQn3Rk)!Btty8(ur3w&m-FR1zS5KpBLpNO-*Pgzg z@ro2eu8i|~B;^_<*&X}oJ(Qyf+C~?wx@fw<{&veAfp6zV^HiO_d%DC3=kiK303vVH zfAv(YrB2 zWDiRgEz4ru!)>GcV2?A)vTfC3QZg^fyb>9clC_j=TaWBInc7n+CsayN?19r(Tc7{6 zhiL|vy!tNZP0MO>-qipSLg%g?1bJ1b*IV|WGgoIo09)_{w(RHL`ydekO0|lD*Ab22 z6(FM#3BZ;E!^c*;MS4<#TCX>Qg9la@v?qPPG7RM$pyD{vpTFv7Kl>RIeWiajT?i(g zgV+AOX!k3*y~4C&u?_tLz0#A8QyCz0zJ(Rvm;PIxdg>`5`iehS1BjD+Aw>v{2MjtG zacA_k@jCQeUX9*~K8e1L_R$}rzd-*N{VUu;`AQU?e}aCuBiQU*IwQkLvfusd(G?S| zL%}#HFz$+3Hg^aSWY_7A?ve<{a^Iue^jzdhDmVwSXUYY|l;Zg_b8}bDcw8zt z+_ctxu6=0V@NdWeNTvCrHAS+sTg7{G86L#yc}S0zghh?}@MGz^ZX)1afa^*^2D38b zrGIcr*LBBhG;M;r<9W;~+rQ1?PiHTlKvQ5W$r^_or7q&iEZj#EbSt`V%!<-e zDvZNpn4?rCQ?iSK>wz9)T6RM=++rDfciOBIK?=v==piUUaBs zQD^%0?05VT<{a~wv6%nv`j6e_Z%Y2$bI*-icN6RbrSBC|%BQ>A!?rK13jya5|Gg=i z|H1B}zPgxQ`ib2vw4KdX$c#;^kuOMNmB)pJW`O+uPn2Vu_`)?#Mh(DH}2W745zYA4hLMZ$s}xA4i{un9)NXThbRx z=qMy6u!R!qP^P6*DnX4#QKR`UU{uCsdw}%p!qZGaIqFG~qAr=f19Zc*J0hurqV@g0 z?-P>yu4SiNjSXIG473PfBEBcqMDaWHQ- ztAtR;xt4fWaNErhoJ2{G)rE1ecRG$QJjd~U6Q-WfVOU*CiEqN~B7gpXSQsk3Gdfw# zv23a|wr$%sN`0$5yc7y6E`#W%%}pkSMKJaK?NjI_jLvP_OH@C;Sfy9 za)7QT%s3wi=eHx`9kOzEUhE(vEN8y#02hiFfBd|Ng{@6HzFHaJ$>h zU>7f5=uL3JDwPgDkzuhF`Um28Ww;Rwue9S~E$z5LNd?0j_vRU&Zm!|4?ji6XJI4`t z5}iO%;)&6HpxP}#p)C%;CaqlsBIDa~8C zE0@XhkHsDx;gE(bq-6B#aJ8}T5>nYWZz+2NQa-$Vckk$!dUVY)?I`-0nE0oh|6~er z`DkQYf>wTBYU_v$bxQ_bkz75$VS&_wDHTV?JmmIW#y;F>`toXfId%+K|MU6HymCbF z3!ds85rsS-EKiqt*JlyG!(Z)J43C2C2#ga4@?cdOAn?i(*Bj(HbXec(YOqz&cG-9d zA$$aO$G)W8RhFDEsRY@Xsl*+s)k$#?=F5l2MiJjR`GbcK*J^-=!|P-bro!_?8W!@p z;SfWucKGmvwux{Z2mGA@yHKyMuOB?Mx(1zf17OxWu(o>Y;QD&Ko>om>V?!*iMbddnFRqM$GEezqYVO7*o|=347pW4>1M1(yMjg zj0gK?&z^lv#h8wPrHuEHlzbz1;SUHHT4Ydd2(nB`lo|)3CFz^)0xq<%j&*ph)hKcGVWz|`lL~jg+xhcaY36q3CF^Ak0o7VFWG}DZ_@EO*M)+2%;*2NZn-- z_z49y0R-jP@7T7jtiZOVp=`BHpp}ufNQRQ3!j{q)W5SrT)o_xT*~3jI7-!3_2(Alp z+?9qG{6T|>0JH&ypsi2ZcE7|TUnWRG@1lIr(^e=Lh8hE`x2^1H*xkq0AiZAKa z8#ZB%0qTxM$G|+Rmuoaf!z-ak8!>P{!EF1F!imoz#zbe|y>KFH{UB(7e(1R=pqDJN z{bBU%BE+|OfyUGZ*7dO zxn{JnbqPVG(z)i|d#~wKh*qiTYkl4hdH}t^R*rEqj5F6bY7yck__e}fFU$NcK-bT* z-p;>;pR4&v4(F1+7(Cc*latYK)E&uyN>Qq$4-pijgElw1fB_aJEUX^LO+O?vnstQ~ zZj}XMqzc;eDK{acC4>l1*T&4Dog0)h=U|K3X{8xo<93T<#VZE~elX~aK4DX7_wvhQ za_1QwYdV(7tk7T#OLx7ylPEIdGvTw@aC0C=eYtewQxuX+RU>5=mus&CEn ztDnKfDnAbv>51LHtLDj2=^`+}9#M*5_1qxN=1&7@O|q)*r5}}^JH@-^0Zj}}GB-lU(4#TQ=x^J!M5{|;`*=3h-Ueo)6s6 zv*BqsjHy5UBRWo*Ph~wDC}FCyuT*X64VRfp87S1=g1$n?1hT=#>X&(VEC4q*B9%c1;0KFq)TqI{g?A6VRc9xV9 z9flQWK-n!mwKX>Vs*No)%yez$7a+9YP>v@845&K?ZM!Hce8vS_a50{=e1`=ra@ea> z7msS!@1Bmrk2=yr?qj6u`u{J`QW>E{06v}&>MsCNc8=<3F7~eCU9Bp3%DE(O6zumm zHii!!JDLSbf$MqoWcWP*3e5RkjXWSg?>1hCLN;!X4PZ$s}xpF&?o zPxh%r!&V;)NtmFFmFIi_YoYrCn1GaT6j@a2dPq2nS>0mLF5K;JL&T4<1XBd~WJcHm zrXcTv4&#u$CQ@j2s#620kLHYCe&^@{FEr5s5JS1Q%E z(Oihi_TEBpZET$?#R(}AVdXnOIlPS9P1WP&z)PFa&w3S|-I}RY9Z_XA> zF_({Ac`$+~9JMA{P1!t!_M>aiE%A#x*=o~N%&LiQl5){7QRNs+iQ=T4NQrK`m1nL*maEDOOT!Vhn+L@_wHP( z&CSj2ix1-RG2Ni8JDpY3r;dzM=5jy39V2j~X4@nNl_-95g_LqF1?Sw|PYJ^VfL2?it#~BxmYQ2`;)GhTv37|Bn-xJ#Xx1O`J2VAyHnd>J-Fia|CKT`~@Oe*S{1V%(ea6*EOE+Jh56N32yAqyOn?30GEWCwyD zAea^jDH8PMsTU5<>0CCgp-4Rgr0lY)Oe)+^Zjoro0P*M9-*3QusFVG0VPVNy#^peqpJ@eFhBAH{-vb8vq zKU(+YG*IZrv@=FYn^tR_F6^jx9U`e?=32dl6hJ&p8%a3`4E^|$4j5B3O<{>HQ6}d; z;uP}kX~+3+zFMuiMxw|p`{ABFdsOvi#%@tKgA*S1O@Pc2!>d-Sxeq(eX{V5PKa!JW zD(cC0cgQ}yXU`tSZdTPVithvc-?rI_$7n}Rsz%?4`Ah;nDs|_LH#0Whoj<{#Q*|nB83n*K2|h+ge+`#MK`q&2Vi3k**D}^TU7&qKve_hlWlWc zFBVM;B9>%a$>k{+X7Sdo=lNIE*(e6cx*umfJk2!#2a}CG&2q5mXY+X4G-9G!-whKf z9qI(hN8oFwMf1)qw5X2MBewr;fPu515&{Ugu_m=D7pu^naZBK@k|>gkriCM$kbXm! zB#p|gTpl#dV^~lnayJlsY}@5mv|ey^&$li$iDvVApklEDX7+au4;9Pdp=u8&hrk#r zl{r1158E|rvZ~;*rQjer*VTxs;%o$gU$6Kck=?;sX4o1x;9v&L{&pdUB^ec)mrAK!>axdE|atu zTf=HxD-?LpqTCSyYQhfLm|{u75#h}WCnC|tywkisilWs>#L=N)KBp*ZTn)i86FhJ_ zsLL{H#SsglxLegt`|&7(uI`?DlHI#^FVU!9(|m6K>AoHVt9QeHsYZal$$_9d1Adyp z!c9JH|7m|`?a3V)E1pYZ#d~n_cc|Wf#PJ4)KDw;U9d$u<#anafjSbbcwRxH93XKTS zt!(Q#oEqI9r6=@_<8+qUz^$ngdd?=}g}04K1BaENo%huGsk84=&R|&!+Qz?`RU*$X z83p4Madi>lX7NeS#87r!7V=XK`zARpVyOI6+cxqx(3|to7X* zp{gHLobaXk)Mo$8o!C-t=zGsy>5+H9e|6`FV2t*m+tK6bdGxoC51FnmQ*K{;%2;^yy>ARQOjW|u(^%3BS5w6m z2J?kX7=DgeIB3zn5I?@2B{*sxY`Q&_z%lV&R@CQJ>Ru$%GPrEZ;6QCxZC7pE zv8Nt}AGa``$$bFLFTw~uk1A2-B@&l_6f3{SqdEjmYJSNU2}FVGx-*31DBspB8$(cv7{X0>(6xHcb$uk)icP)c+|Tiv2O4rrn|A2Svo*3fQ%7-9va0Zf;7<%l>C&WkrHR z+@a$GSXMEGuRp}-aH{uMQ`=2#0iG;=0miYTV{VBH*Sv34H>tE$TWKi+R)jn32R`k# z5P|BiP%t+^l`a>UFd}HVZ?4?_7q{d7QJ&YVvCws^90#Dm1~)BT7X@0jYZ@VH0&K>v z8-|9@8wdbF(3nw_4OtM?0}B$Hy!9{!N@ZQI85Y=f(KbjfSE~krTyaOLs+y*`ge#^A z`FyjM%MrsCM(mncvrr&^OUXl2QQ$UJreq}li_XOYv$6R`^iCaNoa-9M1W9kZMZy7R z=pMW~CEg)V0{}7grin;rFkqOC18Ogrnmn{}=FG~W$*H+SZO#H&5UHbh4K^2rvSqwu zJ$4loGr^E#oq#3dA@Fblstm5(#93dUmxZ{2Kr<8Yn;Km|JQ;bxtBD{SVm}GM%4jR< z5jMUZy@VSDGR;%9q}K#$PAH2CbrY6aG@|g&ZLGd3?TuIwE`$Pcwz0&{NgbfjEWdVcWN`fHf^P(Vtv;me>(Ou3oK!7wyfvi3M zwbA^1RE%AlGsHB^N~Jj;wR%4F4p%%_5@R3G62}K?Ytg}VLhO&%_!HlP{fo7WJ_cWK zPFDe{(rGk_Roj8;U>u-qQJ_W?7Wr$qGU?z8Hri+g9Y*)~LacbcgoiP|fm=&Gr9!st z4cv*H7!)b?$<& zfXNBNuQ;oB)WPXAW)wPu9HDQYzw1askqiWIO?qK;i*X8E(h=3Ey3mH4&(W7;HDk}W zKR6igkat{KHmAt1X!yE5u%XxFmG|61GaP)(vnTo;+%kviPDD%fbMWl38xDYs7q6IeQp_&}Rq(5v>15yrqJ!@Jrx-vD~ zPzcl!itlJ|q3xh5{J^NaUnY%^1UE@?i()VP)%dZcaA&#I_5@rrr{ceU^{Zd4DZeKe z*$cA#5t9%gk|L8UUG;I;;GF043Tsj33B-L9{mEU1{kHx4_fy3%qN*g_S8FsFrR2)= z-L!F&bgQj8i{Mf8d>gC9ZzrllN^BqzZFVL(fGvt5nUpy@R|dI-N~II!kvYq0G#o2L z(0+A%D`ck0Ld!SfI5w@SA1d5dlwa^~b@!3{O zEGf=7(4)aDBLFiU^%&1t;M^0vA;i1=QP~K#S=L>1&WNm!IL~gS%{c;NnCBp)LKm&g zJL)pTbDZBb7rstUH?pknx?G2mx0wiXndmFEW_S<08THXCqaXo#DR)$I^W_lt{DJ6+ z-~Nbl+mWb!m75~Cclcfp8N6nQVZa!O@DOL4a-^^7`U4^6orYJo#_$n%4r!=|j@0iQ zL2^B} zZkmcBV7=)EVLadiulc^L9rO{A~qGFo-*

L%H8?0vBC{)rPu5ARy2bv@dH)z&GYZ*{Oz>wYiZczOpBw4sQo7&dyc{7L;c~;Pc-cgH|Id7P(>YVlis823s*I z)hKOkZ9R*3)3woOMu#{37CC68pB~A<$Lb3*_%&_?{r98kCJdi+=mrtbXPhTa*Gl{C zLyOR?O{)}HcIp#IZ?Y_W!F)TeMC;CE!_X)vF6GV3AHPZfn&_rIcVSEcPp| zTkS0h7?ZSAsUcUQ zzE@Z1E2f(QQC!#eT~{GOYwGQ41uImkdp_a1=fD`x9h|6%R%JoJL~Oy*avYli#lT8- zpIqtz-F01CxctHB`a=l?!_kL{NxG!Uj5jl814 z&6cJ*Dj}+)YSzyuO1`Zr`9@<(0PVUuuY$Ir^T)u;{bTXbBhsZYF!dHZ(OK45kFHVChJPebWLJ4L zVTNHhj*@)oSbyMkRCqk$X6gLUa%TcR7U{wvv?3%XMn1w<5k?E(Sco7_ry^p6Dzd=x zUKI%Vk#KOYpG^aPo_EzgtTo6y^pItn|IG{p8miK*+>{inf5ykqb>laqAgV_aP9a$r zp4-PK1E6bHz|MTKthlo95 zus<>Kxm+L-{>MQ*>X5?d87$MAYiUE^B5P>mSl4a?_uJ#T^QPuQyMAeKpFDQztE}x5B z51OqMo@163lr7706cregC0Uka#sCBnn4Bv*4kP4YJiDx_ijrnOH#C*(T2NI**)eN{ zVLNR#0U-sUt#jJu-RLSW4(j*lST1~+0T#$aFc>P^;%g>emJos%(onxvE3yfXk`A|2 zOC&-E0$Pxwqfx(Nhvsr4e8cLtoNbBlM53wlyg;<<+z%+>3~YwNtmHy_iH31gPVoaR zIxGF;+z_PjUlJNd?){V=sQ4^AKf z7F@g?A0o2{O$F(&>fCref^KwbWW>pX=d=4Ki8BX{7V~m zus-{JECX7jj(k)_vpp>lF#&lkO`;$+jY>43(M4%8sk?q`2P?Evlz9Bn!TpYcF080F zl~08td+E}oHgAyvVCROyb$@Y7{&nH&U;p~TEl=HW!wn9G0^%MbS4eoISBK?yMMTVCRp#rNLpr5T!K0bqE6(CpjdZftxh#;Rg|uoGo})0|Q? zRZ(XZjmeFAU1(&NhKLLvek(k?`8%PQ#{es%oTdU?W5NI{rUd}_u)=ow^X?xh7EAf; z-|@`MzFf08&48)1j1=OzrF}Cqcx@D}utwzy$OLOpE@uQc(f-9@C1m5;nzB#Ag~@*u zog@20BO5IznC6l@I!&lbnAjnW0{RHSD^R@VvorYb%y$pgMQkKwGOIq@2>w6Xjt-*x z(4*)vln*}=ODBQ>CzxG4fnL~+PPcjyg{lJvJ(6l<+j5;0W|9ow%bg~tPaP&p>13r;Pm`?eES z<)<2rMww(OvAmO-9ImvV7wPt;6r#A;%JJ ztXb4-MocBu$o4}g2E$(-%n6=JeUA_7F~m;j+d=xFem@ThYT7ZDc$uZ09Ra4wmJ!AG zk6v8N^gp&a>67AD?r?G!HWS>}IHAh`zt@Dnj@uT7(1f^rU|Jd=aNQ7@T>z0t9dj7> zr?|n1I^)n*L>DDAO0bvTtHo&IG9PiOuX0X4=%4P zXScePF=K3t)oN4wySI{BZSBaBwOWnb>N3k7i78)ft;Q_d*ql-K)@*rY`GeE(c${CB zWm$GvK1Q;cea0~Cp%mq4#3&J4+5F11b4P1DWJ%Kcw`P*CA=cWN&o;v_goncUMcA_} zcm9QD5CQ~Y6HdF9weOZZ`#K-rkKPCEkJ$5|;#V{zKxZPf&VXOoS+|jJ!WL_yOh(jW zShgl0>B#9l8kQ75dx}&zr)q3D>^COyExx1Z#)2n6=XybB2k1dAZHaNs6!Tws0Nrl4 z6ijxrGbo<lid6k)n!d zP6O>wBNez(@|Y1rY%ZwrF|G3kMwkYnq4tkV!KLJZ*X-PS*=2nGdsG-(r>+%5ZCVRG zgA+x{sG|ozMVrHZS(g;C$C~+^$IP zOvj{z_U&R=v>b14a=~8TA1&!^w&|2lN9K_odh`!*$@G{KjOicDV*4=uj<#qtRCDbs zn&LF)i8K4C;i4lAx1F;A| zf!|UJh`|)?>I{3y4YdF_xt50)+QV&3&NIfyq1!2>D;uLQ2t^dw3;WhOK)byG@8Q;o z>j@#`ETxpcXq&RbH93rY*hvXUFd7&iPLS^u@vZ1Rvdsbe1MPb`F(XKQ9-NJ>k^?vm z(+3L*`~9Luz@7%(zAc@ONNrX}Dzk6NByW6dj;LBu!O_|-NWc2|KS7 z)$2Ai(|WS>-;RHpJyQ0yfIvyH*50}Eh-rrD%uMxC_W3<~_6(u?k-=a9gF$Ox zc&SQ4qW#|d7{Wbe7{af=;SF!tUatdw!-Ee#s8_4A;po89Xfz6ofR_AeROYLNqtVF5 zK@o1>)L}FhjMZv2P4O6f3oQs}hMsgOm3X27cN740fC3)v!7vgx4!B*y?TPK;e5<90 zmRk{uwguKzzLpdh1d&#?H#qo~<8Gg5V<=aWH@E@VRxxj%tZ48}yM3dvgqF}DbipQE zN;C%*rz+IVRqeHg;JK_<6tjSjxDg5klX8><2tlgt9evuzpldp45+0~&vxtmJ@|T1r z?8HDEa{n#ZxSO?!p)kgN*|HQ+WI3&B>>0W7@Ps5HvWA03{I!NZzKebTIzl1>-n)(O zMHXtIB}MJ0D4R*s#P0?tgQBuP0aoC~$;*%H_gi?K%M56tFrn%i zU77Ih6)Fn)Y_B(~3nE>y{mu3Db=+E^nyyY13KV31c*Ao+8Q@!s@wRTc#8_3u+( zuW1@_U7~5)>($jc|Mi+6$kplTUPGCE#`VgKuJ@0&hT#aJ$_K~;SnRZX{bu*HVT-BmlTsa8j;aIL_>3zh6Oq5amJFJJ9lcDCdsm- zY1&Ro6UKNilChxeG;74JjU)%<*jJDipD{%r>W%h9zN{g~lRGjDP|eSQuq{UpKS}s3 z-|d3!6MBPVO}4&4`!`bLzQ>3ss4K58v*o-_?&ySw6(ye! zTfe)`1smc#F$LkW@{-F?3wl32)@8DnU_>QCig5&3~7l z#VhruRVL#Sv^Awr6RHbsv@Q7rHoL{Lh_nfhop!KFm9{6Y`V-3ye8WFP46o>nLATG3HU{^!DwTQ_i)gdr;k8RrJ2v zUAW4XO=rDu@#4ku?!AuEMU8XH@%HVfI}yiNeSIgd+wse|EoAez(IVx=u4z+#Vq(jF+YCw057>3%WkWFXJcp;UlXIu$D zVQIFP^aIIpVT%4Fsuu;cCbZioWBfyPRSmA#fxB`ISGpK{_b_K{?$oJMRQHYv7qA%i zatxATIDv<&0|F{w+hCe9K9-tSK4|Vly#y+13sNmd&pt;+P2MPT_32g*r^qL#>$7ntC0s>q=xdB&NG7cXuToD?1-pgF6LLKIonXGI^orym3RP4yvC zI~~Y%gq=DO1$ejBiibGx5{P*2v6mLZdX_EC_F=3slR!(Z0bI9ouq>M_>2{VVQTW<- z`xH**mHX;vk-oSWk;XbT(pGOnmM!tJxyn+-bs@`2(T!sYp>$c5$whIhTJ6bLb%MTF z|IC@77oxYIugg1}Db9`5#*GFzh7V+u*nu;ntt1w&`9V{%aiti;NwIN~#yep|K;W+( z5R3Q#dzSV>7r+e}ZhDsXvf!6Gmf@xg5QpLNHC*`n!7S~4pxf9P~X$w0p0G-oQZ{bi+TYPA793-xT+$m$dgl&78s8F z666Jd=MMkgwrx>8(5$>#Ys~$Ob zj#ipqj=p`CO{?P~NHMPuZQB;}&s=fE6-r&N`AX2S>u`5PM8=YcF$VkaS^9q7ueJ9g z1X%;Ex_!;`0wvH{xT3S=H3X|JH_dk8Amw@&ju9KjBT=9!D|QaWVy+s5{!|(*0uGtL z!&q2m-M2`;%siG`3+wSEQJ zUIEF39{PO(0m;?@R+td1@Y046=e8GHTIS{>EuvUga>sDCx^;BbaaNCRtuk($ykbR> z(2_|lS&TiK^SDACXS@=xK8ym)QKSbfq<`gk#@#D~5Jr7Me9DOOV$k=n{#w{a+6MOK zRkRxdByGJeyi?Bla!CuIoR`4|01#B=je?sTTf3r52tp%(j5HViQSCb1oU1^Wc7)x%y}jAaH-GQqY`=r8AQ`tPq2zvWQ^H@82T*)CiYs%~HI{fZ=p-KxoaRjHw`4_3 z(FmfP5r|Qq_Pa0D;evz`F)XW$#r!*TX0TORf@do65p9Q!*U^pR;Eu;GrcCQcdipYj zF7hzG>$ZN;hL$}x-3m9N=%n=Z3`!A?<8fJL1{$gGadVQ83WkLwinBgHCxXHBz~&!`Ne)_|FX?+Rj=IN|YwjV}F~)HZxwYkE-H_n- z;v~VB5T;xhBRIvtreX%W^x2dhBqK!Mt`Wg+pk;J4gv)iMFNDDg z2^oirPs{$aC`E6B!v@oq4GY^})7g=>8B9|C1(fk8Z}8nkyJR1}-=ZY2M)px*rJCHY z&R(A6GheueYT*7)#f?Vrdbds%Q1s>!k_I-xgPbA6jP7_0OvN zlWHYAPX=(;^8umO{S#Z9oIjcjG%w>rdHc%u{;TIG4E!(y_n=@iE(IZYap?*;C4`K< zpxarQKl4Tlkj@->KNf-~0@RC$%|tBJBq=A@QA~ZKlKYQbYb;1-tNOjxcm$u_rlD>` zJ2l*L`=N;<=~W*8-9+H{*=G|@;{Wt`6&liB*~V?OjoJMH7xKVT(QQh7W1r1}0IoWK zQ2HUxf>4)?ZG;3f~YomAM|N2TNqCnPy=;fYdIU13s#`VS-V=28&Z18qz{ zu!ikxg3WoCB?3t*nV!=2$GgB5SbMfiy&G~%(#w4lYYP)?U zOa+=KSx2H~6<6oGH@nspd_mM=IX^j>2y;m?XO%S5l&9mlIU$*uDP(WJDQ*XP2dv#Yg50Yy7@BuO?RS?G$CaY&a=k{vrD z*Z|I>XjfOF>RcS#6-6almOw+ zj?rb)G(-Di3~%3lIv$P81QCvSV__UD{fG|ReK}VJkB_BYF25LCVlYDPP(1=asN!9B zrL8Dft`Ph-o!u87bPjx@(@cxgNn+$_xUGS5+6KB%=iD^ydZ}(OhLI$*Q%x=+QK7-VdPX*R7A@v=k2nU%UimYE1MS4g+g8j)5_%>_GK8srQG8)2`D_15(>O`Mp@{0Srfp+Em%nV>9{{NRmg+q zPfiU6QABc-0GO<0 zuQ}eZpiGmiYzI}^LKp9FbcNR0RAA;Bq;*z)gEpD034-2i*6SG8>&>Pv2%1d2P&J{v zJI-CMKof&BD(1@7g4&}gX38iw(4u{<(J~$u!oMQ$_@zJsqm55m^95%R?3;lBHuYmm z)dI?_WEjF+w>mz+44GcKj|Npn4-K?dO|@XGsn$o$vR|TySe(}|2J@vb>PNqZXr}MT7!XT`Cn%_FC*3vLfJ=JMV%>div+zv+0P8tv?A}SxLV1$ zsYEJ^1nalvKr*RL#CjyCv|L5<1-sekgr>E%bE(h^Ul*203bl9%e9f?ZW59<{3HmnjeHp9ayc00 z!Z1IpsW!>O^^YCN;4Uhk4=5p4zE&eKJ zo3ikUYzCzo8izN+Udd0ebRu8ABRVAErQ37$(=@5UU2*ag!WpsJ|HpIV)gk-YZMWS< zO_ScXg&>q=RQEMyxwY^%MlZeek`AE1gy;o6qIoo_#b_r=qV0X8DTB79x@a%|Uiyqg zT(LNhy78Xo{mHBxgv(UqREfKhQT6xY6H@n0KdIlIh)s$*KUcjvhGSpQG}&-VCE3%mXP$fRIVjvK#m8pnK-Xt+wN4(b-nJ9>uuj-I}I~ND8jzr1ri= zzhUc|qybQ)+;Q4e6ri(nR~$b3_rOUPm!$IoB@ABYfFl>*u-6}~bTXm7_QAP+p|MmM?4dhmGHem(3PBmkqMf4`9CUgZd^b0INE7G`I z4HGz7LlS6`WNcX^$mGOe_9baGymEY zDfE`%_O-Z+milwQ(!UYC3B41&7kv;R8Ass(wV(}fi%_McmU}9M2n|q#QEC)<={636 z?%Nn}C8c(6jOGivvG7ENAGUOC5Jf`FXgle)wh_yVb_5~)F8mYDd5QB(lVK}t5c`4h}4kTfr(4!-~gSdv~X{t~;){=12v7Zd&pF9Vx)oMc0!95`a zb<83WLsn3%p12i#^nnLrOlOq2kNUBB#KRG{IyzRU^@OdYr9LfHU>sgLhb$OnLAY8T zPy-ET-;^XT4-Ea;Jmg`P@55r|)~SElZF?#HYxPm;`fC- zpGv>SSd@5!Mk3QLvwbN@7RKyMV|?=v+@Y!t#fKg3H=2!>lt(i?g&s2uhH}l&Kkdm^ z1%sufC66^kvBA70QB1ysaZ!vZlAeNTKH*Bk(?iZQH%pU@x3A#2JbJuZE)};9jeJCa z_`WO<*r#;TG#lfAjvUrWvlYUn^hsL;2>N08CAbyv|6aU|t{9D;ys&*y;O|RlOYS4S zjvgY1aJw7TA3zrM(Y6J*gVpG+>oM^E?!}!ucj~6ej|Uj?LzL<~KDDF6)WYPZUiMCs zcFB6yuI#gG`5^svOp-)V22QDDweC8fG>(l_g^E^qorBpX9s-A3MX)V61!9Q&QT@h_mu=x@-^fk6r8U=N&ti||@_7eaCG zE+@ex+xK*{W+&Wv<-wXsEyZ1gk7at)gr}S2TCmY47QQOGv#{ zcTUQOaF?e1X*#hHcU2WLQ1qS$pX>NEx?NhJfsdlNgBi3E-zQLE#-R`(zmsOKJX$kJ z!(%)DmgK>cd_u!7+ZY%i1e4AJ*VQF|kW%s{S0*R-t&?h+#j75GRoeaQWS3TMR>C+D)v;IFvu>s^!VD^n)nUX<)-@tx5sJ->C4pc zt1gvWQ$xqe%&qlV9rUJ#B#paN-l2GB<2aVR6V3SH!d+e+99yq(I0du=Qw)AopiU{m zv>@{gJ7e#b}>fugU zAaY|}3rI=#qcGND#seoCrWk4#%03l*N|1`P33K*w_z#U#7D-C)%(yzvZE!6+rD+~jy9WlG`)2Jp%#%=H+4pSQ=5lH%_ur5oaB>$*nwNa|+H0>>RJtZpS)DlOc11ZC zRLZ`Ev8yO9#+F~M1m|Qp(AjY~{w36w@|vnf=vok{bx_lQItCZiN>jLwgh-n=gxWqK z_=G`7BCbWaZ1!05Xd8;Tn}8DACw=cMITX?3wKZ+E+l{++r7*lM5!e1Yc9lr3bF}7B z7ac%%qBqzTF}TDD-49wwPo7y0O4|^z3xM8ANlJWcDUEYL_z3lqs7HUn*&|+pvMcfC z6-z*kWkB%?kg((4v_GS|P8Tg%RpkLA)L0#JO6c$%=kV$5<>9d22KoEdD?m2W?#;YG zcHod%YmdRo4lTE<>NP%NtJ&Onrlzf{{CqSOZlQ2WJ$YDgJ2Tkwx))(`gK3)2B!|Z2 z(0B%IkF!e0Bb{nIkI4k0fglc+Apd^79mKr^#^s(a#G+9rZI=(2<5b+a>|dsDB?2X0 zuPSjp_uk*MbX_EOe^PmM?S=>X{eHzKWL)bVn6#V-HU7O{&&iVbrcCE-3lzmHoOu)` zUUkI}rlzKDdzJJ~*z6OuxBen-`a2Tw`>NItfwpJ|;}P45Z~g!%sl6?gt+cz={M zc>mq|IBNP6_n{pvKsY*V%2Gh11R(|mU&8Nt=b+#tio2`w^kB<%Hv~LHv>5;Nem3s> z*Z26ZHuhm@VxnyzDQ5aRb0o5DJw44y53ILj1yAhI<&ByCHV?|+Gmnc$ck56oXDnm< z`DaopZ1xfPzHFf3HV;hKJJ223k6O%^5H07X7ss4Gr0-YcQlKxd zispQ#wo(rgzuV;Z6VTn=MsWHEZGIwha$HF(=KOo!*o_Mn4iPtEZ*Ur7*H9Id(hred zi#Y#Y!?Wd6t83~|d5%S-fB207R7W20LEX*b&EL1bRUUVO^S9#zOsCTJQ1!ck=@08p z@@t6q0$38*XoB_ww70+0hok>PA!*U(-|NO{mG<^=Xa60K{=s7r?caX1`A0;4*0_#3 z(PE)*@On&}7Z^^YSw|jU6sZNS%GC1%AAE%R2sNsG?>*;H%um-md9N;w(4l{GB z?_r+PYPB5a^^EaV`L1=2QQ(3BXKWZEWR=4ZJS6x9kp%NhlI&5m2`wJO7@Unz)5h&k zYxXGnGme~c*giNxe+khKs2+u zb9>WFv;no&fbiD;A!O&Yu@*X{s{`4zH|oL_Sfr;(E`hN)@$;&eD^g;uF0f;w6W9N2 z3H$&Cu1Xm*^Ny=?8ct62dQ+1j<+|(SO~$C|ez&o(uz&wTFKUI{sr>rr>UYcr`%NUo-?dff{ z!nCr3FhU67QQk2H%P`w$76>GSV1_6GBsNoo6|F1bdjxW?hXSFjWOt1HTWcATq)dYGyKZu>iM-*`lC$Tq>Or?#ooBUth}z+bOM5-LsY?H$yq7HO&FdB^zlo`&@!3x;S_$Z@!G+a?B(g3hqM?1x$q$%g}gZk#n zL!g6C$O<<2Vm=$>@*?!@|NPb;MF?Dk@FGIOqX>y1LgJ4RlFlI{{WC)3975z}gy{c6 zNFE?0{|7?sqX;P?LaK<6`u`BpT!gfDAf&yFkp4VE{4zquDnh1+kogvbEDa&+y$IPd zLiY0rIRk{8@p~8whCo3T6uO`=3Ptsy=nW`#Lvan1R6x@-G;>3D zvoI(DLqjm!0mD<^O~9=&xUDw$TyT3Sj5r1(Q!pw8qmwY^5R5B@@mZMUhRN$-svD*z z!5@G-2Ed&I;I}>qEQLT8?ux?i5^(os2qs}#5~e>0GaWE12(zOwClB+w!~7DMpM(W* zSTq0@yJ7J$SQ3P#wP9%%mZf3EBq()3X&&x*2kuS4$^fjY4XXpN#t&;l5b{DO3-?vS z{qMj7ad8;H@0IeHq?!!TWCbU@3g)hmS`=JPn_O;2&YwUJW~HU}qKVia??q zKAj1lmBHuhAh{2|7y@5zhutOcRRF$rLRA&)$w0aazO8~x4!$pkePK9I3WvOKI1NWD z;Y1mn901t}{O|_+Q~MY$Kc|2xV#Fk z_~EJ_u4drcX1G28{y!Hff~*=jwxe1;bkj^!doHT83e{bR>V;AL4^V?7YFLAu1JKPu z)OaCkvJMq|fC`VHq6$>vN6j{)<_Xjyi&_<+)>cb$6m30o0=g^-Lr85Y&4q>QjLFu0ppIpnhRAU>_RfK|@k#XbBqj1{yvD zc|GVi4s=@@-Clu4wnw9#XiN}|twiHeXo3q(9DpW`LzBa3N*YbAL3jAkojDX3h3-nC zyIm;gMAIT@dKk@gqggG`?73*J1I;T%^TTMtX0#}X7N0@OoM?Fjt%#x0IJzf??yW(q z@@UN{v^I=F8FWABfgpM?i5@zG9^Qx6m7w*L(1vof@eJDJMVqS8qXj5jA3c^pPpm^v z1<})0Xmc2C@u96Aw9SXM`5u&}(k=dKr2nj^5gg-rk4a^`rL^=z}u!QD+n{L4U80 zKG}}8m!lmawDSzwwGSoY=+igQX9@Iq9wn>MKg-b6a_IXs+FOeDdC>k<=s+AD z^rM5v(4iG2p_3VuO`sn_=tmd&F^7Iyh;k32pPxj( zgwTKIqUuTLS1&rX6rEm&&P37K?&xeaIyVlTOQG{Yl=q_xVRYd#x)?^6K$m>z(mr&# zJGvZ0S3KxS3|(!Hu7=RnG`bc-*Dj-)O7y=W=z4c_y#^zM$%Sb%W)93knB}k=LYO0p z-I%~?Ij~yqU^iv3+Uu}7UaW2qs~5!T<+1vkv4$S3kq>LM4|9ew=P~SN57xK^*7yw8 zB!D%^VFh8Vusc@d#)=(SaV6Hwfi*jWHTPjH+G8zJSj#xpDuT66V6JztHesx-18e&Z z)@~Bk-hs8xVja?0#|o@d4C?~cHGp+{2kTKA>rsjI^kF@-Sg$h7y&daaiuL&b>suS^ zo54H@te*qxKNIVJ2pdp_4J^k7d9c9_Z18q$h!Y!<#D-R5!zbw>R6B<1Ohk19sBQ$+ zOQ8A*RR2rVpg3yK3pLn?8Wu+l6R6>rs8Mm$Xe(+w5H)Fzn(Rc)K+W@@=HH?g3Dj~d zYE>Dvx{lh^M{Q@KcJ)!aZ&CY=s3WM;SQJ`?I!95LnW*bisOy`k+ep+s59)pb_2`Cr zBvH={sAn(K^9bry8udDYdRIn$oA##}*T-$dhDqwxteVIZ2=7)?x~Ng*`3F`AMCO-Z1sPob$t z(6riUdIZhrhGr(vtRR}T7tKzfIYAT&pvWpTw>X-cMDwbndDqeW7>aI13x=Wv*U`eM zXi*xp=st?QiIz-6OWUJm&!Od|(egN2F%qpzqE*+?n#yQx6vc0&m%`}f<|t7bB{rgU zdC8a>U2%U|fb6e5*vFJi` zbTNW1l}DHMqRY$Bl^D9316}(Py%IyOPDHPbMXxVIZ#GA7l}B$~NAEm`-VLDlpF;1a z&<8Wohvm_S|Dcaz=+glD^i%ZNQ1p2aeSRH%5kX&pzFvjCIfA~8q3@FD`!nc=4Cu!& z`YDNi8H#@0iGGWt-zTC!>Z4S9^k+Bp*9r7bee`b}{htP12VMUM-S`yUoQiG*(Cr|) zlLp;Mp}PrmFNW@?(1Qee*c?5)jUH{qRL5+^k6pmJj?)yzY2r9-d7N%6PG22o2;dA~ z;*782Oi`S<7tWFeXG!9$dvUf4I7a~I_!Z|&;#?t|yE)F22j|U!^ZttSg>nAEIDZNU zhT_17xIi2iER74L!G*y^(%_;2Tr7Z#SH`7s;KxhjCvM}X!uXjC_}L8j*eTd7yiz~c|E53>=C2^G)t~wJ}OX2EMarFzh#!g(bHLmq8 zu5$#}EsX00alLnO{VTY^o48>VHwxp%wQ=JVZrU6-3*qMNar0MkixW<61TpB+eC3&aNCKv-BY;Tr?`C-cc_dz25`p$v+=+#`y67RNo~xK|MO9*FxC#(h4-eZ#nK3WsC3Uk2Q78SY;k_kR@+7>Wnx z!2_@0LAUXcGX zEO>GrJSBmrF2mE(;ORj;BM+W&0nbe0SrI&YD~{yBb3=GuWjsHOqd^?~5-&J`7l!eo zu{ajSi!l_wx}4k*o!UkV@vB{ z%Y4}KT-XY*m3}NR6I+!JTb&$R6TsGv#n$b`)<4IB9xNEaeh*?BBUorFwz&|t`6RYw zCbo4Ewrwc3-G}WsiTyDa`!hB6=UVLVTG&6yv47KJ|9!^(e}L_bV7qo=yZ2&yhGKii zV*3VS`@UoQBiMnX*x`fNQ4e;k5q5kacA^w^@(=7(JM6Tu)8DZ(SFv*wvGen=3$3t= zUhGmY>~aXZvJ<;n4!gD$yY9zsRKjkAvG7prW+m*Fu-mDzJ6`NwX6)Wx?EXpYK?r*| z5PK9KdmO+b|6os!V$V`z&m!21R@lqT*ef6Qx)AmT?9F%V-AL@c7yIDBJ{-h8cEdis z#y*e5zWA}PGqG=HvF`!wM>FiF2m5sgMG=bf2a5UtnIGvOa@|n0=O{)H#cYIPT}83~ zK%M}Ka}>o5qj)1x{8A|XKPbUYl<+D_XkJ8sxT5&jG)TRP?cJ!>QGe8i>lv2HG`;j zIaJq!>bFA;m!U>})Wn0DW=74Dqvm5#3mS{FjCU!%65wu?}^a;SY`)S(yZ z=s_L#qE1&)=jW(P5Otl3x&=}9qo~Jo)GHtAy%+U4i~0^kzCx(qYt%o41~fwhTcJVa z&|ohb(g_U>qhSFwJd8$6L?iuZ)JZhjhsI<^V?U#DK{R0`npg`>ijO9H(Uf**YBMw~ zjAjVUY=mZhMzg-7*%35nC-R5UZ;8>|hE=uCQaHZeLE1D!jIF2q0= zYoSX+(dA0$iWgl?j;y#e9+%6H%L6XY zYh2!~xO_oe{%*K}jc|o};R?6H6&0>ndR(#RxZ*o;B_gv_^Wv}AO_rg`E zhN~FHRTi$Y4_CDvuIg1>wR*Vfsd3dqe27+`LTmP*HQ%GP@1k`cT7MhbP(>R*Lz{0x zTh5`a_n~bE(DoAA@jTl3Gupic?b(9%4M6)3pnM-xcpeqkpi&9h$I!w5QO9G*JBR!# z^1nmn6{ymVsu@%pfogvvw*a|MQR8KF=qnW5g~BB$x`d8o(2-Zs(Yw$w4;^2EPE^s! z&(P^(=*%2+b^tp2JvwjD`8UzU4d~*}=;~MK`Z;vNHFTqgZtj6@zJ_jTLAQZ!`wrbP z9^L7oyPijP|Bdb~q5D2W4-7yLe2N}?7Cp2BJ@hGh_*3-AcZ25td_8V}L6MLa1AYR) z^!sBB<+Jz45!2tFU^Gu;lk2@-G*NheCkFBMcsHhVcb!Lf2DAF({e9@qUApgM?3N&w z>r1!T+AW?nM{QwJXlgEuOv5z}SM!1Ki>_`$8<~8)WFqIP6+cRCc2&Q$*fov^ zeuo#C$;ByVnX5-;;ZYm9n)RZn5zb1d54w7kKWal)v#Fx1ZL>k>ySmBcnp2r8&cFOW6)n=9j*3Q=EDd`z^n_A}T(FPmZmIc<%Sf1{V2WAT% z7Q<021tt+#;Dn7F*T1k;*%6)a)&+^F#u-G-T fW)Ed&Jd*tO?2oAJz8^TF1JB8MwBt|k%>V-cm@&IA diff --git a/lib/fonts/fa-v4compatibility.ttf b/lib/fonts/fa-v4compatibility.ttf index b175aa8ece8b1881ed7a23ec6ee6db6ad6b7cd94..0b7ac89fb7cc4aab0837e7165ac968c150843a86 100644 GIT binary patch delta 392 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 delta 4758 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 diff --git a/theme/boost/scss/fontawesome/LICENSE.txt b/theme/boost/scss/fontawesome/LICENSE.txt index 39e18e3d30767..e69c5e39a30ce 100644 --- a/theme/boost/scss/fontawesome/LICENSE.txt +++ b/theme/boost/scss/fontawesome/LICENSE.txt @@ -23,7 +23,7 @@ as SVG and JS file types. In the Font Awesome Free download, the SIL OFL license applies to all icons packaged as web and desktop font files. -Copyright (c) 2023 Fonticons, Inc. (https://fontawesome.com) +Copyright (c) 2024 Fonticons, Inc. (https://fontawesome.com) with Reserved Font Name: "Font Awesome". This Font Software is licensed under the SIL Open Font License, Version 1.1. @@ -123,7 +123,7 @@ OTHER DEALINGS IN THE FONT SOFTWARE. In the Font Awesome Free download, the MIT license applies to all non-font and non-icon files. -Copyright 2023 Fonticons, Inc. +Copyright 2024 Fonticons, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the diff --git a/theme/boost/scss/fontawesome/_animated.scss b/theme/boost/scss/fontawesome/_animated.scss index 93555b2f43767..779125eca09b0 100644 --- a/theme/boost/scss/fontawesome/_animated.scss +++ b/theme/boost/scss/fontawesome/_animated.scss @@ -150,4 +150,3 @@ 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } - diff --git a/theme/boost/scss/fontawesome/_bordered-pulled.scss b/theme/boost/scss/fontawesome/_bordered-pulled.scss index 9068253a7421d..06bf72d8dd5bc 100644 --- a/theme/boost/scss/fontawesome/_bordered-pulled.scss +++ b/theme/boost/scss/fontawesome/_bordered-pulled.scss @@ -9,12 +9,12 @@ padding: var(--#{$fa-css-prefix}-border-padding, #{$fa-border-padding}); } -.#{$fa-css-prefix}-pull-left { +.#{$fa-css-prefix}-pull-left { float: left; margin-right: var(--#{$fa-css-prefix}-pull-margin, #{$fa-pull-margin}); } -.#{$fa-css-prefix}-pull-right { +.#{$fa-css-prefix}-pull-right { float: right; margin-left: var(--#{$fa-css-prefix}-pull-margin, #{$fa-pull-margin}); } diff --git a/theme/boost/scss/fontawesome/_mixins.scss b/theme/boost/scss/fontawesome/_mixins.scss index e06b69aa54bd7..80ab5c81b2284 100644 --- a/theme/boost/scss/fontawesome/_mixins.scss +++ b/theme/boost/scss/fontawesome/_mixins.scss @@ -54,7 +54,6 @@ content: unquote("\"#{ $fa-var }\""); } } - @mixin fa-icon-regular($fa-var) { @extend %fa-icon; @extend .fa-regular; @@ -63,7 +62,6 @@ content: unquote("\"#{ $fa-var }\""); } } - @mixin fa-icon-brands($fa-var) { @extend %fa-icon; @extend .fa-brands; @@ -72,4 +70,3 @@ content: unquote("\"#{ $fa-var }\""); } } - diff --git a/theme/boost/scss/fontawesome/_rotated-flipped.scss b/theme/boost/scss/fontawesome/_rotated-flipped.scss index f27fabee400cc..0bd4b7cb07d80 100644 --- a/theme/boost/scss/fontawesome/_rotated-flipped.scss +++ b/theme/boost/scss/fontawesome/_rotated-flipped.scss @@ -22,10 +22,10 @@ } .#{$fa-css-prefix}-flip-both, -.#{$fa-css-prefix}-flip-horizontal.#{$fa-css-prefix}-flip-vertical { +.#{$fa-css-prefix}-flip-horizontal.#{$fa-css-prefix}-flip-vertical { transform: scale(-1, -1); } .#{$fa-css-prefix}-rotate-by { - transform: rotate(var(--#{$fa-css-prefix}-rotate-angle, none)); + transform: rotate(var(--#{$fa-css-prefix}-rotate-angle, 0)); } diff --git a/theme/boost/scss/fontawesome/_shims.scss b/theme/boost/scss/fontawesome/_shims.scss index 7809aa649012c..7a894a63a5309 100644 --- a/theme/boost/scss/fontawesome/_shims.scss +++ b/theme/boost/scss/fontawesome/_shims.scss @@ -1,2042 +1,1578 @@ .#{$fa-css-prefix}.#{$fa-css-prefix}-glass:before { content: unquote("\"#{ $fa-var-martini-glass-empty }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-envelope-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-envelope-o:before { content: unquote("\"#{ $fa-var-envelope }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-star-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-star-o:before { content: unquote("\"#{ $fa-var-star }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-remove:before { content: unquote("\"#{ $fa-var-xmark }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-close:before { content: unquote("\"#{ $fa-var-xmark }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-gear:before { content: unquote("\"#{ $fa-var-gear }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-trash-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-trash-o:before { content: unquote("\"#{ $fa-var-trash-can }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-home:before { content: unquote("\"#{ $fa-var-house }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-file-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-file-o:before { content: unquote("\"#{ $fa-var-file }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-clock-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-clock-o:before { content: unquote("\"#{ $fa-var-clock }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-arrow-circle-o-down { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-arrow-circle-o-down:before { content: unquote("\"#{ $fa-var-circle-down }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-arrow-circle-o-up { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-arrow-circle-o-up:before { content: unquote("\"#{ $fa-var-circle-up }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-play-circle-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-play-circle-o:before { content: unquote("\"#{ $fa-var-circle-play }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-repeat:before { content: unquote("\"#{ $fa-var-arrow-rotate-right }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-rotate-right:before { content: unquote("\"#{ $fa-var-arrow-rotate-right }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-refresh:before { content: unquote("\"#{ $fa-var-arrows-rotate }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-list-alt { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-list-alt:before { content: unquote("\"#{ $fa-var-rectangle-list }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-dedent:before { content: unquote("\"#{ $fa-var-outdent }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-video-camera:before { content: unquote("\"#{ $fa-var-video }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-picture-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-picture-o:before { content: unquote("\"#{ $fa-var-image }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-photo { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-photo:before { content: unquote("\"#{ $fa-var-image }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-image { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-image:before { content: unquote("\"#{ $fa-var-image }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-map-marker:before { content: unquote("\"#{ $fa-var-location-dot }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-pencil-square-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-pencil-square-o:before { content: unquote("\"#{ $fa-var-pen-to-square }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-edit { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-edit:before { content: unquote("\"#{ $fa-var-pen-to-square }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-share-square-o:before { content: unquote("\"#{ $fa-var-share-from-square }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-check-square-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-check-square-o:before { content: unquote("\"#{ $fa-var-square-check }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-arrows:before { content: unquote("\"#{ $fa-var-up-down-left-right }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-times-circle-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-times-circle-o:before { content: unquote("\"#{ $fa-var-circle-xmark }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-check-circle-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-check-circle-o:before { content: unquote("\"#{ $fa-var-circle-check }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-mail-forward:before { content: unquote("\"#{ $fa-var-share }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-expand:before { content: unquote("\"#{ $fa-var-up-right-and-down-left-from-center }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-compress:before { content: unquote("\"#{ $fa-var-down-left-and-up-right-to-center }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-eye { font-family: 'Font Awesome 6 Free'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-eye-slash { font-family: 'Font Awesome 6 Free'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-warning:before { content: unquote("\"#{ $fa-var-triangle-exclamation }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-calendar:before { content: unquote("\"#{ $fa-var-calendar-days }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-arrows-v:before { content: unquote("\"#{ $fa-var-up-down }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-arrows-h:before { content: unquote("\"#{ $fa-var-left-right }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-bar-chart:before { content: unquote("\"#{ $fa-var-chart-column }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-bar-chart-o:before { content: unquote("\"#{ $fa-var-chart-column }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-twitter-square { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-twitter-square:before { content: unquote("\"#{ $fa-var-square-twitter }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-facebook-square { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-facebook-square:before { content: unquote("\"#{ $fa-var-square-facebook }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-gears:before { content: unquote("\"#{ $fa-var-gears }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-thumbs-o-up { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-thumbs-o-up:before { content: unquote("\"#{ $fa-var-thumbs-up }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-thumbs-o-down { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-thumbs-o-down:before { content: unquote("\"#{ $fa-var-thumbs-down }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-heart-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-heart-o:before { content: unquote("\"#{ $fa-var-heart }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-sign-out:before { content: unquote("\"#{ $fa-var-right-from-bracket }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-linkedin-square { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-linkedin-square:before { content: unquote("\"#{ $fa-var-linkedin }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-thumb-tack:before { content: unquote("\"#{ $fa-var-thumbtack }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-external-link:before { content: unquote("\"#{ $fa-var-up-right-from-square }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-sign-in:before { content: unquote("\"#{ $fa-var-right-to-bracket }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-github-square { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-github-square:before { content: unquote("\"#{ $fa-var-square-github }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-lemon-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-lemon-o:before { content: unquote("\"#{ $fa-var-lemon }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-square-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-square-o:before { content: unquote("\"#{ $fa-var-square }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-bookmark-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-bookmark-o:before { content: unquote("\"#{ $fa-var-bookmark }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-twitter { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-facebook { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-facebook:before { content: unquote("\"#{ $fa-var-facebook-f }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-facebook-f { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-facebook-f:before { content: unquote("\"#{ $fa-var-facebook-f }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-github { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-credit-card { font-family: 'Font Awesome 6 Free'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-feed:before { content: unquote("\"#{ $fa-var-rss }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-hdd-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-hdd-o:before { content: unquote("\"#{ $fa-var-hard-drive }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-o-right { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-o-right:before { content: unquote("\"#{ $fa-var-hand-point-right }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-o-left { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-o-left:before { content: unquote("\"#{ $fa-var-hand-point-left }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-o-up { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-o-up:before { content: unquote("\"#{ $fa-var-hand-point-up }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-o-down { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-o-down:before { content: unquote("\"#{ $fa-var-hand-point-down }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-globe:before { content: unquote("\"#{ $fa-var-earth-americas }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-tasks:before { content: unquote("\"#{ $fa-var-bars-progress }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-arrows-alt:before { content: unquote("\"#{ $fa-var-maximize }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-group:before { content: unquote("\"#{ $fa-var-users }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-chain:before { content: unquote("\"#{ $fa-var-link }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-cut:before { content: unquote("\"#{ $fa-var-scissors }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-files-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-files-o:before { content: unquote("\"#{ $fa-var-copy }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-floppy-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-floppy-o:before { content: unquote("\"#{ $fa-var-floppy-disk }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-save { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-save:before { content: unquote("\"#{ $fa-var-floppy-disk }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-navicon:before { content: unquote("\"#{ $fa-var-bars }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-reorder:before { content: unquote("\"#{ $fa-var-bars }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-magic:before { content: unquote("\"#{ $fa-var-wand-magic-sparkles }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-pinterest { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-pinterest-square { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-pinterest-square:before { content: unquote("\"#{ $fa-var-square-pinterest }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-google-plus-square { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-google-plus-square:before { content: unquote("\"#{ $fa-var-square-google-plus }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-google-plus { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-google-plus:before { content: unquote("\"#{ $fa-var-google-plus-g }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-money:before { content: unquote("\"#{ $fa-var-money-bill-1 }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-unsorted:before { content: unquote("\"#{ $fa-var-sort }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-sort-desc:before { content: unquote("\"#{ $fa-var-sort-down }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-sort-asc:before { content: unquote("\"#{ $fa-var-sort-up }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-linkedin { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-linkedin:before { content: unquote("\"#{ $fa-var-linkedin-in }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-rotate-left:before { content: unquote("\"#{ $fa-var-arrow-rotate-left }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-legal:before { content: unquote("\"#{ $fa-var-gavel }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-tachometer:before { content: unquote("\"#{ $fa-var-gauge-high }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-dashboard:before { content: unquote("\"#{ $fa-var-gauge-high }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-comment-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-comment-o:before { content: unquote("\"#{ $fa-var-comment }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-comments-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-comments-o:before { content: unquote("\"#{ $fa-var-comments }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-flash:before { content: unquote("\"#{ $fa-var-bolt }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-clipboard:before { content: unquote("\"#{ $fa-var-paste }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-lightbulb-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-lightbulb-o:before { content: unquote("\"#{ $fa-var-lightbulb }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-exchange:before { content: unquote("\"#{ $fa-var-right-left }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-cloud-download:before { content: unquote("\"#{ $fa-var-cloud-arrow-down }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-cloud-upload:before { content: unquote("\"#{ $fa-var-cloud-arrow-up }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-bell-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-bell-o:before { content: unquote("\"#{ $fa-var-bell }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-cutlery:before { content: unquote("\"#{ $fa-var-utensils }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-file-text-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-file-text-o:before { content: unquote("\"#{ $fa-var-file-lines }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-building-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-building-o:before { content: unquote("\"#{ $fa-var-building }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-hospital-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-hospital-o:before { content: unquote("\"#{ $fa-var-hospital }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-tablet:before { content: unquote("\"#{ $fa-var-tablet-screen-button }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-mobile:before { content: unquote("\"#{ $fa-var-mobile-screen-button }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-mobile-phone:before { content: unquote("\"#{ $fa-var-mobile-screen-button }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-circle-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-circle-o:before { content: unquote("\"#{ $fa-var-circle }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-mail-reply:before { content: unquote("\"#{ $fa-var-reply }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-github-alt { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-folder-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-folder-o:before { content: unquote("\"#{ $fa-var-folder }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-folder-open-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-folder-open-o:before { content: unquote("\"#{ $fa-var-folder-open }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-smile-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-smile-o:before { content: unquote("\"#{ $fa-var-face-smile }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-frown-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-frown-o:before { content: unquote("\"#{ $fa-var-face-frown }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-meh-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-meh-o:before { content: unquote("\"#{ $fa-var-face-meh }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-keyboard-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-keyboard-o:before { content: unquote("\"#{ $fa-var-keyboard }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-flag-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-flag-o:before { content: unquote("\"#{ $fa-var-flag }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-mail-reply-all:before { content: unquote("\"#{ $fa-var-reply-all }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-star-half-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-star-half-o:before { content: unquote("\"#{ $fa-var-star-half-stroke }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-star-half-empty { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-star-half-empty:before { content: unquote("\"#{ $fa-var-star-half-stroke }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-star-half-full { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-star-half-full:before { content: unquote("\"#{ $fa-var-star-half-stroke }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-code-fork:before { content: unquote("\"#{ $fa-var-code-branch }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-chain-broken:before { content: unquote("\"#{ $fa-var-link-slash }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-unlink:before { content: unquote("\"#{ $fa-var-link-slash }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-calendar-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-calendar-o:before { content: unquote("\"#{ $fa-var-calendar }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-maxcdn { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-html5 { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-css3 { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-unlock-alt:before { content: unquote("\"#{ $fa-var-unlock }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-minus-square-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-minus-square-o:before { content: unquote("\"#{ $fa-var-square-minus }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-level-up:before { content: unquote("\"#{ $fa-var-turn-up }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-level-down:before { content: unquote("\"#{ $fa-var-turn-down }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-pencil-square:before { content: unquote("\"#{ $fa-var-square-pen }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-external-link-square:before { content: unquote("\"#{ $fa-var-square-up-right }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-compass { font-family: 'Font Awesome 6 Free'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-caret-square-o-down { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-caret-square-o-down:before { content: unquote("\"#{ $fa-var-square-caret-down }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-toggle-down { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-toggle-down:before { content: unquote("\"#{ $fa-var-square-caret-down }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-caret-square-o-up { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-caret-square-o-up:before { content: unquote("\"#{ $fa-var-square-caret-up }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-toggle-up { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-toggle-up:before { content: unquote("\"#{ $fa-var-square-caret-up }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-caret-square-o-right { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-caret-square-o-right:before { content: unquote("\"#{ $fa-var-square-caret-right }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-toggle-right { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-toggle-right:before { content: unquote("\"#{ $fa-var-square-caret-right }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-eur:before { content: unquote("\"#{ $fa-var-euro-sign }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-euro:before { content: unquote("\"#{ $fa-var-euro-sign }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-gbp:before { content: unquote("\"#{ $fa-var-sterling-sign }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-usd:before { content: unquote("\"#{ $fa-var-dollar-sign }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-dollar:before { content: unquote("\"#{ $fa-var-dollar-sign }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-inr:before { content: unquote("\"#{ $fa-var-indian-rupee-sign }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-rupee:before { content: unquote("\"#{ $fa-var-indian-rupee-sign }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-jpy:before { content: unquote("\"#{ $fa-var-yen-sign }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-cny:before { content: unquote("\"#{ $fa-var-yen-sign }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-rmb:before { content: unquote("\"#{ $fa-var-yen-sign }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-yen:before { content: unquote("\"#{ $fa-var-yen-sign }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-rub:before { content: unquote("\"#{ $fa-var-ruble-sign }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-ruble:before { content: unquote("\"#{ $fa-var-ruble-sign }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-rouble:before { content: unquote("\"#{ $fa-var-ruble-sign }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-krw:before { content: unquote("\"#{ $fa-var-won-sign }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-won:before { content: unquote("\"#{ $fa-var-won-sign }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-btc { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-bitcoin { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-bitcoin:before { content: unquote("\"#{ $fa-var-btc }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-file-text:before { content: unquote("\"#{ $fa-var-file-lines }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-sort-alpha-asc:before { content: unquote("\"#{ $fa-var-arrow-down-a-z }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-sort-alpha-desc:before { content: unquote("\"#{ $fa-var-arrow-down-z-a }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-sort-amount-asc:before { content: unquote("\"#{ $fa-var-arrow-down-short-wide }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-sort-amount-desc:before { content: unquote("\"#{ $fa-var-arrow-down-wide-short }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-sort-numeric-asc:before { content: unquote("\"#{ $fa-var-arrow-down-1-9 }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-sort-numeric-desc:before { content: unquote("\"#{ $fa-var-arrow-down-9-1 }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-youtube-square { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-youtube-square:before { content: unquote("\"#{ $fa-var-square-youtube }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-youtube { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-xing { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-xing-square { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-xing-square:before { content: unquote("\"#{ $fa-var-square-xing }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-youtube-play { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-youtube-play:before { content: unquote("\"#{ $fa-var-youtube }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-dropbox { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-stack-overflow { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-instagram { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-flickr { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-adn { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-bitbucket { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-bitbucket-square { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-bitbucket-square:before { content: unquote("\"#{ $fa-var-bitbucket }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-tumblr { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-tumblr-square { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-tumblr-square:before { content: unquote("\"#{ $fa-var-square-tumblr }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-long-arrow-down:before { content: unquote("\"#{ $fa-var-down-long }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-long-arrow-up:before { content: unquote("\"#{ $fa-var-up-long }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-long-arrow-left:before { content: unquote("\"#{ $fa-var-left-long }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-long-arrow-right:before { content: unquote("\"#{ $fa-var-right-long }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-apple { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-windows { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-android { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-linux { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-dribbble { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-skype { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-foursquare { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-trello { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-gratipay { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-gittip { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-gittip:before { content: unquote("\"#{ $fa-var-gratipay }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-sun-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-sun-o:before { content: unquote("\"#{ $fa-var-sun }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-moon-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-moon-o:before { content: unquote("\"#{ $fa-var-moon }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-vk { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-weibo { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-renren { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-pagelines { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-stack-exchange { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-arrow-circle-o-right { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-arrow-circle-o-right:before { content: unquote("\"#{ $fa-var-circle-right }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-arrow-circle-o-left { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-arrow-circle-o-left:before { content: unquote("\"#{ $fa-var-circle-left }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-caret-square-o-left { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-caret-square-o-left:before { content: unquote("\"#{ $fa-var-square-caret-left }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-toggle-left { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-toggle-left:before { content: unquote("\"#{ $fa-var-square-caret-left }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-dot-circle-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-dot-circle-o:before { content: unquote("\"#{ $fa-var-circle-dot }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-vimeo-square { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-vimeo-square:before { content: unquote("\"#{ $fa-var-square-vimeo }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-try:before { content: unquote("\"#{ $fa-var-turkish-lira-sign }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-turkish-lira:before { content: unquote("\"#{ $fa-var-turkish-lira-sign }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-plus-square-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-plus-square-o:before { content: unquote("\"#{ $fa-var-square-plus }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-slack { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-wordpress { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-openid { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-institution:before { content: unquote("\"#{ $fa-var-building-columns }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-bank:before { content: unquote("\"#{ $fa-var-building-columns }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-mortar-board:before { content: unquote("\"#{ $fa-var-graduation-cap }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-yahoo { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-google { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-reddit { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-reddit-square { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-reddit-square:before { content: unquote("\"#{ $fa-var-square-reddit }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-stumbleupon-circle { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-stumbleupon { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-delicious { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-digg { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-pied-piper-pp { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-pied-piper-alt { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-drupal { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-joomla { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-behance { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-behance-square { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-behance-square:before { content: unquote("\"#{ $fa-var-square-behance }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-steam { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-steam-square { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-steam-square:before { content: unquote("\"#{ $fa-var-square-steam }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-automobile:before { content: unquote("\"#{ $fa-var-car }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-cab:before { content: unquote("\"#{ $fa-var-taxi }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-spotify { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-deviantart { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-soundcloud { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-file-pdf-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-file-pdf-o:before { content: unquote("\"#{ $fa-var-file-pdf }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-file-word-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-file-word-o:before { content: unquote("\"#{ $fa-var-file-word }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-file-excel-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-file-excel-o:before { content: unquote("\"#{ $fa-var-file-excel }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-file-powerpoint-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-file-powerpoint-o:before { content: unquote("\"#{ $fa-var-file-powerpoint }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-file-image-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-file-image-o:before { content: unquote("\"#{ $fa-var-file-image }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-file-photo-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-file-photo-o:before { content: unquote("\"#{ $fa-var-file-image }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-file-picture-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-file-picture-o:before { content: unquote("\"#{ $fa-var-file-image }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-file-archive-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-file-archive-o:before { content: unquote("\"#{ $fa-var-file-zipper }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-file-zip-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-file-zip-o:before { content: unquote("\"#{ $fa-var-file-zipper }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-file-audio-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-file-audio-o:before { content: unquote("\"#{ $fa-var-file-audio }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-file-sound-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-file-sound-o:before { content: unquote("\"#{ $fa-var-file-audio }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-file-video-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-file-video-o:before { content: unquote("\"#{ $fa-var-file-video }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-file-movie-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-file-movie-o:before { content: unquote("\"#{ $fa-var-file-video }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-file-code-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-file-code-o:before { content: unquote("\"#{ $fa-var-file-code }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-vine { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-codepen { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-jsfiddle { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-life-bouy:before { content: unquote("\"#{ $fa-var-life-ring }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-life-buoy:before { content: unquote("\"#{ $fa-var-life-ring }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-life-saver:before { content: unquote("\"#{ $fa-var-life-ring }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-support:before { content: unquote("\"#{ $fa-var-life-ring }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-circle-o-notch:before { content: unquote("\"#{ $fa-var-circle-notch }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-rebel { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-ra { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-ra:before { content: unquote("\"#{ $fa-var-rebel }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-resistance { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-resistance:before { content: unquote("\"#{ $fa-var-rebel }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-empire { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-ge { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-ge:before { content: unquote("\"#{ $fa-var-empire }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-git-square { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-git-square:before { content: unquote("\"#{ $fa-var-square-git }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-git { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-hacker-news { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-y-combinator-square { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-y-combinator-square:before { content: unquote("\"#{ $fa-var-hacker-news }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-yc-square { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-yc-square:before { content: unquote("\"#{ $fa-var-hacker-news }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-tencent-weibo { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-qq { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-weixin { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-wechat { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-wechat:before { content: unquote("\"#{ $fa-var-weixin }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-send:before { content: unquote("\"#{ $fa-var-paper-plane }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-paper-plane-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-paper-plane-o:before { content: unquote("\"#{ $fa-var-paper-plane }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-send-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-send-o:before { content: unquote("\"#{ $fa-var-paper-plane }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-circle-thin { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-circle-thin:before { content: unquote("\"#{ $fa-var-circle }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-header:before { content: unquote("\"#{ $fa-var-heading }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-futbol-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-futbol-o:before { content: unquote("\"#{ $fa-var-futbol }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-soccer-ball-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-soccer-ball-o:before { content: unquote("\"#{ $fa-var-futbol }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-slideshare { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-twitch { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-yelp { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-newspaper-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-newspaper-o:before { content: unquote("\"#{ $fa-var-newspaper }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-paypal { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-google-wallet { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-cc-visa { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-cc-mastercard { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-cc-discover { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-cc-amex { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-cc-paypal { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-cc-stripe { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-bell-slash-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-bell-slash-o:before { content: unquote("\"#{ $fa-var-bell-slash }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-trash:before { content: unquote("\"#{ $fa-var-trash-can }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-copyright { font-family: 'Font Awesome 6 Free'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-eyedropper:before { content: unquote("\"#{ $fa-var-eye-dropper }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-area-chart:before { content: unquote("\"#{ $fa-var-chart-area }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-pie-chart:before { content: unquote("\"#{ $fa-var-chart-pie }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-line-chart:before { content: unquote("\"#{ $fa-var-chart-line }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-lastfm { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-lastfm-square { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-lastfm-square:before { content: unquote("\"#{ $fa-var-square-lastfm }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-ioxhost { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-angellist { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-cc { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-cc:before { content: unquote("\"#{ $fa-var-closed-captioning }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-ils:before { content: unquote("\"#{ $fa-var-shekel-sign }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-shekel:before { content: unquote("\"#{ $fa-var-shekel-sign }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-sheqel:before { content: unquote("\"#{ $fa-var-shekel-sign }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-buysellads { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-connectdevelop { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-dashcube { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-forumbee { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-leanpub { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-sellsy { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-shirtsinbulk { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-simplybuilt { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-skyatlas { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-diamond { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-diamond:before { content: unquote("\"#{ $fa-var-gem }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-transgender:before { content: unquote("\"#{ $fa-var-mars-and-venus }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-intersex:before { content: unquote("\"#{ $fa-var-mars-and-venus }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-transgender-alt:before { content: unquote("\"#{ $fa-var-transgender }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-facebook-official { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-facebook-official:before { content: unquote("\"#{ $fa-var-facebook }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-pinterest-p { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-whatsapp { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-hotel:before { content: unquote("\"#{ $fa-var-bed }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-viacoin { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-medium { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-y-combinator { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-yc { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-yc:before { content: unquote("\"#{ $fa-var-y-combinator }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-optin-monster { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-opencart { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-expeditedssl { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-battery-4:before { content: unquote("\"#{ $fa-var-battery-full }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-battery:before { content: unquote("\"#{ $fa-var-battery-full }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-battery-3:before { content: unquote("\"#{ $fa-var-battery-three-quarters }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-battery-2:before { content: unquote("\"#{ $fa-var-battery-half }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-battery-1:before { content: unquote("\"#{ $fa-var-battery-quarter }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-battery-0:before { content: unquote("\"#{ $fa-var-battery-empty }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-object-group { font-family: 'Font Awesome 6 Free'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-object-ungroup { font-family: 'Font Awesome 6 Free'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-sticky-note-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-sticky-note-o:before { content: unquote("\"#{ $fa-var-note-sticky }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-cc-jcb { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-cc-diners-club { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-clone { font-family: 'Font Awesome 6 Free'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-hourglass-o:before { content: unquote("\"#{ $fa-var-hourglass }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-hourglass-1:before { content: unquote("\"#{ $fa-var-hourglass-start }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-hourglass-2:before { content: unquote("\"#{ $fa-var-hourglass-half }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-hourglass-3:before { content: unquote("\"#{ $fa-var-hourglass-end }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-rock-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-rock-o:before { content: unquote("\"#{ $fa-var-hand-back-fist }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-grab-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-grab-o:before { content: unquote("\"#{ $fa-var-hand-back-fist }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-paper-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-paper-o:before { content: unquote("\"#{ $fa-var-hand }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-stop-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-stop-o:before { content: unquote("\"#{ $fa-var-hand }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-scissors-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-scissors-o:before { content: unquote("\"#{ $fa-var-hand-scissors }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-lizard-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-lizard-o:before { content: unquote("\"#{ $fa-var-hand-lizard }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-spock-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-spock-o:before { content: unquote("\"#{ $fa-var-hand-spock }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-pointer-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-pointer-o:before { content: unquote("\"#{ $fa-var-hand-pointer }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-peace-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-hand-peace-o:before { content: unquote("\"#{ $fa-var-hand-peace }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-registered { font-family: 'Font Awesome 6 Free'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-creative-commons { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-gg { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-gg-circle { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-odnoklassniki { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-odnoklassniki-square { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-odnoklassniki-square:before { content: unquote("\"#{ $fa-var-square-odnoklassniki }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-get-pocket { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-wikipedia-w { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-safari { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-chrome { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-firefox { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-opera { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-internet-explorer { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-television:before { content: unquote("\"#{ $fa-var-tv }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-contao { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-500px { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-amazon { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-calendar-plus-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-calendar-plus-o:before { content: unquote("\"#{ $fa-var-calendar-plus }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-calendar-minus-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-calendar-minus-o:before { content: unquote("\"#{ $fa-var-calendar-minus }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-calendar-times-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-calendar-times-o:before { content: unquote("\"#{ $fa-var-calendar-xmark }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-calendar-check-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-calendar-check-o:before { content: unquote("\"#{ $fa-var-calendar-check }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-map-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-map-o:before { content: unquote("\"#{ $fa-var-map }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-commenting:before { content: unquote("\"#{ $fa-var-comment-dots }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-commenting-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-commenting-o:before { content: unquote("\"#{ $fa-var-comment-dots }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-houzz { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-vimeo { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-vimeo:before { content: unquote("\"#{ $fa-var-vimeo-v }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-black-tie { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-fonticons { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-reddit-alien { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-edge { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-credit-card-alt:before { content: unquote("\"#{ $fa-var-credit-card }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-codiepie { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-modx { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-fort-awesome { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-usb { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-product-hunt { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-mixcloud { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-scribd { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-pause-circle-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-pause-circle-o:before { content: unquote("\"#{ $fa-var-circle-pause }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-stop-circle-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-stop-circle-o:before { content: unquote("\"#{ $fa-var-circle-stop }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-bluetooth { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-bluetooth-b { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-gitlab { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-wpbeginner { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-wpforms { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-envira { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-wheelchair-alt { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-wheelchair-alt:before { content: unquote("\"#{ $fa-var-accessible-icon }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-question-circle-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-question-circle-o:before { content: unquote("\"#{ $fa-var-circle-question }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-volume-control-phone:before { content: unquote("\"#{ $fa-var-phone-volume }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-asl-interpreting:before { content: unquote("\"#{ $fa-var-hands-asl-interpreting }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-deafness:before { content: unquote("\"#{ $fa-var-ear-deaf }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-hard-of-hearing:before { content: unquote("\"#{ $fa-var-ear-deaf }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-glide { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-glide-g { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-signing:before { content: unquote("\"#{ $fa-var-hands }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-viadeo { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-viadeo-square { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-viadeo-square:before { content: unquote("\"#{ $fa-var-square-viadeo }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-snapchat { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-snapchat-ghost { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-snapchat-ghost:before { content: unquote("\"#{ $fa-var-snapchat }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-snapchat-square { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-snapchat-square:before { content: unquote("\"#{ $fa-var-square-snapchat }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-pied-piper { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-first-order { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-yoast { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-themeisle { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-google-plus-official { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-google-plus-official:before { content: unquote("\"#{ $fa-var-google-plus }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-google-plus-circle { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-google-plus-circle:before { content: unquote("\"#{ $fa-var-google-plus }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-font-awesome { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-fa { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-fa:before { content: unquote("\"#{ $fa-var-font-awesome }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-handshake-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-handshake-o:before { content: unquote("\"#{ $fa-var-handshake }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-envelope-open-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-envelope-open-o:before { content: unquote("\"#{ $fa-var-envelope-open }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-linode { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-address-book-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-address-book-o:before { content: unquote("\"#{ $fa-var-address-book }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-vcard:before { content: unquote("\"#{ $fa-var-address-card }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-address-card-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-address-card-o:before { content: unquote("\"#{ $fa-var-address-card }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-vcard-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-vcard-o:before { content: unquote("\"#{ $fa-var-address-card }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-user-circle-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-user-circle-o:before { content: unquote("\"#{ $fa-var-circle-user }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-user-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-user-o:before { content: unquote("\"#{ $fa-var-user }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-id-badge { font-family: 'Font Awesome 6 Free'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-drivers-license:before { content: unquote("\"#{ $fa-var-id-card }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-id-card-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-id-card-o:before { content: unquote("\"#{ $fa-var-id-card }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-drivers-license-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-drivers-license-o:before { content: unquote("\"#{ $fa-var-id-card }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-quora { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-free-code-camp { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-telegram { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-thermometer-4:before { content: unquote("\"#{ $fa-var-temperature-full }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-thermometer:before { content: unquote("\"#{ $fa-var-temperature-full }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-thermometer-3:before { content: unquote("\"#{ $fa-var-temperature-three-quarters }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-thermometer-2:before { content: unquote("\"#{ $fa-var-temperature-half }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-thermometer-1:before { content: unquote("\"#{ $fa-var-temperature-quarter }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-thermometer-0:before { content: unquote("\"#{ $fa-var-temperature-empty }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-bathtub:before { content: unquote("\"#{ $fa-var-bath }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-s15:before { content: unquote("\"#{ $fa-var-bath }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-window-maximize { font-family: 'Font Awesome 6 Free'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-window-restore { font-family: 'Font Awesome 6 Free'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-times-rectangle:before { content: unquote("\"#{ $fa-var-rectangle-xmark }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-window-close-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-window-close-o:before { content: unquote("\"#{ $fa-var-rectangle-xmark }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-times-rectangle-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-times-rectangle-o:before { content: unquote("\"#{ $fa-var-rectangle-xmark }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-bandcamp { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-grav { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-etsy { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-imdb { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-ravelry { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-eercast { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-eercast:before { content: unquote("\"#{ $fa-var-sellcast }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-snowflake-o { font-family: 'Font Awesome 6 Free'; font-weight: 400; } .#{$fa-css-prefix}.#{$fa-css-prefix}-snowflake-o:before { content: unquote("\"#{ $fa-var-snowflake }\""); } - .#{$fa-css-prefix}.#{$fa-css-prefix}-superpowers { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-wpexplorer { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - .#{$fa-css-prefix}.#{$fa-css-prefix}-meetup { font-family: 'Font Awesome 6 Brands'; font-weight: 400; } - diff --git a/theme/boost/scss/fontawesome/_variables.scss b/theme/boost/scss/fontawesome/_variables.scss index cc9c00ac343de..77885b4d47a77 100644 --- a/theme/boost/scss/fontawesome/_variables.scss +++ b/theme/boost/scss/fontawesome/_variables.scss @@ -806,6 +806,7 @@ $fa-var-glass-martini: \f000; $fa-var-couch: \f4b8; $fa-var-cedi-sign: \e0df; $fa-var-italic: \f033; +$fa-var-table-cells-column-lock: \e678; $fa-var-church: \f51d; $fa-var-comments-dollar: \f653; $fa-var-democrat: \f747; @@ -1530,6 +1531,7 @@ $fa-var-assistive-listening-systems: \f2a2; $fa-var-tree-city: \e587; $fa-var-play: \f04b; $fa-var-font: \f031; +$fa-var-table-cells-row-lock: \e67a; $fa-var-rupiah-sign: \e23d; $fa-var-magnifying-glass: \f002; $fa-var-search: \f002; @@ -2032,6 +2034,7 @@ $fa-var-creative-commons-pd-alt: \f4ed; $fa-var-centercode: \f380; $fa-var-glide-g: \f2a6; $fa-var-drupal: \f1a9; +$fa-var-jxl: \e67b; $fa-var-hire-a-helper: \f3b0; $fa-var-creative-commons-by: \f4e7; $fa-var-unity: \e049; @@ -2133,6 +2136,7 @@ $fa-var-cc-mastercard: \f1f1; $fa-var-itunes-note: \f3b5; $fa-var-golang: \e40f; $fa-var-kickstarter: \f3bb; +$fa-var-square-kickstarter: \f3bb; $fa-var-grav: \f2d6; $fa-var-weibo: \f18a; $fa-var-uncharted: \e084; @@ -2277,6 +2281,7 @@ $fa-var-brave-reverse: \e63d; $fa-var-facebook-f: \f39e; $fa-var-square-google-plus: \f0d4; $fa-var-google-plus-square: \f0d4; +$fa-var-web-awesome: \e682; $fa-var-mandalorian: \f50f; $fa-var-first-order-alt: \f50a; $fa-var-osi: \f41a; @@ -2331,6 +2336,7 @@ $fa-var-palfed: \f3d8; $fa-var-superpowers: \f2dd; $fa-var-resolving: \f3e7; $fa-var-xbox: \f412; +$fa-var-square-web-awesome-stroke: \e684; $fa-var-searchengin: \f3eb; $fa-var-tiktok: \e07b; $fa-var-square-facebook: \f082; @@ -2367,6 +2373,7 @@ $fa-var-earlybirds: \f39a; $fa-var-trade-federation: \f513; $fa-var-autoprefixer: \f41c; $fa-var-whatsapp: \f232; +$fa-var-square-upwork: \e67c; $fa-var-slideshare: \f1e7; $fa-var-google-play: \f3ab; $fa-var-viadeo: \f2a9; @@ -2393,6 +2400,7 @@ $fa-var-yandex: \f413; $fa-var-readme: \f4d5; $fa-var-html5: \f13b; $fa-var-sellsy: \f213; +$fa-var-square-web-awesome: \e683; $fa-var-sass: \f41e; $fa-var-wirsindhandwerk: \e2d0; $fa-var-wsh: \e2d0; @@ -2405,6 +2413,7 @@ $fa-var-pinterest-p: \f231; $fa-var-apper: \f371; $fa-var-fort-awesome: \f286; $fa-var-waze: \f83f; +$fa-var-bluesky: \e671; $fa-var-cc-jcb: \f24b; $fa-var-snapchat: \f2ab; $fa-var-snapchat-ghost: \f2ab; @@ -3277,6 +3286,7 @@ $fa-icons: ( "couch": $fa-var-couch, "cedi-sign": $fa-var-cedi-sign, "italic": $fa-var-italic, + "table-cells-column-lock": $fa-var-table-cells-column-lock, "church": $fa-var-church, "comments-dollar": $fa-var-comments-dollar, "democrat": $fa-var-democrat, @@ -4001,6 +4011,7 @@ $fa-icons: ( "tree-city": $fa-var-tree-city, "play": $fa-var-play, "font": $fa-var-font, + "table-cells-row-lock": $fa-var-table-cells-row-lock, "rupiah-sign": $fa-var-rupiah-sign, "magnifying-glass": $fa-var-magnifying-glass, "search": $fa-var-search, @@ -4505,6 +4516,7 @@ $fa-brand-icons: ( "centercode": $fa-var-centercode, "glide-g": $fa-var-glide-g, "drupal": $fa-var-drupal, + "jxl": $fa-var-jxl, "hire-a-helper": $fa-var-hire-a-helper, "creative-commons-by": $fa-var-creative-commons-by, "unity": $fa-var-unity, @@ -4606,6 +4618,7 @@ $fa-brand-icons: ( "itunes-note": $fa-var-itunes-note, "golang": $fa-var-golang, "kickstarter": $fa-var-kickstarter, + "square-kickstarter": $fa-var-square-kickstarter, "grav": $fa-var-grav, "weibo": $fa-var-weibo, "uncharted": $fa-var-uncharted, @@ -4750,6 +4763,7 @@ $fa-brand-icons: ( "facebook-f": $fa-var-facebook-f, "square-google-plus": $fa-var-square-google-plus, "google-plus-square": $fa-var-google-plus-square, + "web-awesome": $fa-var-web-awesome, "mandalorian": $fa-var-mandalorian, "first-order-alt": $fa-var-first-order-alt, "osi": $fa-var-osi, @@ -4804,6 +4818,7 @@ $fa-brand-icons: ( "superpowers": $fa-var-superpowers, "resolving": $fa-var-resolving, "xbox": $fa-var-xbox, + "square-web-awesome-stroke": $fa-var-square-web-awesome-stroke, "searchengin": $fa-var-searchengin, "tiktok": $fa-var-tiktok, "square-facebook": $fa-var-square-facebook, @@ -4840,6 +4855,7 @@ $fa-brand-icons: ( "trade-federation": $fa-var-trade-federation, "autoprefixer": $fa-var-autoprefixer, "whatsapp": $fa-var-whatsapp, + "square-upwork": $fa-var-square-upwork, "slideshare": $fa-var-slideshare, "google-play": $fa-var-google-play, "viadeo": $fa-var-viadeo, @@ -4866,6 +4882,7 @@ $fa-brand-icons: ( "readme": $fa-var-readme, "html5": $fa-var-html5, "sellsy": $fa-var-sellsy, + "square-web-awesome": $fa-var-square-web-awesome, "sass": $fa-var-sass, "wirsindhandwerk": $fa-var-wirsindhandwerk, "wsh": $fa-var-wsh, @@ -4878,6 +4895,7 @@ $fa-brand-icons: ( "apper": $fa-var-apper, "fort-awesome": $fa-var-fort-awesome, "waze": $fa-var-waze, + "bluesky": $fa-var-bluesky, "cc-jcb": $fa-var-cc-jcb, "snapchat": $fa-var-snapchat, "snapchat-ghost": $fa-var-snapchat-ghost, diff --git a/theme/boost/scss/fontawesome/brands.scss b/theme/boost/scss/fontawesome/brands.scss index 9677d5c23b954..050df5e8c2baa 100644 --- a/theme/boost/scss/fontawesome/brands.scss +++ b/theme/boost/scss/fontawesome/brands.scss @@ -1,7 +1,7 @@ /*! - * Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com + * Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - * Copyright 2023 Fonticons, Inc. + * Copyright 2024 Fonticons, Inc. */ @import 'functions'; @import 'variables'; @@ -16,8 +16,8 @@ font-style: normal; font-weight: 400; font-display: $fa-font-display; - src: url('[[font:core|fa-brands-400.woff2]]') format('woff2'), - url('[[font:core|fa-brands-400.ttf]]') format('truetype'); + src: url('#{$fa-font-path}/fa-brands-400.woff2') format('woff2'), + url('#{$fa-font-path}/fa-brands-400.ttf') format('truetype'); } .fab, diff --git a/theme/boost/scss/fontawesome/fontawesome.scss b/theme/boost/scss/fontawesome/fontawesome.scss index 61541e368d83d..0da7e17187050 100644 --- a/theme/boost/scss/fontawesome/fontawesome.scss +++ b/theme/boost/scss/fontawesome/fontawesome.scss @@ -1,7 +1,7 @@ /*! - * Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com + * Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - * Copyright 2023 Fonticons, Inc. + * Copyright 2024 Fonticons, Inc. */ // Font Awesome core compile (Web Fonts-based) // ------------------------- diff --git a/theme/boost/scss/fontawesome/regular.scss b/theme/boost/scss/fontawesome/regular.scss index 89e9338a33691..0d54996f1e7e8 100644 --- a/theme/boost/scss/fontawesome/regular.scss +++ b/theme/boost/scss/fontawesome/regular.scss @@ -1,7 +1,7 @@ /*! - * Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com + * Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - * Copyright 2023 Fonticons, Inc. + * Copyright 2024 Fonticons, Inc. */ @import 'functions'; @import 'variables'; @@ -16,8 +16,8 @@ font-style: normal; font-weight: 400; font-display: $fa-font-display; - src: url('[[font:core|fa-regular-400.woff2]]') format('woff2'), - url('[[font:core|fa-regular-400.ttf]]') format('truetype'); + src: url('#{$fa-font-path}/fa-regular-400.woff2') format('woff2'), + url('#{$fa-font-path}/fa-regular-400.ttf') format('truetype'); } .far, diff --git a/theme/boost/scss/fontawesome/solid.scss b/theme/boost/scss/fontawesome/solid.scss index 6d623325c49fb..ee66b36d6b2d1 100644 --- a/theme/boost/scss/fontawesome/solid.scss +++ b/theme/boost/scss/fontawesome/solid.scss @@ -1,7 +1,7 @@ /*! - * Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com + * Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - * Copyright 2023 Fonticons, Inc. + * Copyright 2024 Fonticons, Inc. */ @import 'functions'; @import 'variables'; @@ -16,8 +16,8 @@ font-style: normal; font-weight: 900; font-display: $fa-font-display; - src: url('[[font:core|fa-solid-900.woff2]]') format('woff2'), - url('[[font:core|fa-solid-900.ttf]]') format('truetype'); + src: url('#{$fa-font-path}/fa-solid-900.woff2') format('woff2'), + url('#{$fa-font-path}/fa-solid-900.ttf') format('truetype'); } .fas, diff --git a/theme/boost/scss/fontawesome/v4-shims.scss b/theme/boost/scss/fontawesome/v4-shims.scss index 263b16ef70cad..b40c6c54566f2 100644 --- a/theme/boost/scss/fontawesome/v4-shims.scss +++ b/theme/boost/scss/fontawesome/v4-shims.scss @@ -1,7 +1,7 @@ /*! - * Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com + * Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - * Copyright 2023 Fonticons, Inc. + * Copyright 2024 Fonticons, Inc. */ // V4 shims compile (Web Fonts-based) // ------------------------- From 445de6070f2c95d0b8b5a16a675c1aa5295bf6a0 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Thu, 27 Jun 2024 16:22:36 +0200 Subject: [PATCH 030/178] MDL-81661 theme: Set Moodle files after Font Awesome upgrade --- .../content/moodle/components/moodle-icons.md | 2 +- .../hugo/site/data/fontawesomeicons.json | 2 +- lib/thirdpartylibs.xml | 2 +- theme/boost/scss/fontawesome/brands.scss | 4 +- theme/boost/scss/fontawesome/regular.scss | 4 +- theme/boost/scss/fontawesome/solid.scss | 4 +- theme/boost/style/moodle.css | 58 +++++++++++++++---- theme/boost/thirdpartylibs.xml | 2 +- theme/classic/style/moodle.css | 58 +++++++++++++++---- 9 files changed, 104 insertions(+), 32 deletions(-) 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/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/lib/thirdpartylibs.xml b/lib/thirdpartylibs.xml index ef34dc4af5055..51f25ad575ddf 100644 --- a/lib/thirdpartylibs.xml +++ b/lib/thirdpartylibs.xml @@ -446,7 +446,7 @@ All rights reserved. fonts Font Awesome - http://fontawesome.com The Font Awesome font. Font Awesome is the Internet's icon library and toolkit, used by millions of designers, developers, and content creators. - 6.5.1 + 6.5.2 SIL OFL 1.1 https://github.com/FortAwesome/Font-Awesome diff --git a/theme/boost/scss/fontawesome/brands.scss b/theme/boost/scss/fontawesome/brands.scss index 050df5e8c2baa..f9fdd887c43d0 100644 --- a/theme/boost/scss/fontawesome/brands.scss +++ b/theme/boost/scss/fontawesome/brands.scss @@ -16,8 +16,8 @@ font-style: normal; font-weight: 400; font-display: $fa-font-display; - src: url('#{$fa-font-path}/fa-brands-400.woff2') format('woff2'), - url('#{$fa-font-path}/fa-brands-400.ttf') format('truetype'); + src: url('[[font:core|fa-brands-400.woff2]]') format('woff2'), + url('[[font:core|fa-brands-400.ttf]]') format('truetype'); } .fab, diff --git a/theme/boost/scss/fontawesome/regular.scss b/theme/boost/scss/fontawesome/regular.scss index 0d54996f1e7e8..125d9aa6367ab 100644 --- a/theme/boost/scss/fontawesome/regular.scss +++ b/theme/boost/scss/fontawesome/regular.scss @@ -16,8 +16,8 @@ font-style: normal; font-weight: 400; font-display: $fa-font-display; - src: url('#{$fa-font-path}/fa-regular-400.woff2') format('woff2'), - url('#{$fa-font-path}/fa-regular-400.ttf') format('truetype'); + src: url('[[font:core|fa-regular-400.woff2]]') format('woff2'), + url('[[font:core|fa-regular-400.ttf]]') format('truetype'); } .far, diff --git a/theme/boost/scss/fontawesome/solid.scss b/theme/boost/scss/fontawesome/solid.scss index ee66b36d6b2d1..9a6366e2be7c8 100644 --- a/theme/boost/scss/fontawesome/solid.scss +++ b/theme/boost/scss/fontawesome/solid.scss @@ -16,8 +16,8 @@ font-style: normal; font-weight: 900; font-display: $fa-font-display; - src: url('#{$fa-font-path}/fa-solid-900.woff2') format('woff2'), - url('#{$fa-font-path}/fa-solid-900.ttf') format('truetype'); + src: url('[[font:core|fa-solid-900.woff2]]') format('woff2'), + url('[[font:core|fa-solid-900.ttf]]') format('truetype'); } .fas, diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index f0bc8c06201e1..0a135b193fa93 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -1,8 +1,8 @@ @charset "UTF-8"; /*! - * Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com + * Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - * Copyright 2023 Fonticons, Inc. + * Copyright 2024 Fonticons, Inc. */ :root, :host { --fa-style-family-brands: "Font Awesome 6 Brands"; @@ -153,6 +153,10 @@ content: "\f1a9"; } +.fa-jxl:before { + content: "\e67b"; +} + .fa-hire-a-helper:before { content: "\f3b0"; } @@ -557,6 +561,10 @@ content: "\f3bb"; } +.fa-square-kickstarter:before { + content: "\f3bb"; +} + .fa-grav:before { content: "\f2d6"; } @@ -1133,6 +1141,10 @@ content: "\f0d4"; } +.fa-web-awesome:before { + content: "\e682"; +} + .fa-mandalorian:before { content: "\f50f"; } @@ -1349,6 +1361,10 @@ content: "\f412"; } +.fa-square-web-awesome-stroke:before { + content: "\e684"; +} + .fa-searchengin:before { content: "\f3eb"; } @@ -1493,6 +1509,10 @@ content: "\f232"; } +.fa-square-upwork:before { + content: "\e67c"; +} + .fa-slideshare:before { content: "\f1e7"; } @@ -1597,6 +1617,10 @@ content: "\f213"; } +.fa-square-web-awesome:before { + content: "\e683"; +} + .fa-sass:before { content: "\f41e"; } @@ -1645,6 +1669,10 @@ content: "\f83f"; } +.fa-bluesky:before { + content: "\e671"; +} + .fa-cc-jcb:before { content: "\f24b"; } @@ -2094,9 +2122,9 @@ } /*! - * Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com + * Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - * Copyright 2023 Fonticons, Inc. + * Copyright 2024 Fonticons, Inc. */ :root, :host { --fa-style-family-classic: "Font Awesome 6 Free"; @@ -2117,9 +2145,9 @@ } /*! - * Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com + * Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - * Copyright 2023 Fonticons, Inc. + * Copyright 2024 Fonticons, Inc. */ :root, :host { --fa-style-family-classic: "Font Awesome 6 Free"; @@ -2163,9 +2191,9 @@ } /*! - * Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com + * Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - * Copyright 2023 Fonticons, Inc. + * Copyright 2024 Fonticons, Inc. */ .fa.fa-glass:before { content: "\f000"; @@ -4982,9 +5010,9 @@ } /*! - * Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com + * Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - * Copyright 2023 Fonticons, Inc. + * Copyright 2024 Fonticons, Inc. */ .fa { font-family: var(--fa-style-family, "Font Awesome 6 Free"); @@ -5395,7 +5423,7 @@ } .fa-rotate-by { - transform: rotate(var(--fa-rotate-angle, none)); + transform: rotate(var(--fa-rotate-angle, 0)); } .fa-stack { @@ -8462,6 +8490,10 @@ readers do not read off random characters that represent icons */ content: "\f033"; } +.fa-table-cells-column-lock::before { + content: "\e678"; +} + .fa-church::before { content: "\f51d"; } @@ -11358,6 +11390,10 @@ readers do not read off random characters that represent icons */ content: "\f031"; } +.fa-table-cells-row-lock::before { + content: "\e67a"; +} + .fa-rupiah-sign::before { content: "\e23d"; } diff --git a/theme/boost/thirdpartylibs.xml b/theme/boost/thirdpartylibs.xml index 031c8d39f2158..48fa7d65fe4cd 100644 --- a/theme/boost/thirdpartylibs.xml +++ b/theme/boost/thirdpartylibs.xml @@ -200,7 +200,7 @@ scss/fontawesome Font Awesome - http://fontawesome.com Font Awesome CSS, LESS, and Sass files. Font Awesome is the Internet's icon library and toolkit, used by millions of designers, developers, and content creators. - 6.5.1 + 6.5.2 (MIT) https://github.com/FortAwesome/Font-Awesome diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 2551635fa507d..b90f09f9db232 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -1,8 +1,8 @@ @charset "UTF-8"; /*! - * Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com + * Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - * Copyright 2023 Fonticons, Inc. + * Copyright 2024 Fonticons, Inc. */ :root, :host { --fa-style-family-brands: "Font Awesome 6 Brands"; @@ -153,6 +153,10 @@ content: "\f1a9"; } +.fa-jxl:before { + content: "\e67b"; +} + .fa-hire-a-helper:before { content: "\f3b0"; } @@ -557,6 +561,10 @@ content: "\f3bb"; } +.fa-square-kickstarter:before { + content: "\f3bb"; +} + .fa-grav:before { content: "\f2d6"; } @@ -1133,6 +1141,10 @@ content: "\f0d4"; } +.fa-web-awesome:before { + content: "\e682"; +} + .fa-mandalorian:before { content: "\f50f"; } @@ -1349,6 +1361,10 @@ content: "\f412"; } +.fa-square-web-awesome-stroke:before { + content: "\e684"; +} + .fa-searchengin:before { content: "\f3eb"; } @@ -1493,6 +1509,10 @@ content: "\f232"; } +.fa-square-upwork:before { + content: "\e67c"; +} + .fa-slideshare:before { content: "\f1e7"; } @@ -1597,6 +1617,10 @@ content: "\f213"; } +.fa-square-web-awesome:before { + content: "\e683"; +} + .fa-sass:before { content: "\f41e"; } @@ -1645,6 +1669,10 @@ content: "\f83f"; } +.fa-bluesky:before { + content: "\e671"; +} + .fa-cc-jcb:before { content: "\f24b"; } @@ -2094,9 +2122,9 @@ } /*! - * Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com + * Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - * Copyright 2023 Fonticons, Inc. + * Copyright 2024 Fonticons, Inc. */ :root, :host { --fa-style-family-classic: "Font Awesome 6 Free"; @@ -2117,9 +2145,9 @@ } /*! - * Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com + * Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - * Copyright 2023 Fonticons, Inc. + * Copyright 2024 Fonticons, Inc. */ :root, :host { --fa-style-family-classic: "Font Awesome 6 Free"; @@ -2163,9 +2191,9 @@ } /*! - * Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com + * Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - * Copyright 2023 Fonticons, Inc. + * Copyright 2024 Fonticons, Inc. */ .fa.fa-glass:before { content: "\f000"; @@ -4982,9 +5010,9 @@ } /*! - * Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com + * Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - * Copyright 2023 Fonticons, Inc. + * Copyright 2024 Fonticons, Inc. */ .fa { font-family: var(--fa-style-family, "Font Awesome 6 Free"); @@ -5395,7 +5423,7 @@ } .fa-rotate-by { - transform: rotate(var(--fa-rotate-angle, none)); + transform: rotate(var(--fa-rotate-angle, 0)); } .fa-stack { @@ -8462,6 +8490,10 @@ readers do not read off random characters that represent icons */ content: "\f033"; } +.fa-table-cells-column-lock::before { + content: "\e678"; +} + .fa-church::before { content: "\f51d"; } @@ -11358,6 +11390,10 @@ readers do not read off random characters that represent icons */ content: "\f031"; } +.fa-table-cells-row-lock::before { + content: "\e67a"; +} + .fa-rupiah-sign::before { content: "\e23d"; } From 0f21f6e374f1b397cf2b88d2215f9c7966e98323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ure=C3=B1a?= Date: Thu, 4 Jul 2024 15:49:42 +0200 Subject: [PATCH 031/178] MDL-82234 tool_mobile: Return display default login form --- admin/tool/mobile/classes/api.php | 1 + admin/tool/mobile/classes/external.php | 1 + admin/tool/mobile/tests/externallib_test.php | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/admin/tool/mobile/classes/api.php b/admin/tool/mobile/classes/api.php index cadd01a248c21..a8ef1ca6374b0 100644 --- a/admin/tool/mobile/classes/api.php +++ b/admin/tool/mobile/classes/api.php @@ -213,6 +213,7 @@ public static function get_public_config() { 'tool_mobile_qrcodetype' => clean_param(get_config('tool_mobile', 'qrcodetype'), PARAM_INT), 'supportpage' => $sitesupportavailable ? clean_param($CFG->supportpage, PARAM_URL) : '', 'supportavailability' => clean_param($CFG->supportavailability, PARAM_INT), + 'showloginform' => (int) get_config('core', 'showloginform'), ); $typeoflogin = get_config('tool_mobile', 'typeoflogin'); diff --git a/admin/tool/mobile/classes/external.php b/admin/tool/mobile/classes/external.php index 34eba17682fcb..401fb33f079c2 100644 --- a/admin/tool/mobile/classes/external.php +++ b/admin/tool/mobile/classes/external.php @@ -191,6 +191,7 @@ public static function get_public_config_returns() { 'tool_mobile_setuplink' => new external_value(PARAM_URL, 'App download page.', VALUE_OPTIONAL), 'tool_mobile_qrcodetype' => new external_value(PARAM_INT, 'QR login configuration.', VALUE_OPTIONAL), 'warnings' => new external_warnings(), + 'showloginform' => new external_value(PARAM_INT, 'Display default login form.'), ) ); } diff --git a/admin/tool/mobile/tests/externallib_test.php b/admin/tool/mobile/tests/externallib_test.php index a56bd376b03fe..5c739c8a4a7d2 100644 --- a/admin/tool/mobile/tests/externallib_test.php +++ b/admin/tool/mobile/tests/externallib_test.php @@ -102,7 +102,8 @@ public function test_get_public_config(): void { 'tool_mobile_qrcodetype' => get_config('tool_mobile', 'qrcodetype'), 'supportpage' => $CFG->supportpage, 'supportavailability' => $CFG->supportavailability, - 'warnings' => array() + 'warnings' => [], + 'showloginform' => (int) get_config('core', 'showloginform'), ); $this->assertEquals($expected, $result); From a76ec9fdcb695f3cc45463bce225d4d7c88b0f00 Mon Sep 17 00:00:00 2001 From: ferranrecio Date: Tue, 25 Jun 2024 17:43:40 +0200 Subject: [PATCH 032/178] MDL-81765 mod_subsection: add plugin to core --- lib/plugins.json | 1 + .../backup_subsection_activity_task.class.php | 73 ++++++ .../moodle2/backup_subsection_stepslib.php | 48 ++++ ...restore_subsection_activity_task.class.php | 94 ++++++++ .../moodle2/restore_subsection_stepslib.php | 73 ++++++ .../classes/courseformat/sectiondelegate.php | 38 ++++ .../course_module_instance_list_viewed.php | 29 +++ .../classes/event/course_module_viewed.php | 41 ++++ .../after_cm_name_edited_handler.php | 48 ++++ mod/subsection/classes/manager.php | 183 +++++++++++++++ mod/subsection/classes/privacy/provider.php | 37 ++++ mod/subsection/db/access.php | 41 ++++ mod/subsection/db/hooks.php | 33 +++ mod/subsection/db/install.php | 32 +++ mod/subsection/db/install.xml | 23 ++ mod/subsection/db/uninstall.php | 31 +++ mod/subsection/db/upgrade.php | 43 ++++ mod/subsection/index.php | 89 ++++++++ mod/subsection/lang/en/subsection.php | 34 +++ mod/subsection/lib.php | 209 ++++++++++++++++++ mod/subsection/mod_form.php | 67 ++++++ mod/subsection/pix/monologo.svg | 3 + mod/subsection/renderer.php | 25 +++ mod/subsection/settings.php | 34 +++ .../tests/behat/subsection_actionmenu.feature | 51 +++++ .../tests/behat/subsection_handling.feature | 60 +++++ .../tests/behat/subsection_navigation.feature | 43 ++++ .../tests/behat/subsection_rename.feature | 53 +++++ .../courseformat/sectiondelegate_test.php | 72 ++++++ .../sectiondelegatemodule_test.php | 103 +++++++++ mod/subsection/tests/generator/lib.php | 26 +++ mod/subsection/version.php | 31 +++ mod/subsection/view.php | 57 +++++ 33 files changed, 1825 insertions(+) create mode 100644 mod/subsection/backup/moodle2/backup_subsection_activity_task.class.php create mode 100644 mod/subsection/backup/moodle2/backup_subsection_stepslib.php create mode 100644 mod/subsection/backup/moodle2/restore_subsection_activity_task.class.php create mode 100644 mod/subsection/backup/moodle2/restore_subsection_stepslib.php create mode 100644 mod/subsection/classes/courseformat/sectiondelegate.php create mode 100644 mod/subsection/classes/event/course_module_instance_list_viewed.php create mode 100644 mod/subsection/classes/event/course_module_viewed.php create mode 100644 mod/subsection/classes/local/callbacks/after_cm_name_edited_handler.php create mode 100644 mod/subsection/classes/manager.php create mode 100644 mod/subsection/classes/privacy/provider.php create mode 100644 mod/subsection/db/access.php create mode 100644 mod/subsection/db/hooks.php create mode 100644 mod/subsection/db/install.php create mode 100644 mod/subsection/db/install.xml create mode 100644 mod/subsection/db/uninstall.php create mode 100644 mod/subsection/db/upgrade.php create mode 100644 mod/subsection/index.php create mode 100644 mod/subsection/lang/en/subsection.php create mode 100644 mod/subsection/lib.php create mode 100644 mod/subsection/mod_form.php create mode 100644 mod/subsection/pix/monologo.svg create mode 100644 mod/subsection/renderer.php create mode 100644 mod/subsection/settings.php create mode 100644 mod/subsection/tests/behat/subsection_actionmenu.feature create mode 100644 mod/subsection/tests/behat/subsection_handling.feature create mode 100644 mod/subsection/tests/behat/subsection_navigation.feature create mode 100644 mod/subsection/tests/behat/subsection_rename.feature create mode 100644 mod/subsection/tests/courseformat/sectiondelegate_test.php create mode 100644 mod/subsection/tests/courseformat/sectiondelegatemodule_test.php create mode 100644 mod/subsection/tests/generator/lib.php create mode 100644 mod/subsection/version.php create mode 100644 mod/subsection/view.php diff --git a/lib/plugins.json b/lib/plugins.json index af427242c7877..e3b44ae6ad596 100644 --- a/lib/plugins.json +++ b/lib/plugins.json @@ -320,6 +320,7 @@ "quiz", "resource", "scorm", + "subsection", "survey", "url", "wiki", diff --git a/mod/subsection/backup/moodle2/backup_subsection_activity_task.class.php b/mod/subsection/backup/moodle2/backup_subsection_activity_task.class.php new file mode 100644 index 0000000000000..ee5be7eab3734 --- /dev/null +++ b/mod/subsection/backup/moodle2/backup_subsection_activity_task.class.php @@ -0,0 +1,73 @@ +. + +/** + * The task that provides all the steps to perform a complete backup is defined here. + * + * @package mod_subsection + * @category backup + * @copyright 2023 Amaia Anabitarte + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +// More information about the backup process: {@link https://docs.moodle.org/dev/Backup_API}. +// More information about the restore process: {@link https://docs.moodle.org/dev/Restore_API}. + +require_once($CFG->dirroot.'//mod/subsection/backup/moodle2/backup_subsection_stepslib.php'); + +/** + * Provides all the settings and steps to perform a complete backup of mod_subsection. + */ +class backup_subsection_activity_task extends backup_activity_task { + + /** + * Defines particular settings for the plugin. + */ + protected function define_my_settings() { + return; + } + + /** + * Defines particular steps for the backup process. + */ + protected function define_my_steps() { + $this->add_step(new backup_subsection_activity_structure_step('subsection_structure', 'subsection.xml')); + } + + /** + * Codes the transformations to perform in the activity in order to get transportable (encoded) links. + * + * @param string $content + * @return string + */ + public static function encode_content_links($content) { + global $CFG; + + $base = preg_quote($CFG->wwwroot, "/"); + + // Link to the list of subsections. + $search = "/(".$base."\/mod\/subsection\/index.php\?id\=)([0-9]+)/"; + $content = preg_replace($search, '$@SUBSECTIONINDEX*$2@$', $content); + + // Link to page view by moduleid. + $search = "/(".$base."\/mod\/subsection\/view.php\?id\=)([0-9]+)/"; + $content = preg_replace($search, '$@SUBSECTIONVIEWBYID*$2@$', $content); + + return $content; + } +} diff --git a/mod/subsection/backup/moodle2/backup_subsection_stepslib.php b/mod/subsection/backup/moodle2/backup_subsection_stepslib.php new file mode 100644 index 0000000000000..5c8b0fbb2cae2 --- /dev/null +++ b/mod/subsection/backup/moodle2/backup_subsection_stepslib.php @@ -0,0 +1,48 @@ +. + +/** + * Backup steps for mod_subsection are defined here. + * + * @package mod_subsection + * @category backup + * @copyright 2023 Amaia Anabitarte + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// More information about the backup process: {@link https://docs.moodle.org/dev/Backup_API}. + +/** + * Define the complete structure for backup, with file and id annotations. + */ +class backup_subsection_activity_structure_step extends backup_activity_structure_step { + + /** + * Defines the structure of the resulting xml file. + * + * @return backup_nested_element The structure wrapped by the common 'activity' element. + */ + protected function define_structure() { + // Define each element separated. + $subsection = new backup_nested_element('subsection', ['id'], ['name', 'timemodified']); + + // Define sources. + $subsection->set_source_table('subsection', ['id' => backup::VAR_ACTIVITYID]); + + // Return the root element (subsection), wrapped into standard activity structure. + return $this->prepare_activity_structure($subsection); + } +} diff --git a/mod/subsection/backup/moodle2/restore_subsection_activity_task.class.php b/mod/subsection/backup/moodle2/restore_subsection_activity_task.class.php new file mode 100644 index 0000000000000..c49739fed2427 --- /dev/null +++ b/mod/subsection/backup/moodle2/restore_subsection_activity_task.class.php @@ -0,0 +1,94 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +/** + * The task that provides a complete restore of mod_subsection is defined here. + * + * @package mod_subsection + * @category backup + * @copyright 2023 Amaia Anabitarte + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// More information about the backup process: {@link https://docs.moodle.org/dev/Backup_API}. +// More information about the restore process: {@link https://docs.moodle.org/dev/Restore_API}. + +require_once($CFG->dirroot.'//mod/subsection/backup/moodle2/restore_subsection_stepslib.php'); + +/** + * Restore task for mod_subsection. + */ +class restore_subsection_activity_task extends restore_activity_task { + + /** + * Defines particular settings that this activity can have. + */ + protected function define_my_settings() { + return; + } + + /** + * Defines particular steps that this activity can have. + * + * @return base_step. + */ + protected function define_my_steps() { + $this->add_step(new restore_subsection_activity_structure_step('subsection_structure', 'subsection.xml')); + } + + /** + * Defines the contents in the activity that must be processed by the link decoder. + * + * @return array. + */ + public static function define_decode_contents() { + $contents = []; + + // Define the contents. + + return $contents; + } + + /** + * Defines the decoding rules for links belonging to the activity to be executed by the link decoder. + * + * @return array. + */ + public static function define_decode_rules() { + $rules = []; + + // Define the rules. + + return $rules; + } + + /** + * Defines the restore log rules that will be applied by the + * {@see restore_logs_processor} when restoring mod_subsection logs. It + * must return one array of {@see restore_log_rule} objects. + * + * @return array. + */ + public static function define_restore_log_rules() { + $rules = []; + + // Define the rules. + + return $rules; + } +} diff --git a/mod/subsection/backup/moodle2/restore_subsection_stepslib.php b/mod/subsection/backup/moodle2/restore_subsection_stepslib.php new file mode 100644 index 0000000000000..9102fa137f851 --- /dev/null +++ b/mod/subsection/backup/moodle2/restore_subsection_stepslib.php @@ -0,0 +1,73 @@ +. + +/** + * All the steps to restore mod_subsection are defined here. + * + * @package mod_subsection + * @category backup + * @copyright 2023 Amaia Anabitarte + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use mod_subsection\manager; + +// More information about the restore process: {@link https://docs.moodle.org/dev/Restore_API}. + +/** + * Defines the structure step to restore one mod_subsection activity. + */ +class restore_subsection_activity_structure_step extends restore_activity_structure_step { + + /** + * Defines the structure to be restored. + * + * @return restore_path_element[]. + */ + protected function define_structure() { + $paths = []; + $paths[] = new restore_path_element('subsection', '/activity/subsection'); + + // Return the paths wrapped into standard activity structure. + return $this->prepare_activity_structure($paths); + } + + /** + * Process the subsection element. + * + * @param \stdClass $data the data to be processed. + */ + protected function process_subsection($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + $data->course = $this->get_courseid(); + + // Insert the subsection record. + $newitemid = $DB->insert_record('subsection', $data); + // Immediately after inserting "activity" record, call this. + $this->apply_activity_instance($newitemid); + $this->set_delegated_section_mapping(manager::PLUGINNAME, $oldid, $newitemid); + } + + /** + * Defines post-execution actions. + */ + protected function after_execute() { + return; + } +} diff --git a/mod/subsection/classes/courseformat/sectiondelegate.php b/mod/subsection/classes/courseformat/sectiondelegate.php new file mode 100644 index 0000000000000..6c7d38e8ee11b --- /dev/null +++ b/mod/subsection/classes/courseformat/sectiondelegate.php @@ -0,0 +1,38 @@ +. + +namespace mod_subsection\courseformat; + +use action_menu; +use core_courseformat\base as course_format; +use core_courseformat\output\local\content\section\controlmenu; +use core_courseformat\sectiondelegatemodule; +use mod_subsection\manager; +use renderer_base; + +/** + * Subsection plugin section delegate class. + * + * This class implements all the integrations needed to delegate core section logic to + * the plugin. For a basic subsection plugin, all methods are inherited from the + * sectiondelegatemodule class. + * + * @package mod_subsection + * @copyright 2023 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class sectiondelegate extends sectiondelegatemodule { +} diff --git a/mod/subsection/classes/event/course_module_instance_list_viewed.php b/mod/subsection/classes/event/course_module_instance_list_viewed.php new file mode 100644 index 0000000000000..b49eca4441243 --- /dev/null +++ b/mod/subsection/classes/event/course_module_instance_list_viewed.php @@ -0,0 +1,29 @@ +. + +namespace mod_subsection\event; + +/** + * The mod_subsection viewed event class. + * + * @package mod_subsection + * @since Moodle 4.5 + * @copyright 2023 Amaia Anabitarte + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed { + // No code required here as the parent class handles it all. +} diff --git a/mod/subsection/classes/event/course_module_viewed.php b/mod/subsection/classes/event/course_module_viewed.php new file mode 100644 index 0000000000000..06d5474d3bb79 --- /dev/null +++ b/mod/subsection/classes/event/course_module_viewed.php @@ -0,0 +1,41 @@ +. + +namespace mod_subsection\event; + +/** + * The mod_subsection viewed event class. + * + * @package mod_subsection + * @since Moodle 4.5 + * @copyright 2023 Amaia Anabitarte + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_module_viewed extends \core\event\course_module_viewed { + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + $this->data['objecttable'] = 'subsection'; + } + + public static function get_objectid_mapping() { + return ['db' => 'subsection', 'restore' => 'subsection']; + } +} diff --git a/mod/subsection/classes/local/callbacks/after_cm_name_edited_handler.php b/mod/subsection/classes/local/callbacks/after_cm_name_edited_handler.php new file mode 100644 index 0000000000000..e7abad2dbc4cc --- /dev/null +++ b/mod/subsection/classes/local/callbacks/after_cm_name_edited_handler.php @@ -0,0 +1,48 @@ +. + +namespace mod_subsection\local\callbacks; + +use core_courseformat\hook\after_cm_name_edited; +use core_courseformat\formatactions; +use mod_subsection\manager; + +/** + * Class after activity renaming hook handler. + * + * @package mod_subsection + * @copyright 2024 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class after_cm_name_edited_handler { + /** + * Handle the activity name change. + * + * @param after_cm_name_edited $hook + */ + public static function callback(after_cm_name_edited $hook): void { + $cm = $hook->get_cm(); + + if ($cm->modname !== manager::MODULE) { + return; + } + + $section = get_fast_modinfo($cm->course)->get_section_info_by_component(manager::PLUGINNAME, $cm->instance); + if ($section) { + formatactions::section($cm->course)->update($section, ['name' => $hook->get_newname()]); + } + } +} diff --git a/mod/subsection/classes/manager.php b/mod/subsection/classes/manager.php new file mode 100644 index 0000000000000..3e6271e00ecf4 --- /dev/null +++ b/mod/subsection/classes/manager.php @@ -0,0 +1,183 @@ +. + +namespace mod_subsection; + +use cm_info; +use context_module; +use completion_info; +use mod_subsection\event\course_module_viewed; +use moodle_page; +use stdClass; + +/** + * Class manager for subsection + * + * @package mod_subsection + * @copyright 2023 Amaia Anabitarte + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class manager { + + /** Module name. */ + const MODULE = 'subsection'; + + /** The plugin name. */ + const PLUGINNAME = 'mod_subsection'; + + /** @var string plugin path. */ + public $path; + + /** @var stdClass course_module record. */ + private $instance; + + /** @var context_module the current context. */ + private $context; + + /** @var cm_info course_modules record. */ + private $cm; + + /** + * Class constructor. + * + * @param cm_info $cm course module info object + * @param stdClass $instance activity instance object. + */ + public function __construct(cm_info $cm, stdClass $instance) { + global $CFG; + $this->cm = $cm; + $this->instance = $instance; + $this->context = context_module::instance($cm->id); + $this->instance->cmidnumber = $cm->idnumber; + $this->path = $CFG->dirroot . '/mod/' . self::MODULE; + } + + /** + * Create a manager instance from an instance record. + * + * @param stdClass $instance an activity record + * @return manager + */ + public static function create_from_instance(stdClass $instance): self { + $cm = get_coursemodule_from_instance(self::MODULE, $instance->id); + // Ensure that $this->cm is a cm_info object. + $cm = cm_info::create($cm); + return new self($cm, $instance); + } + + /** + * Create a manager instance from a course_modules record. + * + * @param stdClass|cm_info $cm an activity record + * @return manager + */ + public static function create_from_coursemodule($cm): self { + global $DB; + // Ensure that $this->cm is a cm_info object. + $cm = cm_info::create($cm); + $instance = $DB->get_record(self::MODULE, ['id' => $cm->instance], '*', MUST_EXIST); + return new self($cm, $instance); + } + + /** + * Create a manager instance from a record id. + * + * @param int $courseid the course id + * @param int $id an activity id + * @return manager + */ + public static function create_from_id(int $courseid, int $id): self { + $cm = get_coursemodule_from_instance('subsection', $id, $courseid); + return self::create_from_coursemodule($cm); + } + + /** + * Create a manager instance from a subsection_record entry. + * + * @param stdClass $record the subsection_record record + * @return manager + */ + public static function create_from_data_record($record): self { + global $DB; + $instance = $DB->get_record(self::MODULE, ['id' => $record->dataid], '*', MUST_EXIST); + $cm = get_coursemodule_from_instance(self::MODULE, $instance->id); + $cm = cm_info::create($cm); + return new self($cm, $instance); + } + + /** + * Return the current context. + * + * @return context_module + */ + public function get_context(): context_module { + return $this->context; + } + + /** + * Return the current instance. + * + * @return stdClass the instance record + */ + public function get_instance(): stdClass { + return $this->instance; + } + + /** + * Return the current cm_info. + * + * @return cm_info the course module + */ + public function get_coursemodule(): cm_info { + return $this->cm; + } + + /** + * Return the current module renderer. + * + * @param moodle_page|null $page the current page + * @return \mod_subsection_renderer the module renderer + */ + public function get_renderer(?moodle_page $page = null): \mod_subsection_renderer { + global $PAGE; + $page = $page ?? $PAGE; + return $page->get_renderer(self::PLUGINNAME); + } + + /** + * Trigger module viewed event and set the module viewed for completion. + * + * @param stdClass $course course object + */ + public function set_module_viewed(stdClass $course) { + global $CFG; + require_once($CFG->libdir . '/completionlib.php'); + + // Trigger module viewed event. + $event = course_module_viewed::create([ + 'objectid' => $this->instance->id, + 'context' => $this->context, + ]); + $event->add_record_snapshot('course', $course); + $event->add_record_snapshot('course_modules', $this->cm); + $event->add_record_snapshot(self::MODULE, $this->instance); + $event->trigger(); + + // Completion. + $completion = new completion_info($course); + $completion->set_module_viewed($this->cm); + } +} diff --git a/mod/subsection/classes/privacy/provider.php b/mod/subsection/classes/privacy/provider.php new file mode 100644 index 0000000000000..5dbbf8ee805bb --- /dev/null +++ b/mod/subsection/classes/privacy/provider.php @@ -0,0 +1,37 @@ +. + +namespace mod_subsection\privacy; + +/** + * Privacy API implementation for the Subsection plugin. + * + * @package mod_subsection + * @category privacy + * @copyright 2023 Amaia Anabitarte + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Returns stringid of a text explaining that this plugin stores no personal data. + * + * @return string + */ + public static function get_reason(): string { + return 'privacy:metadata'; + } +} diff --git a/mod/subsection/db/access.php b/mod/subsection/db/access.php new file mode 100644 index 0000000000000..7731f6c8fef63 --- /dev/null +++ b/mod/subsection/db/access.php @@ -0,0 +1,41 @@ +. + +/** + * Plugin capabilities are defined here. + * + * @package mod_subsection + * @category access + * @copyright 2023 Amaia Anabitarte + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = [ + + 'mod/subsection:addinstance' => [ + 'riskbitmask' => RISK_XSS, + + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => [ + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW, + ], + 'clonepermissionsfrom' => 'moodle/course:manageactivities', + ], +]; diff --git a/mod/subsection/db/hooks.php b/mod/subsection/db/hooks.php new file mode 100644 index 0000000000000..37b3fd1198d26 --- /dev/null +++ b/mod/subsection/db/hooks.php @@ -0,0 +1,33 @@ +. + +/** + * Hook callbacks for Subsection + * + * @package mod_subsection + * @copyright 2024 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$callbacks = [ + [ + 'hook' => core_courseformat\hook\after_cm_name_edited::class, + 'callback' => 'mod_subsection\local\callbacks\after_cm_name_edited_handler::callback', + 'priority' => 0, + ], +]; diff --git a/mod/subsection/db/install.php b/mod/subsection/db/install.php new file mode 100644 index 0000000000000..6ee6ac8c89913 --- /dev/null +++ b/mod/subsection/db/install.php @@ -0,0 +1,32 @@ +. + +/** + * Code to be executed after the plugin's database scheme has been installed is defined here. + * + * @package mod_subsection + * @category upgrade + * @copyright 2023 Amaia Anabitarte + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Custom code to be run on installing the plugin. + */ +function xmldb_subsection_install() { + + return true; +} diff --git a/mod/subsection/db/install.xml b/mod/subsection/db/install.xml new file mode 100644 index 0000000000000..a452b5315cf13 --- /dev/null +++ b/mod/subsection/db/install.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + +
+
+
diff --git a/mod/subsection/db/uninstall.php b/mod/subsection/db/uninstall.php new file mode 100644 index 0000000000000..9b0d49697aa94 --- /dev/null +++ b/mod/subsection/db/uninstall.php @@ -0,0 +1,31 @@ +. + +/** + * Code that is executed before the tables and data are dropped during the plugin uninstallation. + * + * @package mod_subsection + * @category upgrade + * @copyright 2023 Amaia Anabitarte + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Custom uninstallation procedure. + */ +function xmldb_subsection_uninstall() { + return true; +} diff --git a/mod/subsection/db/upgrade.php b/mod/subsection/db/upgrade.php new file mode 100644 index 0000000000000..9374187f51159 --- /dev/null +++ b/mod/subsection/db/upgrade.php @@ -0,0 +1,43 @@ +. + +/** + * Plugin upgrade steps are defined here. + * + * @package mod_subsection + * @category upgrade + * @copyright 2023 Amaia Anabitarte + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Execute mod_subsection upgrade from the given old version. + * + * @param int $oldversion + * @return bool + */ +function xmldb_subsection_upgrade($oldversion) { + global $DB; + + $dbman = $DB->get_manager(); + + // For further information please read {@link https://docs.moodle.org/dev/Upgrade_API}. + // + // You will also have to create the db/install.xml file by using the XMLDB Editor. + // Documentation for the XMLDB Editor can be found at {@link https://docs.moodle.org/dev/XMLDB_editor}. + + return true; +} diff --git a/mod/subsection/index.php b/mod/subsection/index.php new file mode 100644 index 0000000000000..04910b166d260 --- /dev/null +++ b/mod/subsection/index.php @@ -0,0 +1,89 @@ +. + +/** + * Display information about all the mod_subsection modules in the requested course. + * + * @package mod_subsection + * @copyright 2023 Amaia Anabitarte + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require(__DIR__.'/../../config.php'); + +require_once(__DIR__.'/lib.php'); + +$id = required_param('id', PARAM_INT); + +$course = $DB->get_record('course', ['id' => $id], '*', MUST_EXIST); +require_course_login($course); + +$coursecontext = context_course::instance($course->id); +$event = \mod_subsection\event\course_module_instance_list_viewed::create(['context' => $coursecontext]); +$event->add_record_snapshot('course', $course); +$event->trigger(); + +$PAGE->set_url('/mod/subsection/index.php', ['id' => $id]); +$PAGE->set_title(format_string($course->fullname)); +$PAGE->set_heading(format_string($course->fullname)); +$PAGE->set_context($coursecontext); + +echo $OUTPUT->header(); + +$modulenameplural = get_string('modulenameplural', 'mod_subsection'); +echo $OUTPUT->heading($modulenameplural); + +$subsections = get_all_instances_in_course('subsection', $course); + +if (empty($subsections)) { + notice(get_string('thereareno', 'moodle', $modulenameplural), "$CFG->wwwroot/course/view.php?id=$course->id"); +} + +$table = new html_table(); +$table->attributes['class'] = 'generaltable mod_index'; + +if ($course->format == 'weeks') { + $table->head = [get_string('week'), get_string('name')]; + $table->align = ['center', 'left']; +} else if ($course->format == 'topics') { + $table->head = [get_string('topic'), get_string('name')]; + $table->align = ['center', 'left', 'left', 'left']; +} else { + $table->head = [get_string('name')]; + $table->align = ['left', 'left', 'left']; +} + +foreach ($subsections as $subsection) { + if (!$subsection->visible) { + $link = html_writer::link( + new moodle_url('/mod/subsection/view.php', ['id' => $subsection->coursemodule]), + format_string($subsection->name, true), + ['class' => 'dimmed']); + } else { + $link = html_writer::link( + new moodle_url('/mod/subsection/view.php', ['id' => $subsection->coursemodule]), + format_string($subsection->name, true)); + } + + if ($course->format == 'weeks' || $course->format == 'topics') { + $table->data[] = [$subsection->section, $link]; + } else { + $table->data[] = [$link]; + } +} + +echo html_writer::table($table); +echo $OUTPUT->footer(); diff --git a/mod/subsection/lang/en/subsection.php b/mod/subsection/lang/en/subsection.php new file mode 100644 index 0000000000000..c8fa796b5365c --- /dev/null +++ b/mod/subsection/lang/en/subsection.php @@ -0,0 +1,34 @@ +. + +/** + * Plugin strings are defined here. + * + * @package mod_subsection + * @category string + * @copyright 2023 Amaia Anabitarte + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['modulename'] = 'Subsection'; +$string['modulename_help'] = 'Delegated subsections'; +$string['modulenameplural'] = 'Subsections'; +$string['pluginadministration'] = 'Subsection administration'; +$string['pluginname'] = 'Subsection'; +$string['privacy:metadata'] = 'Subsection does not store any personal data'; +$string['subsection:addinstance'] = 'Add subsection'; +$string['subsection:view'] = 'View subsection'; +$string['subsectionname'] = 'Name'; diff --git a/mod/subsection/lib.php b/mod/subsection/lib.php new file mode 100644 index 0000000000000..9a8ed89e8dfae --- /dev/null +++ b/mod/subsection/lib.php @@ -0,0 +1,209 @@ +. + +/** + * Library of interface functions and constants. + * + * @package mod_subsection + * @copyright 2023 Amaia Anabitarte + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + use core_courseformat\formatactions; + use mod_subsection\manager; + +/** + * Return if the plugin supports $feature. + * + * @param string $feature Constant representing the feature. + * @return mixed True if module supports feature, false if not, null if doesn't know or string for the module purpose. + */ +function subsection_supports($feature) { + return match ($feature) { + FEATURE_MOD_ARCHETYPE => MOD_ARCHETYPE_RESOURCE, + FEATURE_GROUPS => false, + FEATURE_GROUPINGS => false, + FEATURE_MOD_INTRO => false, + FEATURE_COMPLETION_TRACKS_VIEWS => true, + FEATURE_GRADE_HAS_GRADE => false, + FEATURE_GRADE_OUTCOMES => false, + FEATURE_BACKUP_MOODLE2 => true, + FEATURE_SHOW_DESCRIPTION => false, + FEATURE_MOD_PURPOSE => MOD_PURPOSE_CONTENT, + default => null, + }; +} + +/** + * Saves a new instance of the mod_subsection into the database. + * + * Given an object containing all the necessary data, (defined by the form + * in mod_form.php) this function will create a new instance and return the id + * number of the instance. + * + * @param object $moduleinstance An object from the form. + * @param mod_subsection_mod_form $mform The form. + * @return int The id of the newly inserted record. + */ +function subsection_add_instance($moduleinstance, $mform = null) { + global $DB; + + $moduleinstance->timecreated = time(); + + $id = $DB->insert_record('subsection', $moduleinstance); + + formatactions::section($moduleinstance->course)->create_delegated( + manager::PLUGINNAME, + $id, + (object)[ + 'name' => $moduleinstance->name, + ]); + + return $id; +} + +/** + * Updates an instance of the mod_subsection in the database. + * + * Given an object containing all the necessary data (defined in mod_form.php), + * this function will update an existing instance with new data. + * + * @param object $moduleinstance An object from the form in mod_form.php. + * @param mod_subsection_mod_form $mform The form. + * @return bool True if successful, false otherwise. + */ +function subsection_update_instance($moduleinstance, $mform = null) { + global $DB; + + $moduleinstance->timemodified = time(); + $moduleinstance->id = $moduleinstance->instance; + + return $DB->update_record('subsection', $moduleinstance); +} + +/** + * Removes an instance of the mod_subsection from the database. + * + * @param int $id Id of the module instance. + * @return bool True if successful, false on failure. + */ +function subsection_delete_instance($id) { + global $DB; + + $exists = $DB->get_record('subsection', ['id' => $id]); + if (!$exists) { + return false; + } + + $cm = get_coursemodule_from_instance(manager::MODULE, $id); + $delegatesection = get_fast_modinfo($cm->course)->get_section_info_by_component(manager::PLUGINNAME, $id); + if ($delegatesection) { + formatactions::section($cm->course)->delete($delegatesection); + } + + $DB->delete_records('subsection', ['id' => $id]); + + return true; +} + +/** + * Returns the lists of all browsable file areas within the given module context. + * + * The file area 'intro' for the activity introduction field is added automatically + * by {@see file_browser::get_file_info_context_module()}. + * + * @package mod_subsection + * @category files + * + * @param stdClass $course + * @param stdClass $cm + * @param stdClass $context + * @return string[]. + */ +function subsection_get_file_areas($course, $cm, $context) { + return []; +} + +/** + * File browsing support for mod_subsection file areas. + * + * @package mod_subsection + * @category files + * + * @param file_browser $browser + * @param array $areas + * @param stdClass $course + * @param stdClass $cm + * @param stdClass $context + * @param string $filearea + * @param int $itemid + * @param string $filepath + * @param string $filename + * @return file_info|null file_info instance or null if not found. + */ +function subsection_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) { + return null; +} + +/** + * Serves the files from the mod_subsection file areas. + * + * @package mod_subsection + * @category files + * + * @param stdClass $course The course object. + * @param stdClass $cm The course module object. + * @param stdClass $context The mod_subsection's context. + * @param string $filearea The name of the file area. + * @param array $args Extra arguments (itemid, path). + * @param bool $forcedownload Whether or not force download. + * @param array $options Additional options affecting the file serving. + */ +function subsection_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, $options = []) { + global $DB, $CFG; + + if ($context->contextlevel != CONTEXT_MODULE) { + send_file_not_found(); + } + + require_login($course, true, $cm); + send_file_not_found(); +} + +/** + * Extends the global navigation tree by adding mod_subsection nodes if there is a relevant content. + * + * This can be called by an AJAX request so do not rely on $PAGE as it might not be set up properly. + * + * @param navigation_node $subsectionnode An object representing the navigation tree node. + * @param stdClass $course + * @param stdClass $module + * @param cm_info $cm + */ +function subsection_extend_navigation($subsectionnode, $course, $module, $cm) { +} + +/** + * Extends the settings navigation with the mod_subsection settings. + * + * This function is called when the context for the page is a mod_subsection module. + * This is not called by AJAX so it is safe to rely on the $PAGE. + * + * @param settings_navigation $settingsnav {@see settings_navigation} + * @param navigation_node $subsectionnode {@see navigation_node} + */ +function subsection_extend_settings_navigation($settingsnav, $subsectionnode = null) { +} diff --git a/mod/subsection/mod_form.php b/mod/subsection/mod_form.php new file mode 100644 index 0000000000000..2cd1927b4bc12 --- /dev/null +++ b/mod/subsection/mod_form.php @@ -0,0 +1,67 @@ +. + +/** + * The main mod_subsection configuration form. + * + * @package mod_subsection + * @copyright 2023 Amaia Anabitarte + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/course/moodleform_mod.php'); + +/** + * Module instance settings form. + * + * @package mod_subsection + * @copyright 2023 Amaia Anabitarte + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_subsection_mod_form extends moodleform_mod { + + /** + * Defines forms elements + */ + public function definition() { + global $CFG; + + $mform = $this->_form; + + // Adding the "general" fieldset, where all the common settings are shown. + $mform->addElement('header', 'general', get_string('general', 'form')); + + // Adding the standard "name" field. + $mform->addElement('text', 'name', get_string('subsectionname', 'mod_subsection'), ['size' => '64']); + + if (!empty($CFG->formatstringstriptags)) { + $mform->setType('name', PARAM_TEXT); + } else { + $mform->setType('name', PARAM_CLEANHTML); + } + + $mform->addRule('name', null, 'required', null, 'client'); + $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); + + // Add standard elements. + $this->standard_coursemodule_elements(); + + // Add standard buttons. + $this->add_action_buttons(); + } +} diff --git a/mod/subsection/pix/monologo.svg b/mod/subsection/pix/monologo.svg new file mode 100644 index 0000000000000..b3bef3ef0587b --- /dev/null +++ b/mod/subsection/pix/monologo.svg @@ -0,0 +1,3 @@ + + + diff --git a/mod/subsection/renderer.php b/mod/subsection/renderer.php new file mode 100644 index 0000000000000..1ecf339344032 --- /dev/null +++ b/mod/subsection/renderer.php @@ -0,0 +1,25 @@ +. + +/** + * Subsection activity renderer. + * + * @copyright 2023 Amaia Anabitarte + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package mod_subsection + */ +class mod_subsection_renderer extends plugin_renderer_base { +} diff --git a/mod/subsection/settings.php b/mod/subsection/settings.php new file mode 100644 index 0000000000000..9025c8122d68f --- /dev/null +++ b/mod/subsection/settings.php @@ -0,0 +1,34 @@ +. + +/** + * Plugin administration pages are defined here. + * + * @package mod_subsection + * @category admin + * @copyright 2023 Amaia Anabitarte + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +if ($hassiteconfig) { + $settings = new admin_settingpage('mod_subsection_settings', new lang_string('pluginname', 'mod_subsection')); + + // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf + if ($ADMIN->fulltree) { + } +} diff --git a/mod/subsection/tests/behat/subsection_actionmenu.feature b/mod/subsection/tests/behat/subsection_actionmenu.feature new file mode 100644 index 0000000000000..084a0c9d4533b --- /dev/null +++ b/mod/subsection/tests/behat/subsection_actionmenu.feature @@ -0,0 +1,51 @@ +@mod @mod_subsection +Feature: The module menu replaces the section menu when accessing the subsection page + In order to use subsections + As an teacher + I need to see the module action menu in the section page. + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | category | numsections | + | Course 1 | C1 | 0 | 2 | + And the following "activity" exists: + | activity | subsection | + | name | Subsection1 | + | course | C1 | + | idnumber | subsection1 | + | section | 1 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And I am on the "C1" "Course" page logged in as "teacher1" + + @javascript + Scenario: The action menu for subsection page meets the module menu + Given I click on "Subsection1" "link" in the "region-main" "region" + And I turn editing mode on + # Open the action menu. + When I click on "Edit" "icon" in the "[data-region='header-actions-container']" "css_element" + Then I should not see "Move right" + And I should not see "Assign roles" + And I should not see "Permalink" + And I should not see "Highlight" + And I should see "Edit settings" + And I should see "Move" + And I should see "Hide" + And I should see "Duplicate" + And I should see "Delete" + + @javascript + Scenario: The action menu for subsection module has less options thant a regular activity + Given I turn editing mode on + When I open "Subsection1" actions menu + Then I should not see "Move right" + And I should not see "Assign roles" + And I should see "Edit settings" + And I should see "Move" + And I should see "Hide" + And I should see "Duplicate" + And I should see "Delete" diff --git a/mod/subsection/tests/behat/subsection_handling.feature b/mod/subsection/tests/behat/subsection_handling.feature new file mode 100644 index 0000000000000..64365667f2c7f --- /dev/null +++ b/mod/subsection/tests/behat/subsection_handling.feature @@ -0,0 +1,60 @@ +@mod @mod_subsection +Feature: Teachers create and destroy subsections + In order to use subsections + As an teacher + I need to create and destroy subsections + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | category | numsections | + | Course 1 | C1 | 0 | 2 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And I log in as "teacher1" + + Scenario: Subsections are not listed as regular sections + Given the following "activities" exist: + | activity | name | course | idnumber | section | + | subsection | Subsection1 | C1 | forum1 | 1 | + | data | Subactivity1 | C1 | data1 | 3 | + When I am on "Course 1" course homepage + Then "Subsection1" "section" should not exist + And I should not see "Subactivity1" in the "region-main" "region" + And I click on "Subsection1" "link" in the "region-main" "region" + And I should see "Subsection1" in the "page" "region" + And I should see "Subactivity1" in the "region-main" "region" + + Scenario: Activities can be created in a subsection + Given the following "activities" exist: + | activity | name | course | idnumber | section | + | subsection | Subsection1 | C1 | forum1 | 1 | + When I add an "assign" activity to course "Course 1" section "3" and I fill the form with: + | Assignment name | Test assignment name | + | ID number | Test assignment name | + | Description | Test assignment description | + And I am on "Course 1" course homepage + And I click on "Subsection1" "link" in the "region-main" "region" + Then I should see "Test assignment name" in the "region-main" "region" + And I am on "Course 1" course homepage + And I should not see "Test assignment name" in the "region-main" "region" + + @javascript + Scenario: Teacher can create activities in a subsection page with the activity chooser + Given the following "activities" exist: + | activity | name | course | idnumber | section | + | subsection | Subsection1 | C1 | forum1 | 1 | + When I am on "Course 1" course homepage with editing mode on + And I click on "Subsection1" "link" in the "region-main" "region" + And I add a "Assignment" to section "3" using the activity chooser + And I set the following fields to these values: + | Assignment name | Test assignment name | + | ID number | Test assignment name | + | Description | Test assignment description | + And I press "Save and return to course" + Then I should see "Test assignment name" in the "region-main" "region" + And I am on "Course 1" course homepage + And I should not see "Test assignment name" in the "region-main" "region" diff --git a/mod/subsection/tests/behat/subsection_navigation.feature b/mod/subsection/tests/behat/subsection_navigation.feature new file mode 100644 index 0000000000000..3ee554d29d944 --- /dev/null +++ b/mod/subsection/tests/behat/subsection_navigation.feature @@ -0,0 +1,43 @@ +@mod @mod_subsection +Feature: Teachers navigate to subsections + In order to use subsections + As an teacher + I need to navigate to subsections + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | category | numsections | initsections | + | Course 1 | C1 | 0 | 1 | 1 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And the following "activities" exist: + | activity | name | course | idnumber | section | + | subsection | Subsection 1 | C1 | subsection1 | 1 | + | page | Page in Subsection 1 | C1 | page1 | 2 | + And I log in as "teacher1" + + Scenario: Subsection section page shows parent section in the breadcrumb + When I am on the "C1 > Subsection 1" "course > section" page + Then "C1" "link" should exist in the ".breadcrumb" "css_element" + And "Section 1" "link" should exist in the ".breadcrumb" "css_element" + And "Subsection 1" "text" should exist in the ".breadcrumb" "css_element" + + Scenario: Activity page shows subsection and its parent section in the breadcrumb + When I am on the "page1" "Activity" page + Then "C1" "link" should exist in the ".breadcrumb" "css_element" + And "Section 1" "link" should exist in the ".breadcrumb" "css_element" + And "Subsection 1" "link" should exist in the ".breadcrumb" "css_element" + And "Page in Subsection 1" "text" should exist in the ".breadcrumb" "css_element" + + Scenario: Sections and Subsections are displayed in the navigation block + Given the following config values are set as admin: + | unaddableblocks | | theme_boost| + And I turn editing mode on + When I am on the "page1" "Activity" page + And I add the "Navigation" block if not present + Then "Section 1" "link" should appear before "Subsection 1" "link" in the "Navigation" "block" + And "Subsection 1" "link" should appear before "Page in Subsection 1" "link" in the "Navigation" "block" diff --git a/mod/subsection/tests/behat/subsection_rename.feature b/mod/subsection/tests/behat/subsection_rename.feature new file mode 100644 index 0000000000000..18c14f2748d51 --- /dev/null +++ b/mod/subsection/tests/behat/subsection_rename.feature @@ -0,0 +1,53 @@ +@mod @mod_subsection +Feature: Teachers can rename subsections + In order to change subsections name + As an teacher + I need to sync subsection and activity names + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | category | numsections | + | Course 1 | C1 | 0 | 2 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And the following "activities" exist: + | activity | name | course | idnumber | section | + | subsection | Subsection activity | C1 | forum1 | 1 | + | data | Subactivity | C1 | data1 | 3 | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + + @javascript + Scenario: Renaming the subsection activity changes the subsection name + Given I should see "Subsection activity" in the "page-content" "region" + When I set the field "Edit title" in the "Subsection activity" "activity" to "New name" + And I should not see "Subsection activity" in the "region-main" "region" + And I should see "New name" in the "page-content" "region" + Then I click on "New name" "link" in the "page-content" "region" + And I should see "New name" in the "page" "region" + And I should see "Subactivity" in the "region-main" "region" + + Scenario: Renaming the activity using the settings form rename the subsection name + Given I should see "Subsection activity" in the "page-content" "region" + When I click on "Edit settings" "link" in the "Subsection activity" "activity" + And I set the following fields to these values: + | Name | New name | + And I press "Save and display" + Then I should see "New name" in the "page" "region" + And I should see "Subactivity" in the "region-main" "region" + And I am on "Course 1" course homepage + And I should see "New name" in the "page-content" "region" + + @javascript + Scenario: Renaming the subsection renames the subsection activity name + Given I click on "Subsection activity" "link" in the "page-content" "region" + And I should see "Subsection activity" in the "page" "region" + And I should see "Subactivity" in the "region-main" "region" + When I set the field "Edit section name" in the "page" "region" to "New name" + Then I should see "New name" in the "page" "region" + And I am on "Course 1" course homepage + And I should see "New name" in the "page-content" "region" diff --git a/mod/subsection/tests/courseformat/sectiondelegate_test.php b/mod/subsection/tests/courseformat/sectiondelegate_test.php new file mode 100644 index 0000000000000..b421321381622 --- /dev/null +++ b/mod/subsection/tests/courseformat/sectiondelegate_test.php @@ -0,0 +1,72 @@ +. + +namespace mod_subsection\courseformat; + +/** + * Subsection delegated section tests. + * + * @package mod_subsection + * @copyright 2024 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \mod_subsection\courseformat\sectiondelegate + * @coversDefaultClass \mod_subsection\courseformat\sectiondelegate + */ +final class sectiondelegate_test extends \advanced_testcase { + + /** + * Test has_delegate_class(). + * + * @covers ::has_delegate_class + */ + public function test_has_delegate_class(): void { + $this->assertTrue(sectiondelegate::has_delegate_class('mod_subsection')); + } + + /** + * Test get_section_action_menu(). + * + * @covers ::get_section_action_menu + */ + public function test_get_section_action_menu(): void { + global $PAGE; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 1]); + $this->getDataGenerator()->create_module('subsection', ['course' => $course->id, 'section' => 1]); + + $modinfo = get_fast_modinfo($course->id); + $sectioninfos = $modinfo->get_section_info_all(); + // Get the section info for the delegated section. + $sectioninfo = $sectioninfos[2]; + $delegated = sectiondelegate::instance($sectioninfo); + $format = course_get_format($course); + + $outputclass = $format->get_output_classname('content\\section\\controlmenu'); + $controlmenu = new $outputclass($format, $sectioninfo); + $renderer = $PAGE->get_renderer('format_' . $course->format); + + // The default section menu should be different for the delegated section menu. + $result = $delegated->get_section_action_menu($format, $controlmenu, $renderer); + foreach ($result->get_secondary_actions() as $secondaryaction) { + // Highlight and Permalink are only present in section menu (not module), so they shouldn't be find in the result. + $this->assertNotEquals(get_string('highlight'), $secondaryaction->text); + $this->assertNotEquals(get_string('sectionlink', 'course'), $secondaryaction->text); + } + } +} diff --git a/mod/subsection/tests/courseformat/sectiondelegatemodule_test.php b/mod/subsection/tests/courseformat/sectiondelegatemodule_test.php new file mode 100644 index 0000000000000..1d28f7eb7a2c8 --- /dev/null +++ b/mod/subsection/tests/courseformat/sectiondelegatemodule_test.php @@ -0,0 +1,103 @@ +. + +namespace mod_subsection\courseformat; + +use mod_subsection\courseformat\sectiondelegate as testsectiondelegatemodule; +use section_info; +use cm_info; +use stdClass; + +/** + * Section delegate module tests. + * + * @package mod_subsection + * @copyright 2024 Mikel Martín + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \core_courseformat\sectiondelegatemodule + * @coversDefaultClass \core_courseformat\sectiondelegatemodule + */ +final class sectiondelegatemodule_test extends \advanced_testcase { + + /** + * Test get_parent_section. + * + * @covers ::get_parent_section + */ + public function test_get_parent_section(): void { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 2]); + $module = $this->getDataGenerator()->create_module('subsection', (object)['course' => $course->id, 'section' => 2]); + + // Get the section info for the delegated section. + $sectioninfo = get_fast_modinfo($course)->get_section_info_by_component('mod_subsection', $module->id); + + /** @var testsectiondelegatemodule */ + $delegated = sectiondelegate::instance($sectioninfo); + + $parentsectioninfo = $delegated->get_parent_section(); + + $this->assertInstanceOf(section_info::class, $parentsectioninfo); + $this->assertEquals(2, $parentsectioninfo->sectionnum); + } + + /** + * Test get_cm. + * + * @covers ::get_cm + */ + public function test_get_cm(): void { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 1]); + $module = $this->getDataGenerator()->create_module('subsection', (object)['course' => $course->id, 'section' => 1]); + + // Get the section info for the delegated section. + $sectioninfo = get_fast_modinfo($course)->get_section_info_by_component('mod_subsection', $module->id); + + /** @var testsectiondelegatemodule */ + $delegated = sectiondelegate::instance($sectioninfo); + + $delegatedsectioncm = $delegated->get_cm(); + + $this->assertInstanceOf(cm_info::class, $delegatedsectioncm); + $this->assertEquals($module->id, $delegatedsectioncm->instance); + } + + /** + * Test get_course. + * + * @covers ::get_course + */ + public function test_get_course(): void { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 1]); + $module = $this->getDataGenerator()->create_module('subsection', (object)['course' => $course->id, 'section' => 1]); + + // Get the section info for the delegated section. + $sectioninfo = get_fast_modinfo($course)->get_section_info_by_component('mod_subsection', $module->id); + + /** @var testsectiondelegatemodule */ + $delegated = sectiondelegate::instance($sectioninfo); + + $delegatedsectioncourse = $delegated->get_course(); + + $this->assertInstanceOf(stdClass::class, $delegatedsectioncourse); + $this->assertEquals($course->id, $delegatedsectioncourse->id); + } +} diff --git a/mod/subsection/tests/generator/lib.php b/mod/subsection/tests/generator/lib.php new file mode 100644 index 0000000000000..6379bf6e3bc9a --- /dev/null +++ b/mod/subsection/tests/generator/lib.php @@ -0,0 +1,26 @@ +. + +/** + * Data generator class for mod_subsection. + * + * @package mod_subsection + * @category test + * @copyright 2023 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_subsection_generator extends testing_module_generator { +} diff --git a/mod/subsection/version.php b/mod/subsection/version.php new file mode 100644 index 0000000000000..cf5b1647a32fb --- /dev/null +++ b/mod/subsection/version.php @@ -0,0 +1,31 @@ +. + +/** + * Plugin version and other meta-data are defined here. + * + * @package mod_subsection + * @copyright 2023 Amaia Anabitarte + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'mod_subsection'; +$plugin->release = '0.1.0'; +$plugin->version = 2024070100; +$plugin->requires = 2024070500; +$plugin->maturity = MATURITY_ALPHA; diff --git a/mod/subsection/view.php b/mod/subsection/view.php new file mode 100644 index 0000000000000..82f97a7bede09 --- /dev/null +++ b/mod/subsection/view.php @@ -0,0 +1,57 @@ +. + +/** + * Prints an instance of mod_subsection. + * + * @package mod_subsection + * @copyright 2023 Amaia Anabitarte + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use mod_subsection\manager; +use core_courseformat\formatactions; + +require(__DIR__.'/../../config.php'); +require_once(__DIR__.'/lib.php'); + +// Course module id. +$id = required_param('id', PARAM_INT); +$cm = get_coursemodule_from_id('subsection', $id, 0, false, MUST_EXIST); +$manager = manager::create_from_coursemodule($cm); +$course = $DB->get_record('course', ['id' => $cm->course], '*', MUST_EXIST); +$moduleinstance = $manager->get_instance(); + +require_login($course, true, $cm); + +$modulecontext = $manager->get_context(); +$manager->set_module_viewed($course); + +$modinfo = get_fast_modinfo($course); + +$delegatesection = $modinfo->get_section_info_by_component(manager::PLUGINNAME, $moduleinstance->id); +if (!$delegatesection) { + // Some restorations can produce a situation where the section is not found. + // In that case, we create a new one. + formatactions::section($course)->create_delegated( + manager::PLUGINNAME, + $id, + (object) [ + 'name' => $moduleinstance->name, + ] + ); +} +redirect(new moodle_url('/course/section.php', ['id' => $delegatesection->id])); From c7742b7a23af09a1474e9ce0eb3415072c9681bf Mon Sep 17 00:00:00 2001 From: ferranrecio Date: Thu, 4 Jul 2024 14:03:19 +0200 Subject: [PATCH 033/178] MDL-81765 course: fix double highlighted in subsections --- course/format/templates/local/courseindex/section.mustache | 3 ++- theme/boost/scss/moodle/courseindex.scss | 5 +++++ theme/boost/style/moodle.css | 4 ++++ theme/classic/style/moodle.css | 4 ++++ 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/course/format/templates/local/courseindex/section.mustache b/course/format/templates/local/courseindex/section.mustache index ca9186f84a8ca..0741eb70210fb 100644 --- a/course/format/templates/local/courseindex/section.mustache +++ b/course/format/templates/local/courseindex/section.mustache @@ -27,6 +27,7 @@ "number": 1, "sectionurl": "#", "indexcollapsed": 0, + "component": null, "current": 1, "visible": 1, "hasrestrictions": 0, @@ -56,7 +57,7 @@ } }}

Date: Fri, 5 Jul 2024 16:59:55 +0100 Subject: [PATCH 034/178] MDL-82401 quiz: editing modals must be removed on close --- mod/quiz/amd/build/add_question_modal.min.js | 2 +- .../amd/build/add_question_modal.min.js.map | 2 +- .../build/modal_add_random_question.min.js | 2 +- .../modal_add_random_question.min.js.map | 2 +- mod/quiz/amd/src/add_question_modal.js | 1 + mod/quiz/amd/src/modal_add_random_question.js | 2 ++ .../editing_add_from_question_bank.feature | 20 +++++++++++++++++++ .../tests/behat/editing_add_random.feature | 12 +++++++++++ 8 files changed, 39 insertions(+), 4 deletions(-) diff --git a/mod/quiz/amd/build/add_question_modal.min.js b/mod/quiz/amd/build/add_question_modal.min.js index 57e67301cd28c..c565b06c23ac7 100644 --- a/mod/quiz/amd/build/add_question_modal.min.js +++ b/mod/quiz/amd/build/add_question_modal.min.js @@ -5,6 +5,6 @@ define("mod_quiz/add_question_modal",["exports","core/modal"],(function(_exports * @module mod_quiz/add_question_modal * @copyright 2023 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=(obj=_modal)&&obj.__esModule?obj:{default:obj};class AddQuestionModal extends _modal.default{configure(modalConfig){modalConfig.large=!0,modalConfig.show=!0,this.setContextId(modalConfig.contextId),this.setAddOnPageId(modalConfig.addOnPage),super.configure(modalConfig)}constructor(root){super(root),this.contextId=null,this.addOnPageId=null}setContextId(id){this.contextId=id}getContextId(){return this.contextId}setAddOnPageId(id){this.addOnPageId=id}getAddOnPageId(){return this.addOnPageId}}return _exports.default=AddQuestionModal,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=(obj=_modal)&&obj.__esModule?obj:{default:obj};class AddQuestionModal extends _modal.default{configure(modalConfig){modalConfig.large=!0,modalConfig.show=!0,modalConfig.removeOnClose=!0,this.setContextId(modalConfig.contextId),this.setAddOnPageId(modalConfig.addOnPage),super.configure(modalConfig)}constructor(root){super(root),this.contextId=null,this.addOnPageId=null}setContextId(id){this.contextId=id}getContextId(){return this.contextId}setAddOnPageId(id){this.addOnPageId=id}getAddOnPageId(){return this.addOnPageId}}return _exports.default=AddQuestionModal,_exports.default})); //# sourceMappingURL=add_question_modal.min.js.map \ No newline at end of file diff --git a/mod/quiz/amd/build/add_question_modal.min.js.map b/mod/quiz/amd/build/add_question_modal.min.js.map index 8f2082971fdb1..a907164be0e23 100644 --- a/mod/quiz/amd/build/add_question_modal.min.js.map +++ b/mod/quiz/amd/build/add_question_modal.min.js.map @@ -1 +1 @@ -{"version":3,"file":"add_question_modal.min.js","sources":["../src/add_question_modal.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 * Contain the logic for the add random question modal.\n *\n * @module mod_quiz/add_question_modal\n * @copyright 2023 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from 'core/modal';\n\nexport default class AddQuestionModal extends Modal {\n configure(modalConfig) {\n // Add question modals are always large.\n modalConfig.large = true;\n\n // Always show on creation.\n modalConfig.show = true;\n\n // Apply question modal configuration.\n this.setContextId(modalConfig.contextId);\n this.setAddOnPageId(modalConfig.addOnPage);\n\n // Apply standard configuration.\n super.configure(modalConfig);\n }\n\n constructor(root) {\n super(root);\n\n this.contextId = null;\n this.addOnPageId = null;\n }\n\n /**\n * Save the Moodle context id that the question bank is being\n * rendered in.\n *\n * @method setContextId\n * @param {Number} id\n */\n setContextId(id) {\n this.contextId = id;\n }\n\n /**\n * Retrieve the saved Moodle context id.\n *\n * @method getContextId\n * @return {Number}\n */\n getContextId() {\n return this.contextId;\n }\n\n /**\n * Set the id of the page that the question should be added to\n * when the user clicks the add to quiz link.\n *\n * @method setAddOnPageId\n * @param {Number} id\n */\n setAddOnPageId(id) {\n this.addOnPageId = id;\n }\n\n /**\n * Returns the saved page id for the question to be added to.\n *\n * @method getAddOnPageId\n * @return {Number}\n */\n getAddOnPageId() {\n return this.addOnPageId;\n }\n\n}\n"],"names":["AddQuestionModal","Modal","configure","modalConfig","large","show","setContextId","contextId","setAddOnPageId","addOnPage","constructor","root","addOnPageId","id","getContextId","this","getAddOnPageId"],"mappings":";;;;;;;iJAyBqBA,yBAAyBC,eAC1CC,UAAUC,aAENA,YAAYC,OAAQ,EAGpBD,YAAYE,MAAO,OAGdC,aAAaH,YAAYI,gBACzBC,eAAeL,YAAYM,iBAG1BP,UAAUC,aAGpBO,YAAYC,YACFA,WAEDJ,UAAY,UACZK,YAAc,KAUvBN,aAAaO,SACJN,UAAYM,GASrBC,sBACWC,KAAKR,UAUhBC,eAAeK,SACND,YAAcC,GASvBG,wBACWD,KAAKH"} \ No newline at end of file +{"version":3,"file":"add_question_modal.min.js","sources":["../src/add_question_modal.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 * Contain the logic for the add random question modal.\n *\n * @module mod_quiz/add_question_modal\n * @copyright 2023 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from 'core/modal';\n\nexport default class AddQuestionModal extends Modal {\n configure(modalConfig) {\n // Add question modals are always large.\n modalConfig.large = true;\n\n // Always show on creation.\n modalConfig.show = true;\n modalConfig.removeOnClose = true;\n\n // Apply question modal configuration.\n this.setContextId(modalConfig.contextId);\n this.setAddOnPageId(modalConfig.addOnPage);\n\n // Apply standard configuration.\n super.configure(modalConfig);\n }\n\n constructor(root) {\n super(root);\n\n this.contextId = null;\n this.addOnPageId = null;\n }\n\n /**\n * Save the Moodle context id that the question bank is being\n * rendered in.\n *\n * @method setContextId\n * @param {Number} id\n */\n setContextId(id) {\n this.contextId = id;\n }\n\n /**\n * Retrieve the saved Moodle context id.\n *\n * @method getContextId\n * @return {Number}\n */\n getContextId() {\n return this.contextId;\n }\n\n /**\n * Set the id of the page that the question should be added to\n * when the user clicks the add to quiz link.\n *\n * @method setAddOnPageId\n * @param {Number} id\n */\n setAddOnPageId(id) {\n this.addOnPageId = id;\n }\n\n /**\n * Returns the saved page id for the question to be added to.\n *\n * @method getAddOnPageId\n * @return {Number}\n */\n getAddOnPageId() {\n return this.addOnPageId;\n }\n\n}\n"],"names":["AddQuestionModal","Modal","configure","modalConfig","large","show","removeOnClose","setContextId","contextId","setAddOnPageId","addOnPage","constructor","root","addOnPageId","id","getContextId","this","getAddOnPageId"],"mappings":";;;;;;;iJAyBqBA,yBAAyBC,eAC1CC,UAAUC,aAENA,YAAYC,OAAQ,EAGpBD,YAAYE,MAAO,EACnBF,YAAYG,eAAgB,OAGvBC,aAAaJ,YAAYK,gBACzBC,eAAeN,YAAYO,iBAG1BR,UAAUC,aAGpBQ,YAAYC,YACFA,WAEDJ,UAAY,UACZK,YAAc,KAUvBN,aAAaO,SACJN,UAAYM,GASrBC,sBACWC,KAAKR,UAUhBC,eAAeK,SACND,YAAcC,GASvBG,wBACWD,KAAKH"} \ No newline at end of file diff --git a/mod/quiz/amd/build/modal_add_random_question.min.js b/mod/quiz/amd/build/modal_add_random_question.min.js index af4c41fad09c2..2a96535c73087 100644 --- a/mod/quiz/amd/build/modal_add_random_question.min.js +++ b/mod/quiz/amd/build/modal_add_random_question.min.js @@ -1,3 +1,3 @@ -define("mod_quiz/modal_add_random_question",["exports","jquery","./add_question_modal","core/notification","core/fragment","core/templates","core_form/changechecker","core/ajax","core/pending"],(function(_exports,_jquery,_add_question_modal,Notification,Fragment,Templates,FormChangeChecker,_ajax,_pending){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}}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.default=void 0,_jquery=_interopRequireDefault(_jquery),_add_question_modal=_interopRequireDefault(_add_question_modal),Notification=_interopRequireWildcard(Notification),Fragment=_interopRequireWildcard(Fragment),Templates=_interopRequireWildcard(Templates),FormChangeChecker=_interopRequireWildcard(FormChangeChecker),_pending=_interopRequireDefault(_pending);const SELECTORS={EXISTING_CATEGORY_CONTAINER:'[data-region="existing-category-container"]',EXISTING_CATEGORY_TAB:"#id_existingcategoryheader",NEW_CATEGORY_CONTAINER:'[data-region="new-category-container"]',NEW_CATEGORY_TAB:"#id_newcategoryheader",TAB_CONTENT:'[data-region="tab-content"]',ADD_ON_PAGE_FORM_ELEMENT:'[name="addonpage"]',ADD_RANDOM_BUTTON:'input[type="submit"][name="addrandom"]',ADD_NEW_CATEGORY_BUTTON:'input[type="submit"][name="newcategory"]',SUBMIT_BUTTON_ELEMENT:'input[type="submit"][name="addrandom"], input[type="submit"][name="newcategory"]',FORM_HEADER:"legend",SELECT_NUMBER_TO_ADD:"#menurandomcount",NEW_CATEGORY_ELEMENT:"#categoryname",PARENT_CATEGORY_ELEMENT:"#parentcategory",FILTER_CONDITION_ELEMENT:"[data-filtercondition]",FORM_ELEMENT:"#add_random_question_form",MESSAGE_INPUT:'[name="message"]'};class ModalAddRandomQuestion extends _add_question_modal.default{static init(contextId,category,returnUrl,cmid){let showNewCategory=!(arguments.length>4&&void 0!==arguments[4])||arguments[4];document.addEventListener("click",(e=>{const trigger=e.target.closest('.menu [data-action="addarandomquestion"]');trigger&&(e.preventDefault(),ModalAddRandomQuestion.create({contextId:contextId,category:category,returnUrl:returnUrl,cmid:cmid,title:trigger.dataset.header,addOnPage:trigger.dataset.addonpage,templateContext:{hidden:showNewCategory}}))}))}constructor(root){super(root),this.category=null,this.returnUrl=null,this.cmid=null,this.loadedForm=!1}configure(modalConfig){this.setCategory(modalConfig.category),this.setReturnUrl(modalConfig.returnUrl),this.setCMID(modalConfig.cmid),super.configure(modalConfig)}setAddOnPageId(id){super.setAddOnPageId(id),this.getBody().find(SELECTORS.ADD_ON_PAGE_FORM_ELEMENT).val(id)}setCategory(category){this.category=category}getCategory(){return this.category}setReturnUrl(url){this.returnUrl=url}getReturnUrl(){return this.returnUrl}setCMID(id){this.cmid=id}getCMID(){return this.cmid}moveContentIntoTab(tabContent,tabElement){tabContent.find(SELECTORS.FORM_HEADER).addClass("hidden"),tabContent.wrap(tabElement)}moveTabsIntoTabContent(form){const tabContent=this.getBody().find(SELECTORS.TAB_CONTENT).empty();form.find('[role="tabpanel"]').wrapAll(tabContent)}moveCancelButtonToTabs(form){const cancelButton=form.find(SELECTORS.CANCEL_BUTTON_ELEMENT).addClass("ml-1"),tabFooters=form.find('[data-region="footer"]');cancelButton.closest(SELECTORS.BUTTON_CONTAINER).remove(),cancelButton.clone().appendTo(tabFooters)}loadForm(){const cmid=this.getCMID(),cat=this.getCategory(),addonpage=this.getAddOnPageId(),returnurl=this.getReturnUrl();return Fragment.loadFragment("mod_quiz","add_random_question_form",this.getContextId(),{addonpage:addonpage,cat:cat,returnurl:returnurl,cmid:cmid}).then(((html,js)=>{const form=(0,_jquery.default)(html),existingCategoryTabContent=form.find(SELECTORS.EXISTING_CATEGORY_TAB),existingCategoryTab=this.getBody().find(SELECTORS.EXISTING_CATEGORY_CONTAINER),newCategoryTabContent=form.find(SELECTORS.NEW_CATEGORY_TAB),newCategoryTab=this.getBody().find(SELECTORS.NEW_CATEGORY_CONTAINER);this.moveContentIntoTab(existingCategoryTabContent,existingCategoryTab),this.moveContentIntoTab(newCategoryTabContent,newCategoryTab),this.moveTabsIntoTabContent(form),Templates.replaceNode(this.getBody().find(SELECTORS.TAB_CONTENT),form,js)})).then((()=>{FormChangeChecker.disableAllChecks(),this.getBody()[0].addEventListener("click",(e=>{if(!e.target.closest(SELECTORS.SUBMIT_BUTTON_ELEMENT))return;e.preventDefault();if(e.target.closest(SELECTORS.ADD_RANDOM_BUTTON)){var _document$querySelect;const randomcount=document.querySelector(SELECTORS.SELECT_NUMBER_TO_ADD).value,filtercondition=null===(_document$querySelect=document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset)||void 0===_document$querySelect?void 0:_document$querySelect.filtercondition;return void this.addQuestions(cmid,addonpage,randomcount,filtercondition,"","")}e.target.closest(SELECTORS.ADD_NEW_CATEGORY_BUTTON)&&this.addQuestions(cmid,addonpage,1,"",document.querySelector(SELECTORS.NEW_CATEGORY_ELEMENT).value,document.querySelector(SELECTORS.PARENT_CATEGORY_ELEMENT).value)}))})).catch(Notification.exception)}async addQuestions(cmid,addonpage,randomcount,filtercondition,newcategory,parentcategory){new _pending.default("mod-quiz/modal_add_random_questions");const call={methodname:"mod_quiz_add_random_questions",args:{cmid:cmid,addonpage:addonpage,randomcount:randomcount,filtercondition:filtercondition,newcategory:newcategory,parentcategory:parentcategory}};try{const response=await(0,_ajax.call)([call])[0],form=document.querySelector(SELECTORS.FORM_ELEMENT);form.querySelector(SELECTORS.MESSAGE_INPUT).value=response.message,form.submit()}catch(e){Notification.exception(e)}}show(){super.show(this),this.loadedForm||(this.loadForm(window.location.search),this.loadedForm=!0)}}return _exports.default=ModalAddRandomQuestion,_defineProperty(ModalAddRandomQuestion,"TYPE","mod_quiz-quiz-add-random-question"),_defineProperty(ModalAddRandomQuestion,"TEMPLATE","mod_quiz/modal_add_random_question"),ModalAddRandomQuestion.registerModalType(),_exports.default})); +define("mod_quiz/modal_add_random_question",["exports","jquery","./add_question_modal","core/notification","core/fragment","core/templates","core_form/changechecker","core/ajax","core/pending"],(function(_exports,_jquery,_add_question_modal,Notification,Fragment,Templates,FormChangeChecker,_ajax,_pending){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}}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.default=void 0,_jquery=_interopRequireDefault(_jquery),_add_question_modal=_interopRequireDefault(_add_question_modal),Notification=_interopRequireWildcard(Notification),Fragment=_interopRequireWildcard(Fragment),Templates=_interopRequireWildcard(Templates),FormChangeChecker=_interopRequireWildcard(FormChangeChecker),_pending=_interopRequireDefault(_pending);const SELECTORS={EXISTING_CATEGORY_CONTAINER:'[data-region="existing-category-container"]',EXISTING_CATEGORY_TAB:"#id_existingcategoryheader",NEW_CATEGORY_CONTAINER:'[data-region="new-category-container"]',NEW_CATEGORY_TAB:"#id_newcategoryheader",TAB_CONTENT:'[data-region="tab-content"]',ADD_ON_PAGE_FORM_ELEMENT:'[name="addonpage"]',ADD_RANDOM_BUTTON:'input[type="submit"][name="addrandom"]',ADD_NEW_CATEGORY_BUTTON:'input[type="submit"][name="newcategory"]',SUBMIT_BUTTON_ELEMENT:'input[type="submit"][name="addrandom"], input[type="submit"][name="newcategory"]',FORM_HEADER:"legend",SELECT_NUMBER_TO_ADD:"#menurandomcount",NEW_CATEGORY_ELEMENT:"#categoryname",PARENT_CATEGORY_ELEMENT:"#parentcategory",FILTER_CONDITION_ELEMENT:"[data-filtercondition]",FORM_ELEMENT:"#add_random_question_form",MESSAGE_INPUT:'[name="message"]'};class ModalAddRandomQuestion extends _add_question_modal.default{static init(contextId,category,returnUrl,cmid){let showNewCategory=!(arguments.length>4&&void 0!==arguments[4])||arguments[4];document.addEventListener("click",(e=>{const trigger=e.target.closest('.menu [data-action="addarandomquestion"]');trigger&&(e.preventDefault(),ModalAddRandomQuestion.create({contextId:contextId,category:category,returnUrl:returnUrl,cmid:cmid,title:trigger.dataset.header,addOnPage:trigger.dataset.addonpage,templateContext:{hidden:showNewCategory}}))}))}constructor(root){super(root),this.category=null,this.returnUrl=null,this.cmid=null,this.loadedForm=!1}configure(modalConfig){modalConfig.removeOnClose=!0,this.setCategory(modalConfig.category),this.setReturnUrl(modalConfig.returnUrl),this.setCMID(modalConfig.cmid),super.configure(modalConfig)}setAddOnPageId(id){super.setAddOnPageId(id),this.getBody().find(SELECTORS.ADD_ON_PAGE_FORM_ELEMENT).val(id)}setCategory(category){this.category=category}getCategory(){return this.category}setReturnUrl(url){this.returnUrl=url}getReturnUrl(){return this.returnUrl}setCMID(id){this.cmid=id}getCMID(){return this.cmid}moveContentIntoTab(tabContent,tabElement){tabContent.find(SELECTORS.FORM_HEADER).addClass("hidden"),tabContent.wrap(tabElement)}moveTabsIntoTabContent(form){const tabContent=this.getBody().find(SELECTORS.TAB_CONTENT).empty();form.find('[role="tabpanel"]').wrapAll(tabContent)}moveCancelButtonToTabs(form){const cancelButton=form.find(SELECTORS.CANCEL_BUTTON_ELEMENT).addClass("ml-1"),tabFooters=form.find('[data-region="footer"]');cancelButton.closest(SELECTORS.BUTTON_CONTAINER).remove(),cancelButton.clone().appendTo(tabFooters)}loadForm(){const cmid=this.getCMID(),cat=this.getCategory(),addonpage=this.getAddOnPageId(),returnurl=this.getReturnUrl();return Fragment.loadFragment("mod_quiz","add_random_question_form",this.getContextId(),{addonpage:addonpage,cat:cat,returnurl:returnurl,cmid:cmid}).then(((html,js)=>{const form=(0,_jquery.default)(html),existingCategoryTabContent=form.find(SELECTORS.EXISTING_CATEGORY_TAB),existingCategoryTab=this.getBody().find(SELECTORS.EXISTING_CATEGORY_CONTAINER),newCategoryTabContent=form.find(SELECTORS.NEW_CATEGORY_TAB),newCategoryTab=this.getBody().find(SELECTORS.NEW_CATEGORY_CONTAINER);this.moveContentIntoTab(existingCategoryTabContent,existingCategoryTab),this.moveContentIntoTab(newCategoryTabContent,newCategoryTab),this.moveTabsIntoTabContent(form),Templates.replaceNode(this.getBody().find(SELECTORS.TAB_CONTENT),form,js)})).then((()=>{FormChangeChecker.disableAllChecks(),this.getBody()[0].addEventListener("click",(e=>{if(!e.target.closest(SELECTORS.SUBMIT_BUTTON_ELEMENT))return;e.preventDefault();if(e.target.closest(SELECTORS.ADD_RANDOM_BUTTON)){var _document$querySelect;const randomcount=document.querySelector(SELECTORS.SELECT_NUMBER_TO_ADD).value,filtercondition=null===(_document$querySelect=document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset)||void 0===_document$querySelect?void 0:_document$querySelect.filtercondition;return void this.addQuestions(cmid,addonpage,randomcount,filtercondition,"","")}e.target.closest(SELECTORS.ADD_NEW_CATEGORY_BUTTON)&&this.addQuestions(cmid,addonpage,1,"",document.querySelector(SELECTORS.NEW_CATEGORY_ELEMENT).value,document.querySelector(SELECTORS.PARENT_CATEGORY_ELEMENT).value)}))})).catch(Notification.exception)}async addQuestions(cmid,addonpage,randomcount,filtercondition,newcategory,parentcategory){new _pending.default("mod-quiz/modal_add_random_questions");const call={methodname:"mod_quiz_add_random_questions",args:{cmid:cmid,addonpage:addonpage,randomcount:randomcount,filtercondition:filtercondition,newcategory:newcategory,parentcategory:parentcategory}};try{const response=await(0,_ajax.call)([call])[0],form=document.querySelector(SELECTORS.FORM_ELEMENT);form.querySelector(SELECTORS.MESSAGE_INPUT).value=response.message,form.submit()}catch(e){Notification.exception(e)}}show(){super.show(this),this.loadedForm||(this.loadForm(window.location.search),this.loadedForm=!0)}}return _exports.default=ModalAddRandomQuestion,_defineProperty(ModalAddRandomQuestion,"TYPE","mod_quiz-quiz-add-random-question"),_defineProperty(ModalAddRandomQuestion,"TEMPLATE","mod_quiz/modal_add_random_question"),ModalAddRandomQuestion.registerModalType(),_exports.default})); //# sourceMappingURL=modal_add_random_question.min.js.map \ No newline at end of file diff --git a/mod/quiz/amd/build/modal_add_random_question.min.js.map b/mod/quiz/amd/build/modal_add_random_question.min.js.map index 6d26ea8047f39..4158d4c8226f6 100644 --- a/mod/quiz/amd/build/modal_add_random_question.min.js.map +++ b/mod/quiz/amd/build/modal_add_random_question.min.js.map @@ -1 +1 @@ -{"version":3,"file":"modal_add_random_question.min.js","sources":["../src/modal_add_random_question.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 * Contain the logic for the add random question modal.\n *\n * @module mod_quiz/modal_add_random_question\n * @copyright 2018 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Modal from './add_question_modal';\nimport * as Notification from 'core/notification';\nimport * as Fragment from 'core/fragment';\nimport * as Templates from 'core/templates';\nimport * as FormChangeChecker from 'core_form/changechecker';\nimport {call as fetchMany} from 'core/ajax';\nimport Pending from 'core/pending';\n\nconst SELECTORS = {\n EXISTING_CATEGORY_CONTAINER: '[data-region=\"existing-category-container\"]',\n EXISTING_CATEGORY_TAB: '#id_existingcategoryheader',\n NEW_CATEGORY_CONTAINER: '[data-region=\"new-category-container\"]',\n NEW_CATEGORY_TAB: '#id_newcategoryheader',\n TAB_CONTENT: '[data-region=\"tab-content\"]',\n ADD_ON_PAGE_FORM_ELEMENT: '[name=\"addonpage\"]',\n ADD_RANDOM_BUTTON: 'input[type=\"submit\"][name=\"addrandom\"]',\n ADD_NEW_CATEGORY_BUTTON: 'input[type=\"submit\"][name=\"newcategory\"]',\n SUBMIT_BUTTON_ELEMENT: 'input[type=\"submit\"][name=\"addrandom\"], input[type=\"submit\"][name=\"newcategory\"]',\n FORM_HEADER: 'legend',\n SELECT_NUMBER_TO_ADD: '#menurandomcount',\n NEW_CATEGORY_ELEMENT: '#categoryname',\n PARENT_CATEGORY_ELEMENT: '#parentcategory',\n FILTER_CONDITION_ELEMENT: '[data-filtercondition]',\n FORM_ELEMENT: '#add_random_question_form',\n MESSAGE_INPUT: '[name=\"message\"]',\n};\n\nexport default class ModalAddRandomQuestion extends Modal {\n static TYPE = 'mod_quiz-quiz-add-random-question';\n static TEMPLATE = 'mod_quiz/modal_add_random_question';\n\n /**\n * Create the add random question modal.\n *\n * @param {Number} contextId Current context id.\n * @param {string} category Category id and category context id comma separated.\n * @param {string} returnUrl URL to return to after form submission.\n * @param {Number} cmid Current course module id.\n * @param {boolean} showNewCategory Display the New category tab when selecting random questions.\n */\n static init(contextId, category, returnUrl, cmid, showNewCategory = true) {\n const selector = '.menu [data-action=\"addarandomquestion\"]';\n document.addEventListener('click', (e) => {\n const trigger = e.target.closest(selector);\n if (!trigger) {\n return;\n }\n e.preventDefault();\n\n ModalAddRandomQuestion.create({\n contextId,\n category,\n returnUrl,\n cmid,\n\n title: trigger.dataset.header,\n addOnPage: trigger.dataset.addonpage,\n\n templateContext: {\n hidden: showNewCategory,\n },\n });\n });\n }\n\n /**\n * Constructor for the Modal.\n *\n * @param {object} root The root jQuery element for the modal\n */\n constructor(root) {\n super(root);\n this.category = null;\n this.returnUrl = null;\n this.cmid = null;\n this.loadedForm = false;\n }\n\n configure(modalConfig) {\n this.setCategory(modalConfig.category);\n this.setReturnUrl(modalConfig.returnUrl);\n this.setCMID(modalConfig.cmid);\n\n super.configure(modalConfig);\n }\n\n /**\n * Set the id of the page that the question should be added to\n * when the user clicks the add to quiz link.\n *\n * @method setAddOnPageId\n * @param {int} id\n */\n setAddOnPageId(id) {\n super.setAddOnPageId(id);\n this.getBody().find(SELECTORS.ADD_ON_PAGE_FORM_ELEMENT).val(id);\n }\n\n /**\n * Set the category for this form. The category is a comma separated\n * category id and category context id.\n *\n * @method setCategory\n * @param {string} category\n */\n setCategory(category) {\n this.category = category;\n }\n\n /**\n * Returns the saved category.\n *\n * @method getCategory\n * @return {string}\n */\n getCategory() {\n return this.category;\n }\n\n /**\n * Set the return URL for the form.\n *\n * @method setReturnUrl\n * @param {string} url\n */\n setReturnUrl(url) {\n this.returnUrl = url;\n }\n\n /**\n * Returns the return URL for the form.\n *\n * @method getReturnUrl\n * @return {string}\n */\n getReturnUrl() {\n return this.returnUrl;\n }\n\n /**\n * Set the course module id for the form.\n *\n * @method setCMID\n * @param {Number} id\n */\n setCMID(id) {\n this.cmid = id;\n }\n\n /**\n * Returns the course module id for the form.\n *\n * @method getCMID\n * @return {Number}\n */\n getCMID() {\n return this.cmid;\n }\n\n /**\n * Moves a given form element inside (a child of) a given tab element.\n *\n * Hides the 'legend' (e.g. header) element of the form element because the\n * tab has the name.\n *\n * Moves the submit button into a footer element at the bottom of the form\n * element for styling purposes.\n *\n * @method moveContentIntoTab\n * @param {jquery} tabContent The form element to move into the tab.\n * @param {jquey} tabElement The tab element for the form element to move into.\n */\n moveContentIntoTab(tabContent, tabElement) {\n // Hide the header because the tabs show us which part of the form we're\n // looking at.\n tabContent.find(SELECTORS.FORM_HEADER).addClass('hidden');\n // Move the element inside a tab.\n tabContent.wrap(tabElement);\n }\n\n /**\n * Empty the tab content container and move all tabs from the form into the\n * tab container element.\n *\n * @method moveTabsIntoTabContent\n * @param {jquery} form The form element.\n */\n moveTabsIntoTabContent(form) {\n // Empty it to remove the loading icon.\n const tabContent = this.getBody().find(SELECTORS.TAB_CONTENT).empty();\n // Make sure all tabs are inside the tab content element.\n form.find('[role=\"tabpanel\"]').wrapAll(tabContent);\n }\n\n /**\n * Make sure all of the tabs have a cancel button in their fotter to sit along\n * side the submit button.\n *\n * @method moveCancelButtonToTabs\n * @param {jquey} form The form element.\n */\n moveCancelButtonToTabs(form) {\n const cancelButton = form.find(SELECTORS.CANCEL_BUTTON_ELEMENT).addClass('ml-1');\n const tabFooters = form.find('[data-region=\"footer\"]');\n // Remove the buttons container element.\n cancelButton.closest(SELECTORS.BUTTON_CONTAINER).remove();\n cancelButton.clone().appendTo(tabFooters);\n }\n\n /**\n * Load the add random question form in a fragement and perform some transformation\n * on the HTML to convert it into tabs for rendering in the modal.\n *\n * @method loadForm\n * @return {promise} Resolved with form HTML and JS.\n */\n loadForm() {\n const cmid = this.getCMID();\n const cat = this.getCategory();\n const addonpage = this.getAddOnPageId();\n const returnurl = this.getReturnUrl();\n\n return Fragment.loadFragment(\n 'mod_quiz',\n 'add_random_question_form',\n this.getContextId(),\n {\n addonpage,\n cat,\n returnurl,\n cmid,\n }\n )\n .then((html, js) =>{\n const form = $(html);\n const existingCategoryTabContent = form.find(SELECTORS.EXISTING_CATEGORY_TAB);\n const existingCategoryTab = this.getBody().find(SELECTORS.EXISTING_CATEGORY_CONTAINER);\n const newCategoryTabContent = form.find(SELECTORS.NEW_CATEGORY_TAB);\n const newCategoryTab = this.getBody().find(SELECTORS.NEW_CATEGORY_CONTAINER);\n\n // Transform the form into tabs for better rendering in the modal.\n this.moveContentIntoTab(existingCategoryTabContent, existingCategoryTab);\n this.moveContentIntoTab(newCategoryTabContent, newCategoryTab);\n this.moveTabsIntoTabContent(form);\n\n Templates.replaceNode(this.getBody().find(SELECTORS.TAB_CONTENT), form, js);\n return;\n })\n .then(() => {\n // Make sure the form change checker is disabled otherwise it'll stop the user from navigating away from the\n // page once the modal is hidden.\n FormChangeChecker.disableAllChecks();\n\n // Add question to quiz.\n this.getBody()[0].addEventListener('click', (e) => {\n const button = e.target.closest(SELECTORS.SUBMIT_BUTTON_ELEMENT);\n if (!button) {\n return;\n }\n e.preventDefault();\n\n // Add Random questions if the add random button was clicked.\n const addRandomButton = e.target.closest(SELECTORS.ADD_RANDOM_BUTTON);\n if (addRandomButton) {\n const randomcount = document.querySelector(SELECTORS.SELECT_NUMBER_TO_ADD).value;\n const filtercondition = document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset?.filtercondition;\n\n this.addQuestions(cmid, addonpage, randomcount, filtercondition, '', '');\n return;\n }\n // Add new category if the add category button was clicked.\n const addCategoryButton = e.target.closest(SELECTORS.ADD_NEW_CATEGORY_BUTTON);\n if (addCategoryButton) {\n this.addQuestions(\n cmid,\n addonpage,\n 1,\n '',\n document.querySelector(SELECTORS.NEW_CATEGORY_ELEMENT).value,\n document.querySelector(SELECTORS.PARENT_CATEGORY_ELEMENT).value\n );\n return;\n }\n });\n })\n .catch(Notification.exception);\n }\n\n /**\n * Call web service function to add random questions\n *\n * @param {number} cmid course module id\n * @param {number} addonpage the page where random questions will be added to\n * @param {number} randomcount Number of random questions\n * @param {string} filtercondition Filter condition\n * @param {string} newcategory add new category\n * @param {string} parentcategory parent category of new category\n */\n async addQuestions(\n cmid,\n addonpage,\n randomcount,\n filtercondition,\n newcategory,\n parentcategory\n ) {\n // We do not need to resolve this Pending because the form submission will result in a page redirect.\n new Pending('mod-quiz/modal_add_random_questions');\n const call = {\n methodname: 'mod_quiz_add_random_questions',\n args: {\n cmid,\n addonpage,\n randomcount,\n filtercondition,\n newcategory,\n parentcategory,\n }\n };\n try {\n const response = await fetchMany([call])[0];\n const form = document.querySelector(SELECTORS.FORM_ELEMENT);\n const messageInput = form.querySelector(SELECTORS.MESSAGE_INPUT);\n messageInput.value = response.message;\n form.submit();\n } catch (e) {\n Notification.exception(e);\n }\n }\n\n /**\n * Override the modal show function to load the form when this modal is first\n * shown.\n *\n * @method show\n */\n show() {\n super.show(this);\n\n if (!this.loadedForm) {\n this.loadForm(window.location.search);\n this.loadedForm = true;\n }\n }\n}\n\nModalAddRandomQuestion.registerModalType();\n"],"names":["SELECTORS","EXISTING_CATEGORY_CONTAINER","EXISTING_CATEGORY_TAB","NEW_CATEGORY_CONTAINER","NEW_CATEGORY_TAB","TAB_CONTENT","ADD_ON_PAGE_FORM_ELEMENT","ADD_RANDOM_BUTTON","ADD_NEW_CATEGORY_BUTTON","SUBMIT_BUTTON_ELEMENT","FORM_HEADER","SELECT_NUMBER_TO_ADD","NEW_CATEGORY_ELEMENT","PARENT_CATEGORY_ELEMENT","FILTER_CONDITION_ELEMENT","FORM_ELEMENT","MESSAGE_INPUT","ModalAddRandomQuestion","Modal","contextId","category","returnUrl","cmid","showNewCategory","document","addEventListener","e","trigger","target","closest","preventDefault","create","title","dataset","header","addOnPage","addonpage","templateContext","hidden","constructor","root","loadedForm","configure","modalConfig","setCategory","setReturnUrl","setCMID","setAddOnPageId","id","getBody","find","val","getCategory","this","url","getReturnUrl","getCMID","moveContentIntoTab","tabContent","tabElement","addClass","wrap","moveTabsIntoTabContent","form","empty","wrapAll","moveCancelButtonToTabs","cancelButton","CANCEL_BUTTON_ELEMENT","tabFooters","BUTTON_CONTAINER","remove","clone","appendTo","loadForm","cat","getAddOnPageId","returnurl","Fragment","loadFragment","getContextId","then","html","js","existingCategoryTabContent","existingCategoryTab","newCategoryTabContent","newCategoryTab","Templates","replaceNode","FormChangeChecker","disableAllChecks","randomcount","querySelector","value","filtercondition","_document$querySelect","addQuestions","catch","Notification","exception","newcategory","parentcategory","Pending","call","methodname","args","response","message","submit","show","window","location","search","registerModalType"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"modal_add_random_question.min.js","sources":["../src/modal_add_random_question.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 * Contain the logic for the add random question modal.\n *\n * @module mod_quiz/modal_add_random_question\n * @copyright 2018 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Modal from './add_question_modal';\nimport * as Notification from 'core/notification';\nimport * as Fragment from 'core/fragment';\nimport * as Templates from 'core/templates';\nimport * as FormChangeChecker from 'core_form/changechecker';\nimport {call as fetchMany} from 'core/ajax';\nimport Pending from 'core/pending';\n\nconst SELECTORS = {\n EXISTING_CATEGORY_CONTAINER: '[data-region=\"existing-category-container\"]',\n EXISTING_CATEGORY_TAB: '#id_existingcategoryheader',\n NEW_CATEGORY_CONTAINER: '[data-region=\"new-category-container\"]',\n NEW_CATEGORY_TAB: '#id_newcategoryheader',\n TAB_CONTENT: '[data-region=\"tab-content\"]',\n ADD_ON_PAGE_FORM_ELEMENT: '[name=\"addonpage\"]',\n ADD_RANDOM_BUTTON: 'input[type=\"submit\"][name=\"addrandom\"]',\n ADD_NEW_CATEGORY_BUTTON: 'input[type=\"submit\"][name=\"newcategory\"]',\n SUBMIT_BUTTON_ELEMENT: 'input[type=\"submit\"][name=\"addrandom\"], input[type=\"submit\"][name=\"newcategory\"]',\n FORM_HEADER: 'legend',\n SELECT_NUMBER_TO_ADD: '#menurandomcount',\n NEW_CATEGORY_ELEMENT: '#categoryname',\n PARENT_CATEGORY_ELEMENT: '#parentcategory',\n FILTER_CONDITION_ELEMENT: '[data-filtercondition]',\n FORM_ELEMENT: '#add_random_question_form',\n MESSAGE_INPUT: '[name=\"message\"]',\n};\n\nexport default class ModalAddRandomQuestion extends Modal {\n static TYPE = 'mod_quiz-quiz-add-random-question';\n static TEMPLATE = 'mod_quiz/modal_add_random_question';\n\n /**\n * Create the add random question modal.\n *\n * @param {Number} contextId Current context id.\n * @param {string} category Category id and category context id comma separated.\n * @param {string} returnUrl URL to return to after form submission.\n * @param {Number} cmid Current course module id.\n * @param {boolean} showNewCategory Display the New category tab when selecting random questions.\n */\n static init(contextId, category, returnUrl, cmid, showNewCategory = true) {\n const selector = '.menu [data-action=\"addarandomquestion\"]';\n document.addEventListener('click', (e) => {\n const trigger = e.target.closest(selector);\n if (!trigger) {\n return;\n }\n e.preventDefault();\n\n ModalAddRandomQuestion.create({\n contextId,\n category,\n returnUrl,\n cmid,\n\n title: trigger.dataset.header,\n addOnPage: trigger.dataset.addonpage,\n\n templateContext: {\n hidden: showNewCategory,\n },\n });\n });\n }\n\n /**\n * Constructor for the Modal.\n *\n * @param {object} root The root jQuery element for the modal\n */\n constructor(root) {\n super(root);\n this.category = null;\n this.returnUrl = null;\n this.cmid = null;\n this.loadedForm = false;\n }\n\n configure(modalConfig) {\n modalConfig.removeOnClose = true;\n\n this.setCategory(modalConfig.category);\n this.setReturnUrl(modalConfig.returnUrl);\n this.setCMID(modalConfig.cmid);\n\n super.configure(modalConfig);\n }\n\n /**\n * Set the id of the page that the question should be added to\n * when the user clicks the add to quiz link.\n *\n * @method setAddOnPageId\n * @param {int} id\n */\n setAddOnPageId(id) {\n super.setAddOnPageId(id);\n this.getBody().find(SELECTORS.ADD_ON_PAGE_FORM_ELEMENT).val(id);\n }\n\n /**\n * Set the category for this form. The category is a comma separated\n * category id and category context id.\n *\n * @method setCategory\n * @param {string} category\n */\n setCategory(category) {\n this.category = category;\n }\n\n /**\n * Returns the saved category.\n *\n * @method getCategory\n * @return {string}\n */\n getCategory() {\n return this.category;\n }\n\n /**\n * Set the return URL for the form.\n *\n * @method setReturnUrl\n * @param {string} url\n */\n setReturnUrl(url) {\n this.returnUrl = url;\n }\n\n /**\n * Returns the return URL for the form.\n *\n * @method getReturnUrl\n * @return {string}\n */\n getReturnUrl() {\n return this.returnUrl;\n }\n\n /**\n * Set the course module id for the form.\n *\n * @method setCMID\n * @param {Number} id\n */\n setCMID(id) {\n this.cmid = id;\n }\n\n /**\n * Returns the course module id for the form.\n *\n * @method getCMID\n * @return {Number}\n */\n getCMID() {\n return this.cmid;\n }\n\n /**\n * Moves a given form element inside (a child of) a given tab element.\n *\n * Hides the 'legend' (e.g. header) element of the form element because the\n * tab has the name.\n *\n * Moves the submit button into a footer element at the bottom of the form\n * element for styling purposes.\n *\n * @method moveContentIntoTab\n * @param {jquery} tabContent The form element to move into the tab.\n * @param {jquey} tabElement The tab element for the form element to move into.\n */\n moveContentIntoTab(tabContent, tabElement) {\n // Hide the header because the tabs show us which part of the form we're\n // looking at.\n tabContent.find(SELECTORS.FORM_HEADER).addClass('hidden');\n // Move the element inside a tab.\n tabContent.wrap(tabElement);\n }\n\n /**\n * Empty the tab content container and move all tabs from the form into the\n * tab container element.\n *\n * @method moveTabsIntoTabContent\n * @param {jquery} form The form element.\n */\n moveTabsIntoTabContent(form) {\n // Empty it to remove the loading icon.\n const tabContent = this.getBody().find(SELECTORS.TAB_CONTENT).empty();\n // Make sure all tabs are inside the tab content element.\n form.find('[role=\"tabpanel\"]').wrapAll(tabContent);\n }\n\n /**\n * Make sure all of the tabs have a cancel button in their fotter to sit along\n * side the submit button.\n *\n * @method moveCancelButtonToTabs\n * @param {jquey} form The form element.\n */\n moveCancelButtonToTabs(form) {\n const cancelButton = form.find(SELECTORS.CANCEL_BUTTON_ELEMENT).addClass('ml-1');\n const tabFooters = form.find('[data-region=\"footer\"]');\n // Remove the buttons container element.\n cancelButton.closest(SELECTORS.BUTTON_CONTAINER).remove();\n cancelButton.clone().appendTo(tabFooters);\n }\n\n /**\n * Load the add random question form in a fragement and perform some transformation\n * on the HTML to convert it into tabs for rendering in the modal.\n *\n * @method loadForm\n * @return {promise} Resolved with form HTML and JS.\n */\n loadForm() {\n const cmid = this.getCMID();\n const cat = this.getCategory();\n const addonpage = this.getAddOnPageId();\n const returnurl = this.getReturnUrl();\n\n return Fragment.loadFragment(\n 'mod_quiz',\n 'add_random_question_form',\n this.getContextId(),\n {\n addonpage,\n cat,\n returnurl,\n cmid,\n }\n )\n .then((html, js) =>{\n const form = $(html);\n const existingCategoryTabContent = form.find(SELECTORS.EXISTING_CATEGORY_TAB);\n const existingCategoryTab = this.getBody().find(SELECTORS.EXISTING_CATEGORY_CONTAINER);\n const newCategoryTabContent = form.find(SELECTORS.NEW_CATEGORY_TAB);\n const newCategoryTab = this.getBody().find(SELECTORS.NEW_CATEGORY_CONTAINER);\n\n // Transform the form into tabs for better rendering in the modal.\n this.moveContentIntoTab(existingCategoryTabContent, existingCategoryTab);\n this.moveContentIntoTab(newCategoryTabContent, newCategoryTab);\n this.moveTabsIntoTabContent(form);\n\n Templates.replaceNode(this.getBody().find(SELECTORS.TAB_CONTENT), form, js);\n return;\n })\n .then(() => {\n // Make sure the form change checker is disabled otherwise it'll stop the user from navigating away from the\n // page once the modal is hidden.\n FormChangeChecker.disableAllChecks();\n\n // Add question to quiz.\n this.getBody()[0].addEventListener('click', (e) => {\n const button = e.target.closest(SELECTORS.SUBMIT_BUTTON_ELEMENT);\n if (!button) {\n return;\n }\n e.preventDefault();\n\n // Add Random questions if the add random button was clicked.\n const addRandomButton = e.target.closest(SELECTORS.ADD_RANDOM_BUTTON);\n if (addRandomButton) {\n const randomcount = document.querySelector(SELECTORS.SELECT_NUMBER_TO_ADD).value;\n const filtercondition = document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset?.filtercondition;\n\n this.addQuestions(cmid, addonpage, randomcount, filtercondition, '', '');\n return;\n }\n // Add new category if the add category button was clicked.\n const addCategoryButton = e.target.closest(SELECTORS.ADD_NEW_CATEGORY_BUTTON);\n if (addCategoryButton) {\n this.addQuestions(\n cmid,\n addonpage,\n 1,\n '',\n document.querySelector(SELECTORS.NEW_CATEGORY_ELEMENT).value,\n document.querySelector(SELECTORS.PARENT_CATEGORY_ELEMENT).value\n );\n return;\n }\n });\n })\n .catch(Notification.exception);\n }\n\n /**\n * Call web service function to add random questions\n *\n * @param {number} cmid course module id\n * @param {number} addonpage the page where random questions will be added to\n * @param {number} randomcount Number of random questions\n * @param {string} filtercondition Filter condition\n * @param {string} newcategory add new category\n * @param {string} parentcategory parent category of new category\n */\n async addQuestions(\n cmid,\n addonpage,\n randomcount,\n filtercondition,\n newcategory,\n parentcategory\n ) {\n // We do not need to resolve this Pending because the form submission will result in a page redirect.\n new Pending('mod-quiz/modal_add_random_questions');\n const call = {\n methodname: 'mod_quiz_add_random_questions',\n args: {\n cmid,\n addonpage,\n randomcount,\n filtercondition,\n newcategory,\n parentcategory,\n }\n };\n try {\n const response = await fetchMany([call])[0];\n const form = document.querySelector(SELECTORS.FORM_ELEMENT);\n const messageInput = form.querySelector(SELECTORS.MESSAGE_INPUT);\n messageInput.value = response.message;\n form.submit();\n } catch (e) {\n Notification.exception(e);\n }\n }\n\n /**\n * Override the modal show function to load the form when this modal is first\n * shown.\n *\n * @method show\n */\n show() {\n super.show(this);\n\n if (!this.loadedForm) {\n this.loadForm(window.location.search);\n this.loadedForm = true;\n }\n }\n}\n\nModalAddRandomQuestion.registerModalType();\n"],"names":["SELECTORS","EXISTING_CATEGORY_CONTAINER","EXISTING_CATEGORY_TAB","NEW_CATEGORY_CONTAINER","NEW_CATEGORY_TAB","TAB_CONTENT","ADD_ON_PAGE_FORM_ELEMENT","ADD_RANDOM_BUTTON","ADD_NEW_CATEGORY_BUTTON","SUBMIT_BUTTON_ELEMENT","FORM_HEADER","SELECT_NUMBER_TO_ADD","NEW_CATEGORY_ELEMENT","PARENT_CATEGORY_ELEMENT","FILTER_CONDITION_ELEMENT","FORM_ELEMENT","MESSAGE_INPUT","ModalAddRandomQuestion","Modal","contextId","category","returnUrl","cmid","showNewCategory","document","addEventListener","e","trigger","target","closest","preventDefault","create","title","dataset","header","addOnPage","addonpage","templateContext","hidden","constructor","root","loadedForm","configure","modalConfig","removeOnClose","setCategory","setReturnUrl","setCMID","setAddOnPageId","id","getBody","find","val","getCategory","this","url","getReturnUrl","getCMID","moveContentIntoTab","tabContent","tabElement","addClass","wrap","moveTabsIntoTabContent","form","empty","wrapAll","moveCancelButtonToTabs","cancelButton","CANCEL_BUTTON_ELEMENT","tabFooters","BUTTON_CONTAINER","remove","clone","appendTo","loadForm","cat","getAddOnPageId","returnurl","Fragment","loadFragment","getContextId","then","html","js","existingCategoryTabContent","existingCategoryTab","newCategoryTabContent","newCategoryTab","Templates","replaceNode","FormChangeChecker","disableAllChecks","randomcount","querySelector","value","filtercondition","_document$querySelect","addQuestions","catch","Notification","exception","newcategory","parentcategory","Pending","call","methodname","args","response","message","submit","show","window","location","search","registerModalType"],"mappings":"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"} \ No newline at end of file diff --git a/mod/quiz/amd/src/add_question_modal.js b/mod/quiz/amd/src/add_question_modal.js index 2947d7cf9dd06..466883dcbd4b6 100644 --- a/mod/quiz/amd/src/add_question_modal.js +++ b/mod/quiz/amd/src/add_question_modal.js @@ -30,6 +30,7 @@ export default class AddQuestionModal extends Modal { // Always show on creation. modalConfig.show = true; + modalConfig.removeOnClose = true; // Apply question modal configuration. this.setContextId(modalConfig.contextId); diff --git a/mod/quiz/amd/src/modal_add_random_question.js b/mod/quiz/amd/src/modal_add_random_question.js index 151e81316d6b7..2f8e8ea250f2b 100644 --- a/mod/quiz/amd/src/modal_add_random_question.js +++ b/mod/quiz/amd/src/modal_add_random_question.js @@ -101,6 +101,8 @@ export default class ModalAddRandomQuestion extends Modal { } configure(modalConfig) { + modalConfig.removeOnClose = true; + this.setCategory(modalConfig.category); this.setReturnUrl(modalConfig.returnUrl); this.setCMID(modalConfig.cmid); diff --git a/mod/quiz/tests/behat/editing_add_from_question_bank.feature b/mod/quiz/tests/behat/editing_add_from_question_bank.feature index 096f30995f255..e5671404278e4 100644 --- a/mod/quiz/tests/behat/editing_add_from_question_bank.feature +++ b/mod/quiz/tests/behat/editing_add_from_question_bank.feature @@ -81,6 +81,26 @@ Feature: Adding questions to a quiz from the question bank And I should see "Feature question 5" And I should see "Feature question 9" + Scenario: After closing and reopening the modal, it still works + Given the following "question categories" exist: + | contextlevel | reference | name | + | Course | C1 | My collection | + And the following "question" exists: + | questioncategory | My collection | + | qtype | essay | + | name | Feature question | + | questiontext | Write about topic | + | user | teacher1 | + When I am on the "Quiz 1" "mod_quiz > Edit" page logged in as teacher1 + And I open the "last" add to quiz menu + And I follow "from question bank" + And I click on "Close" "button" in the "Add from the question bank at the end" "dialogue" + And I open the "last" add to quiz menu + And I follow "from question bank" + And I set the field "Category" to "My collection" + And I press "Apply filters" + Then I should see "Feature question" + Scenario: Questions are added in the right place with multiple sections Given the following "questions" exist: | questioncategory | qtype | name | questiontext | diff --git a/mod/quiz/tests/behat/editing_add_random.feature b/mod/quiz/tests/behat/editing_add_random.feature index f62410480f5d7..7dc16b6dcb430 100644 --- a/mod/quiz/tests/behat/editing_add_random.feature +++ b/mod/quiz/tests/behat/editing_add_random.feature @@ -84,6 +84,18 @@ Feature: Adding random questions to a quiz based on category and tags And I should see "question 1 name" And I should see "\"listen\" & \"answer\"" + Scenario: After closing and reopening the modal, it still works + When I am on the "Quiz 1" "mod_quiz > Edit" page logged in as teacher1 + And I open the "last" add to quiz menu + And I follow "a random question" + And I click on "Close" "button" in the "Add a random question at the end" "dialogue" + And I open the "last" add to quiz menu + And I follow "a random question" + And I should not see "question 3 name" + And I set the field "Category" to "Subcategory" + And I press "Apply filters" + Then I should see "question 3 name" + Scenario: Teacher without moodle/question:useall should not see the add a random question menu item Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | From db0d4ab6772a1d80aecde04fd406aa932cb69b09 Mon Sep 17 00:00:00 2001 From: Stevani Andolo Date: Tue, 21 May 2024 16:04:13 +0800 Subject: [PATCH 035/178] MDL-80947 communication_customlink: Check before customlink update --- .../customlink/classes/communication_feature.php | 8 ++++++-- .../tests/communication_feature_test.php | 16 ++++++++++++++++ communication/tests/api_test.php | 2 ++ 3 files changed, 24 insertions(+), 2 deletions(-) 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/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/tests/api_test.php b/communication/tests/api_test.php index bb1eb99d25e81..173fa7f663c59 100644 --- a/communication/tests/api_test.php +++ b/communication/tests/api_test.php @@ -476,6 +476,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', From d98867dd8bde7a9f594c6e04acb58f2924fc844c Mon Sep 17 00:00:00 2001 From: meirzamoodle Date: Sun, 7 Jul 2024 13:35:32 +0700 Subject: [PATCH 036/178] MDL-66251 form: Hiding and disabling static elements. Added functionality to locate static text elements within the form, improving the ability to target and manipulate the static elements for hiding and disabling purposes. --- lib/form/form.js | 66 ++++++++++++++++- lib/form/templates/element-static.mustache | 12 ++- lib/form/tests/behat/disabledif.feature | 12 +++ .../static_hideif_disabledif_form.php | 73 +++++++++++++++++++ lib/form/tests/behat/hideif.feature | 8 ++ 5 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 lib/form/tests/behat/fixtures/static_hideif_disabledif_form.php diff --git a/lib/form/form.js b/lib/form/form.js index 0ef899f64f38a..c9cde31c89075 100644 --- a/lib/form/form.js +++ b/lib/form/form.js @@ -16,6 +16,7 @@ if (typeof M.form.dependencyManager === 'undefined') { _dirty: null, _nameCollections: null, _fileinputs: null, + _staticElements: null, _editors: null, _editorNameSuffix: '[text]', @@ -105,6 +106,14 @@ if (typeof M.form.dependencyManager === 'undefined') { allnames[name].push(node); } }); + // Locate any static elements for each name. + this.get('form').all('.form-control-static').each(function(node) { + var name = node.getData('name'); + if (({}).hasOwnProperty.call(allnames, name)) { + names[name].push(node); + allnames[name].push(node); + } + }); this._nameCollections = {names: names, allnames: allnames}; }, @@ -259,20 +268,47 @@ if (typeof M.form.dependencyManager === 'undefined') { * @param {Boolean} disabled True to disable, false to enable. */ _disableElement: function(name, disabled) { - var els = this.elementsByName(name), + const els = this.elementsByName(name), filepicker = this.isFilePicker(name), - editors = this.get('form').all('.fitem [data-fieldtype="editor"] textarea[name="' + name + '[text]"]'); + editors = this.get('form').all('.fitem [data-fieldtype="editor"] textarea[name="' + name + '[text]"]'), + staticElement = this.isStaticElement(name); els.each(function(node) { + const fitem = node.ancestor('.fitem'); if (disabled) { node.setAttribute('disabled', 'disabled'); } else { node.removeAttribute('disabled'); } - + // Enable/Disable static elements if exist. + if (staticElement) { + const disabledNonTextElements = 'INPUT,SELECT,TEXTAREA,BUTTON,A'; + if (disabled) { + // Mute the text inside the current static element. + fitem.addClass('text-muted'); + // Disabled non-text elements in the static if exist. + fitem.all(disabledNonTextElements).each(function(disabledElement) { + if (disabledElement.get('tagName').toUpperCase() === "A") { + disabledElement.addClass('disabled'); + } else { + disabledElement.setAttribute('disabled', 'disabled'); + } + }); + } else { + // Unmute the text inside the current static element. + fitem.removeClass('text-muted'); + // Enabled non-text elements in the static if exist. + fitem.all(disabledNonTextElements).each(function(disabledElement) { + if (disabledElement.get('tagName').toUpperCase() === "A") { + disabledElement.removeClass('disabled'); + } else { + disabledElement.removeAttribute('disabled', 'disabled'); + } + }); + } + } // Extra code to disable filepicker or filemanager form elements if (filepicker) { - var fitem = node.ancestor('.fitem'); if (fitem) { if (disabled) { fitem.addClass('disabled'); @@ -363,6 +399,28 @@ if (typeof M.form.dependencyManager === 'undefined') { return false; }, + /** + * Checks if a form element with the given name is static. + * + * @param {string} el - The name of the form element to check. + * @returns {boolean} - Returns true if the form element is static, otherwise false. + */ + isStaticElement: function(el) { + if (!this._staticElements) { + const staticElements = {}; + const els = this.get('form').all('.fitem [data-fieldtype="static"] .form-control-static'); + els.each(function(node) { + if (node.getData('name') === el) { + staticElements[node.getData('name')] = true; + } + }); + this._staticElements = staticElements; + } + if (({}).hasOwnProperty.call(this._staticElements, el)) { + return this._staticElements[el] || false; + } + return false; + }, _dependencyNotchecked: function(elements, value, isHide) { var lock = false; elements.each(function() { diff --git a/lib/form/templates/element-static.mustache b/lib/form/templates/element-static.mustache index 409cd95ea4ce0..e8fa064d9dd6b 100644 --- a/lib/form/templates/element-static.mustache +++ b/lib/form/templates/element-static.mustache @@ -32,12 +32,20 @@ Example context (json): { "label": "Example label", - "element": { "html": "Example HTML", "staticlabel": true } + "element": { + "html": "Example HTML", + "staticlabel": true, + "extraclasses": null, + "name": "static_name", + "id": "id_static", + "wrapperid": "fitem_id_static", + "iderror": "id_error_static" + } } }} {{< core_form/element-template }} {{$element}} -
+
{{{element.html}}}
{{/element}} diff --git a/lib/form/tests/behat/disabledif.feature b/lib/form/tests/behat/disabledif.feature index 1cf4bcee4c678..97bcdabde4caf 100644 --- a/lib/form/tests/behat/disabledif.feature +++ b/lib/form/tests/behat/disabledif.feature @@ -14,3 +14,15 @@ Feature: disabledIf functionality in forms Then the "disabled" attribute of "input#id_some_filemanager" "css_element" should contain "true" # Test file manager in a group. And the "disabled" attribute of "input#id_filemanager_group_some_filemanager_group" "css_element" should contain "true" + + Scenario: The static element is disabled when 'eq' disabledIf conditions are met + Given I am on fixture page "/lib/form/tests/behat/fixtures/static_hideif_disabledif_form.php" + And I should see "Static with form elements" + When I click on "Disable" "radio" + And the "class" attribute of "#fitem_id_some_static" "css_element" should contain "text-muted" + And the "disabled" attribute of "input#id_some_static_username" "css_element" should contain "true" + And the "disabled" attribute of "Check" "button" should contain "true" + Then I click on "Enable" "radio" + And the "class" attribute of "#fitem_id_some_static" "css_element" should not contain "text-muted" + And the "#id_some_static_username" "css_element" should be enabled + And the "class" attribute of "Check" "button" should not contain "disabled" diff --git a/lib/form/tests/behat/fixtures/static_hideif_disabledif_form.php b/lib/form/tests/behat/fixtures/static_hideif_disabledif_form.php new file mode 100644 index 0000000000000..28a2f8ad58fd3 --- /dev/null +++ b/lib/form/tests/behat/fixtures/static_hideif_disabledif_form.php @@ -0,0 +1,73 @@ +. + +require_once(__DIR__ . '/../../../../../config.php'); + +defined('BEHAT_SITE_RUNNING') || die(); + +global $CFG, $PAGE, $OUTPUT; +require_once($CFG->libdir . '/formslib.php'); +$PAGE->set_url('/lib/form/tests/behat/fixtures/static_hideif_disabledif_form.php'); +$PAGE->add_body_class('limitedwidth'); +require_login(); +$PAGE->set_context(core\context\system::instance()); + +/** + * Test class for hiding and disabling static elements. + * + * @package core_form + * @copyright Meirza + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class test_static_hideif_disabledif_form extends moodleform { + + /** + * Form definition. + */ + public function definition(): void { + $mform = $this->_form; + + // Radio buttons. + $radiogroup = [ + $mform->createElement('radio', 'some_radios', '', 'Enable', '1'), + $mform->createElement('radio', 'some_radios', '', 'Disable', '2'), + $mform->createElement('radio', 'some_radios', '', 'Hide', '3'), + ]; + + $mform->addGroup($radiogroup, 'some_radios_group', 'Enable/Disable/Hide', ' ', false); + $mform->setDefault('some_radios', 1); + + // Static element with conditions. + $mform->addElement( + 'static', + 'some_static', + 'Static element', + 'Static with
form elements + + ', + ); + $mform->disabledIf('some_static', 'some_radios', 'eq', '2'); + $mform->hideIf('some_static', 'some_radios', 'eq', '3'); + + $this->add_action_buttons(); + } +} + +$form = new test_static_hideif_disabledif_form(); + +echo $OUTPUT->header(); +$form->display(); +echo $OUTPUT->footer(); diff --git a/lib/form/tests/behat/hideif.feature b/lib/form/tests/behat/hideif.feature index 4fd4c5772f011..768c116248b88 100644 --- a/lib/form/tests/behat/hideif.feature +++ b/lib/form/tests/behat/hideif.feature @@ -31,3 +31,11 @@ Feature: hideIf functionality in forms And I should see "My test editor" When I click on "Hide" "radio" Then I should not see "My test editor" + + Scenario: The static element is hidden when 'eq' hideIf conditions are met + Given I am on fixture page "/lib/form/tests/behat/fixtures/static_hideif_disabledif_form.php" + And I should see "Static with form elements" + When I click on "Hide" "radio" + Then I should not see "Static with form elements" + And I click on "Enable" "radio" + And I should see "Static with form elements" From a3e8495ac597e7f2cfeac50740d9dedd1e166201 Mon Sep 17 00:00:00 2001 From: Angelia Dela Cruz Date: Wed, 25 Jan 2023 11:22:35 +0800 Subject: [PATCH 037/178] MDL-76654 behat: Coverage for workshop self-assessment --- .../tests/behat/self_assessment.feature | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 mod/workshop/tests/behat/self_assessment.feature diff --git a/mod/workshop/tests/behat/self_assessment.feature b/mod/workshop/tests/behat/self_assessment.feature new file mode 100644 index 0000000000000..1986ab26a1dc8 --- /dev/null +++ b/mod/workshop/tests/behat/self_assessment.feature @@ -0,0 +1,81 @@ +@mod @mod_workshop @javascript +Feature: Workshop self-assessment + In order to use workshop activity + As a student + I need to be able to add and assess my own submission + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | student1 | Sam1 | Student1 | student1@example.com | + | student2 | Sam2 | Student2 | student2@example.com | + | student3 | Sam3 | Student3 | student3@example.com | + | teacher1 | Terry1 | Teacher1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | + | Course1 | c1 | + And the following "course enrolments" exist: + | user | course | role | + | student1 | c1 | student | + | student2 | c1 | student | + | student3 | c1 | student | + | teacher1 | c1 | editingteacher | + And the following "activities" exist: + | activity | name | course | idnumber | useselfassessment | + | workshop | TestWorkshop | c1 | workshop1 | 1 | + And I am on the "TestWorkshop" "workshop activity" page logged in as teacher1 + And I change phase in workshop "TestWorkshop" to "Submission phase" + And I am on the "TestWorkshop" "workshop activity" page logged in as student1 + And I add a submission in workshop "TestWorkshop" as:" + | Title | Submission1 | + | Submission content | Some content | + And I am on the "TestWorkshop" "workshop activity" page logged in as student2 + And I add a submission in workshop "TestWorkshop" as:" + | Title | Submission2 | + | Submission content | Some content | + And I am on the "TestWorkshop" "workshop activity" page logged in as student3 + And I add a submission in workshop "TestWorkshop" as:" + | Title | Submission3 | + | Submission content | Some content | + And I am on the "TestWorkshop" "workshop activity" page logged in as teacher1 + And I click on "Submissions allocation" "link" + And I select "Random allocation" from the "jump" singleselect + And I set the following fields to these values: + | addselfassessment | 1 | + And I press "Save changes" + + Scenario: Student can assess their own submission + When I select "Manual allocation" from the "jump" singleselect + Then the "by" select box should contain "Sam1 Student1" + And the "by" select box should contain "Sam2 Student2" + And the "by" select box should contain "Sam3 Student3" + And I should see "Sam1 Student1" in the "Sam1 Student1" "table_row" + And I should see "Sam2 Student2" in the "Sam2 Student2" "table_row" + And I should see "Sam3 Student3" in the "Sam3 Student3" "table_row" + # Then the following should exist in the "allocations" table: + # | Participant is reviewed by | Participant | Participant is reviewer of | + # | Sam1 Student1 | Sam1 Student1 | Sam1 Student1 | + # | Sam2 Student2 | Sam2 Student2 | Sam2 Student2 | + # | Sam3 Student3 | Sam3 Student3 | Sam3 Student3 | + And I change phase in workshop "TestWorkshop" to "Assessment phase" + And I am on the "TestWorkshop" "workshop activity" page logged in as student1 + And I should see "Assess yourself" + And I should see "Your submission" + And I should see "Assigned submissions to assess" + And I should see "Submission1" + And I should see "by Sam1 Student1" + And the "Assess" "button" should be enabled + And I am on the "TestWorkshop" "workshop activity" page logged in as student2 + And I should see "Assess yourself" + And I should see "Your submission" + And I should see "Assigned submissions to assess" + And I should see "Submission2" + And I should see "by Sam2 Student2" + And the "Assess" "button" should be enabled + And I am on the "TestWorkshop" "workshop activity" page logged in as student3 + And I should see "Assess yourself" + And I should see "Your submission" + And I should see "Assigned submissions to assess" + And I should see "Submission3" + And I should see "by Sam3 Student3" + And the "Assess" "button" should be enabled From 8988e4228aa86a66fc1be580001ef26a30bdd8eb Mon Sep 17 00:00:00 2001 From: Simey Lameze Date: Wed, 25 Jan 2023 11:22:35 +0800 Subject: [PATCH 038/178] MDL-76654 behat: fixes and improvements to self assessment test --- .../tests/behat/self_assessment.feature | 67 ++++++++----------- 1 file changed, 28 insertions(+), 39 deletions(-) diff --git a/mod/workshop/tests/behat/self_assessment.feature b/mod/workshop/tests/behat/self_assessment.feature index 1986ab26a1dc8..21d6aa59e579b 100644 --- a/mod/workshop/tests/behat/self_assessment.feature +++ b/mod/workshop/tests/behat/self_assessment.feature @@ -1,4 +1,4 @@ -@mod @mod_workshop @javascript +@mod @mod_workshop Feature: Workshop self-assessment In order to use workshop activity As a student @@ -6,11 +6,11 @@ Feature: Workshop self-assessment Background: Given the following "users" exist: - | username | firstname | lastname | email | - | student1 | Sam1 | Student1 | student1@example.com | - | student2 | Sam2 | Student2 | student2@example.com | - | student3 | Sam3 | Student3 | student3@example.com | - | teacher1 | Terry1 | Teacher1 | teacher1@example.com | + | username | firstname | lastname | email | + | student1 | Student | One | student1@example.com | + | student2 | Student | Two | student2@example.com | + | student3 | Student | Three | student3@example.com | + | teacher1 | Teacher | One | teacher1@example.com | And the following "courses" exist: | fullname | shortname | | Course1 | c1 | @@ -24,17 +24,19 @@ Feature: Workshop self-assessment | activity | name | course | idnumber | useselfassessment | | workshop | TestWorkshop | c1 | workshop1 | 1 | And I am on the "TestWorkshop" "workshop activity" page logged in as teacher1 + And I edit assessment form in workshop "TestWorkshop" as: + | id_description__idx_0_editor | Aspect1 | And I change phase in workshop "TestWorkshop" to "Submission phase" And I am on the "TestWorkshop" "workshop activity" page logged in as student1 - And I add a submission in workshop "TestWorkshop" as:" + And I add a submission in workshop "TestWorkshop" as: | Title | Submission1 | | Submission content | Some content | And I am on the "TestWorkshop" "workshop activity" page logged in as student2 - And I add a submission in workshop "TestWorkshop" as:" + And I add a submission in workshop "TestWorkshop" as: | Title | Submission2 | | Submission content | Some content | And I am on the "TestWorkshop" "workshop activity" page logged in as student3 - And I add a submission in workshop "TestWorkshop" as:" + And I add a submission in workshop "TestWorkshop" as: | Title | Submission3 | | Submission content | Some content | And I am on the "TestWorkshop" "workshop activity" page logged in as teacher1 @@ -46,36 +48,23 @@ Feature: Workshop self-assessment Scenario: Student can assess their own submission When I select "Manual allocation" from the "jump" singleselect - Then the "by" select box should contain "Sam1 Student1" - And the "by" select box should contain "Sam2 Student2" - And the "by" select box should contain "Sam3 Student3" - And I should see "Sam1 Student1" in the "Sam1 Student1" "table_row" - And I should see "Sam2 Student2" in the "Sam2 Student2" "table_row" - And I should see "Sam3 Student3" in the "Sam3 Student3" "table_row" - # Then the following should exist in the "allocations" table: - # | Participant is reviewed by | Participant | Participant is reviewer of | - # | Sam1 Student1 | Sam1 Student1 | Sam1 Student1 | - # | Sam2 Student2 | Sam2 Student2 | Sam2 Student2 | - # | Sam3 Student3 | Sam3 Student3 | Sam3 Student3 | + # Verify that each student has themself listed as a reviewer. + And the following should exist in the "allocations" table: + | -1- | -2- | -3- | + | Student One | Student One | Student One | + | Student Three | Student Three | Student Three | + | Student Two | Student Two | Student Two | And I change phase in workshop "TestWorkshop" to "Assessment phase" + # Confirm that the student can assess their own submission. And I am on the "TestWorkshop" "workshop activity" page logged in as student1 - And I should see "Assess yourself" - And I should see "Your submission" - And I should see "Assigned submissions to assess" - And I should see "Submission1" - And I should see "by Sam1 Student1" - And the "Assess" "button" should be enabled - And I am on the "TestWorkshop" "workshop activity" page logged in as student2 - And I should see "Assess yourself" - And I should see "Your submission" - And I should see "Assigned submissions to assess" - And I should see "Submission2" - And I should see "by Sam2 Student2" - And the "Assess" "button" should be enabled - And I am on the "TestWorkshop" "workshop activity" page logged in as student3 - And I should see "Assess yourself" - And I should see "Your submission" - And I should see "Assigned submissions to assess" - And I should see "Submission3" - And I should see "by Sam3 Student3" + Then I should see "Assess yourself" And the "Assess" "button" should be enabled + And I assess submission "Student One" in workshop "TestWorkshop" as: + | grade__idx_0 | 10 / 10 | + | peercomment__idx_0 | My work is amazing hence the grade. | + | Feedback for the author | Good work as always | + And the "Re-assess" "button" should be enabled + And I should see "Already graded" + # As teacher, confirm that Student One assessed his own work and received a grade. + And I am on the TestWorkshop "workshop activity" page logged in as teacher1 + And I should see grade "80" for workshop participant "Student One" set by peer "Student One" From 8ae03ee493f805e8ad6f306c40f69a1a20043d63 Mon Sep 17 00:00:00 2001 From: Ilya Tregubov Date: Thu, 27 Jun 2024 09:45:55 +0800 Subject: [PATCH 039/178] MDL-82292 mod_assign: Merge picture and name columns --- .upgradenotes/MDL-82292-2024070102564148.yml | 6 ++++++ mod/assign/gradingtable.php | 20 +++++++++++------- mod/assign/styles.css | 4 ++++ mod/assign/tests/behat/quickgrading.feature | 1 - mod/assign/tests/locallib_test.php | 22 ++++++++++---------- 5 files changed, 33 insertions(+), 20 deletions(-) create mode 100644 .upgradenotes/MDL-82292-2024070102564148.yml 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/mod/assign/gradingtable.php b/mod/assign/gradingtable.php index bbb0af951d4a6..89e4c46b68e8b 100644 --- a/mod/assign/gradingtable.php +++ b/mod/assign/gradingtable.php @@ -386,12 +386,8 @@ public function __construct(assign $assignment,
'; } - // User picture. if ($this->hasviewblind || !$this->assignment->is_blind_marking()) { - if (!$this->is_downloading()) { - $columns[] = 'picture'; - $headers[] = get_string('pictureofuser'); - } else { + if ($this->is_downloading()) { $columns[] = 'recordid'; $headers[] = get_string('recordid', 'assign'); } @@ -399,7 +395,6 @@ public function __construct(assign $assignment, // Fullname. $columns[] = 'fullname'; $headers[] = get_string('fullname'); - // Participant # details if can view real identities. if ($this->assignment->is_blind_marking()) { if (!$this->is_downloading()) { @@ -548,6 +543,7 @@ public function __construct(assign $assignment, // Set the columns. $this->define_columns($columns); + $this->set_columnsattributes(['fullname' => ['class' => 'username']]); $this->define_headers($headers); foreach ($extrauserfields as $extrafield) { $this->column_class($extrafield, $extrafield); @@ -882,8 +878,16 @@ public function col_outcomes(stdClass $row) { * * @param stdClass $row * @return string + * @deprecated since Moodle 4.5 + * @todo Final deprecation in Moodle 6.0. See MDL-82336. */ + #[\core\attribute\deprecated( + replacement: null, + since: '4.5', + reason: 'Picture column is merged with fullname column' + )] public function col_picture(stdClass $row) { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); return $this->output->user_picture($row); } @@ -896,8 +900,8 @@ public function col_picture(stdClass $row) { public function col_fullname($row) { if (!$this->is_downloading()) { $courseid = $this->assignment->get_course()->id; - $link = new moodle_url('/user/view.php', array('id' => $row->id, 'course' => $courseid)); - $fullname = $this->output->action_link($link, $this->assignment->fullname($row)); + $fullname = $this->output->render(\core_user::get_profile_picture($row, null, + ['courseid' => $courseid, 'includefullname' => true])); } else { $fullname = $this->assignment->fullname($row); } diff --git a/mod/assign/styles.css b/mod/assign/styles.css index 134b3a5b56bee..9b4656d2c29eb 100644 --- a/mod/assign/styles.css +++ b/mod/assign/styles.css @@ -294,6 +294,10 @@ white-space: nowrap; } +.path-mod-assign .gradingtable .username .d-inline-block { + white-space: nowrap; +} + .path-mod-assign .gradingtable .moodle-actionmenu[data-enhanced].show .menu a { padding-left: 12px; padding-right: 12px; diff --git a/mod/assign/tests/behat/quickgrading.feature b/mod/assign/tests/behat/quickgrading.feature index 170039cc62a1b..4a5eb5738a409 100644 --- a/mod/assign/tests/behat/quickgrading.feature +++ b/mod/assign/tests/behat/quickgrading.feature @@ -114,7 +114,6 @@ Feature: In an assignment, teachers grade multiple students on one page And I should not see "1337" And I am on the "Test assignment name" "assign activity" page logged in as teacher1 And I follow "View all submissions" - And I click on "Hide User picture" "link" And I click on "Hide Full name" "link" And I click on "Hide Email address" "link" And I click on "Hide Status" "link" diff --git a/mod/assign/tests/locallib_test.php b/mod/assign/tests/locallib_test.php index 28d5c48841ccb..6ac66569aceb6 100644 --- a/mod/assign/tests/locallib_test.php +++ b/mod/assign/tests/locallib_test.php @@ -401,7 +401,7 @@ public function test_gradingtable_status_rendering(): void { $document = new \DOMDocument(); @$document->loadHTML($output); $xpath = new \DOMXPath($document); - $this->assertEmpty($xpath->evaluate('string(//td[@id="mod_assign_grading-' . $assign->get_context()->id. '_r0_c8"])')); + $this->assertEmpty($xpath->evaluate('string(//td[@id="mod_assign_grading-' . $assign->get_context()->id. '_r0_c7"])')); } /** @@ -477,25 +477,25 @@ public function test_gradingtable_group_submissions_rendering(): void { // Check status. $this->assertSame(get_string('submissionstatus_submitted', 'assign'), - $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c4"]/div[@class="submissionstatussubmitted"])')); + $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c3"]/div[@class="submissionstatussubmitted"])')); $this->assertSame(get_string('submissionstatus_submitted', 'assign'), - $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c4"]/div[@class="submissionstatussubmitted"])')); + $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c3"]/div[@class="submissionstatussubmitted"])')); // Check submission last modified date. - $this->assertGreaterThan(0, strtotime($xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c8"])'))); - $this->assertGreaterThan(0, strtotime($xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c8"])'))); + $this->assertGreaterThan(0, strtotime($xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c7"])'))); + $this->assertGreaterThan(0, strtotime($xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c7"])'))); // Check group. - $this->assertSame($group->name, $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c5"])')); - $this->assertSame($group->name, $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c5"])')); + $this->assertSame($group->name, $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c4"])')); + $this->assertSame($group->name, $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c4"])')); // Check submission text. - $this->assertSame('Submission text', $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c9"]/div/div)')); - $this->assertSame('Submission text', $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c9"]/div/div)')); + $this->assertSame('Submission text', $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c8"]/div/div)')); + $this->assertSame('Submission text', $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c8"]/div/div)')); // Check comments can be made. - $this->assertEquals(1, $xpath->evaluate('count(//td[@id="' . $xpathuniqueidroot . '_r0_c10"]//textarea)')); - $this->assertEquals(1, $xpath->evaluate('count(//td[@id="' . $xpathuniqueidroot . '_r3_c10"]//textarea)')); + $this->assertEquals(1, $xpath->evaluate('count(//td[@id="' . $xpathuniqueidroot . '_r0_c9"]//textarea)')); + $this->assertEquals(1, $xpath->evaluate('count(//td[@id="' . $xpathuniqueidroot . '_r3_c9"]//textarea)')); } public function test_show_intro(): void { From dabf88a99a1f10db07106930897b657f613b8511 Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Mon, 8 Jul 2024 09:13:19 +0700 Subject: [PATCH 040/178] MDL-82404 core_cohort: Fix random Behat failure - Switched to use Table row verification instead of text assertion for better result - Used the correct selector for checkboxes --- cohort/amd/build/actions.min.js | 2 +- cohort/amd/build/actions.min.js.map | 2 +- cohort/amd/src/actions.js | 12 +++--------- cohort/tests/behat/add_cohort.feature | 17 +++++++++++++---- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/cohort/amd/build/actions.min.js b/cohort/amd/build/actions.min.js index c5d49ea397455..e54666369afaf 100644 --- a/cohort/amd/build/actions.min.js +++ b/cohort/amd/build/actions.min.js @@ -5,6 +5,6 @@ define("core_cohort/actions",["exports","core/event_dispatcher","core/notificati * @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_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 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 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})); //# 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..4885abd46495c 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 {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 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 // Delete multiple cohorts.\n document.addEventListener('click', event => {\n const cohortDeleteSelected = event.target.closest(SELECTORS.DELETEBUTTON);\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","cohortId","dataset","itemid","checkbox","querySelector","label","id","innerHTML","value","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,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,YAMzFhB,SAASC,iBAAiB,SAASI,cACzBY,qBAAuBZ,MAAMC,OAAOC,QAAQT,2BAC9CmB,qBAAsB,CACtBZ,MAAMa,uBAEAC,cAAgBnB,SAASY,cAAcQ,gBAAgBC,QAAQC,QAC/DC,oBAAsBJ,cAAcK,iBAAiB1B,yBACxB,IAA/ByB,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,MAAMjB,eAG7D,6BAAce,iBAChBH,MAAK,yCACYM,aAAaC,YAAa,CAACC,oBAAoB,GAAOjB,eAC7DU,eAAeQ,aAEzBC,MAAMC,sBAAaC,cACzBF,OAAM"} \ No newline at end of file diff --git a/cohort/amd/src/actions.js b/cohort/amd/src/actions.js index e529714f79852..a64801222593c 100644 --- a/cohort/amd/src/actions.js +++ b/cohort/amd/src/actions.js @@ -62,26 +62,20 @@ 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); - if (cohortDeleteSelected) { event.preventDefault(); 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 | From 3d3a90761b350ab32e7c806add29983710dc42ea Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Thu, 27 Jun 2024 22:42:12 +0100 Subject: [PATCH 041/178] MDL-82328 auth_oauth2: don't load custom profile field data too early. By prematurely loading custom profile data in the constructor, under specific circumstances it would happen before the page was fully initialised which meant that any attempt at applying filters when formatting profile field data would result in thrown exception. --- auth/oauth2/classes/auth.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/auth/oauth2/classes/auth.php b/auth/oauth2/classes/auth.php index b8023216fb18a..c591d6528af1d 100644 --- a/auth/oauth2/classes/auth.php +++ b/auth/oauth2/classes/auth.php @@ -63,7 +63,6 @@ class auth extends \auth_plugin_base { public function __construct() { $this->authtype = 'oauth2'; $this->config = get_config('auth_oauth2'); - $this->customfields = $this->get_custom_user_profile_fields(); } /** @@ -310,7 +309,7 @@ private function update_user(array $externaldata, $userdata) { return $userdata; } - $allfields = array_merge($this->userfields, $this->customfields); + $allfields = array_merge($this->userfields, $this->get_custom_user_profile_fields()); // Go through each field from the external data. foreach ($externaldata as $fieldname => $value) { From 0ccafe76f6a511623947225618ee18de0b7fa375 Mon Sep 17 00:00:00 2001 From: ferranrecio Date: Mon, 8 Jul 2024 13:34:28 +0200 Subject: [PATCH 042/178] MDL-81765 mod_subsection: disable mod subsection by default --- course/format/tests/stateactions_test.php | 6 ++++++ lib/tests/modinfolib_test.php | 8 ++++++++ mod/subsection/db/install.php | 4 ++++ mod/subsection/tests/behat/subsection_actionmenu.feature | 3 ++- mod/subsection/tests/behat/subsection_handling.feature | 3 ++- mod/subsection/tests/behat/subsection_navigation.feature | 3 ++- mod/subsection/tests/behat/subsection_rename.feature | 3 ++- .../tests/courseformat/sectiondelegate_test.php | 3 +++ .../tests/courseformat/sectiondelegatemodule_test.php | 9 +++++++++ report/outline/tests/behat/subsection_reports.feature | 3 ++- 10 files changed, 40 insertions(+), 5 deletions(-) diff --git a/course/format/tests/stateactions_test.php b/course/format/tests/stateactions_test.php index 9aa21ecdef8a9..1d9bacebc2c78 100644 --- a/course/format/tests/stateactions_test.php +++ b/course/format/tests/stateactions_test.php @@ -1432,6 +1432,9 @@ public function test_set_cm_indentation_delegated_section(): void { $this->resetAfterTest(); $this->setAdminUser(); + $manager = \core_plugin_manager::resolve_plugininfo_class('mod'); + $manager::enable_plugin('subsection', 1); + $course = $this->getDataGenerator()->create_course(); $subsection = $this->getDataGenerator()->create_module('subsection', ['course' => $course]); $otheractvity = $this->getDataGenerator()->create_module('forum', ['course' => $course]); @@ -1511,6 +1514,9 @@ public function test_filter_cms_with_section_delegate(): void { $this->resetAfterTest(); $this->setAdminUser(); + $manager = \core_plugin_manager::resolve_plugininfo_class('mod'); + $manager::enable_plugin('subsection', 1); + $course = $this->getDataGenerator()->create_course(); $subsection = $this->getDataGenerator()->create_module('subsection', ['course' => $course]); $otheractvity = $this->getDataGenerator()->create_module('forum', ['course' => $course]); diff --git a/lib/tests/modinfolib_test.php b/lib/tests/modinfolib_test.php index e7f084ef9e9b7..84c1099560ad9 100644 --- a/lib/tests/modinfolib_test.php +++ b/lib/tests/modinfolib_test.php @@ -1795,6 +1795,10 @@ public function test_multiple_modinfo_cache_purge(): void { */ public function test_get_sections_delegated_by_cm(): void { $this->resetAfterTest(); + + $manager = \core_plugin_manager::resolve_plugininfo_class('mod'); + $manager::enable_plugin('subsection', 1); + $course = $this->getDataGenerator()->create_course(['numsections' => 1]); $modinfo = get_fast_modinfo($course); @@ -1823,6 +1827,10 @@ public function test_get_sections_delegated_by_cm(): void { */ public function test_get_delegated_section_info(): void { $this->resetAfterTest(); + + $manager = \core_plugin_manager::resolve_plugininfo_class('mod'); + $manager::enable_plugin('subsection', 1); + $course = $this->getDataGenerator()->create_course(['numsections' => 1]); // Add a section delegated by a course module. diff --git a/mod/subsection/db/install.php b/mod/subsection/db/install.php index 6ee6ac8c89913..6578ac2bcd3c9 100644 --- a/mod/subsection/db/install.php +++ b/mod/subsection/db/install.php @@ -27,6 +27,10 @@ * Custom code to be run on installing the plugin. */ function xmldb_subsection_install() { + global $DB; + + // Disable the chat activity module on new installs by default. + $DB->set_field('modules', 'visible', 0, ['name' => 'subsection']); return true; } diff --git a/mod/subsection/tests/behat/subsection_actionmenu.feature b/mod/subsection/tests/behat/subsection_actionmenu.feature index 084a0c9d4533b..a7008e0b53ab0 100644 --- a/mod/subsection/tests/behat/subsection_actionmenu.feature +++ b/mod/subsection/tests/behat/subsection_actionmenu.feature @@ -5,7 +5,8 @@ Feature: The module menu replaces the section menu when accessing the subsection I need to see the module action menu in the section page. Background: - Given the following "users" exist: + Given I enable "subsection" "mod" plugin + And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: diff --git a/mod/subsection/tests/behat/subsection_handling.feature b/mod/subsection/tests/behat/subsection_handling.feature index 64365667f2c7f..0224555f3b8fb 100644 --- a/mod/subsection/tests/behat/subsection_handling.feature +++ b/mod/subsection/tests/behat/subsection_handling.feature @@ -5,7 +5,8 @@ Feature: Teachers create and destroy subsections I need to create and destroy subsections Background: - Given the following "users" exist: + Given I enable "subsection" "mod" plugin + And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: diff --git a/mod/subsection/tests/behat/subsection_navigation.feature b/mod/subsection/tests/behat/subsection_navigation.feature index 3ee554d29d944..e34012781a53d 100644 --- a/mod/subsection/tests/behat/subsection_navigation.feature +++ b/mod/subsection/tests/behat/subsection_navigation.feature @@ -5,7 +5,8 @@ Feature: Teachers navigate to subsections I need to navigate to subsections Background: - Given the following "users" exist: + Given I enable "subsection" "mod" plugin + And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: diff --git a/mod/subsection/tests/behat/subsection_rename.feature b/mod/subsection/tests/behat/subsection_rename.feature index 18c14f2748d51..cba16f2ef44a9 100644 --- a/mod/subsection/tests/behat/subsection_rename.feature +++ b/mod/subsection/tests/behat/subsection_rename.feature @@ -5,7 +5,8 @@ Feature: Teachers can rename subsections I need to sync subsection and activity names Background: - Given the following "users" exist: + Given I enable "subsection" "mod" plugin + And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: diff --git a/mod/subsection/tests/courseformat/sectiondelegate_test.php b/mod/subsection/tests/courseformat/sectiondelegate_test.php index b421321381622..13129dd375383 100644 --- a/mod/subsection/tests/courseformat/sectiondelegate_test.php +++ b/mod/subsection/tests/courseformat/sectiondelegate_test.php @@ -47,6 +47,9 @@ public function test_get_section_action_menu(): void { $this->resetAfterTest(); $this->setAdminUser(); + $manager = \core_plugin_manager::resolve_plugininfo_class('mod'); + $manager::enable_plugin('subsection', 1); + $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 1]); $this->getDataGenerator()->create_module('subsection', ['course' => $course->id, 'section' => 1]); diff --git a/mod/subsection/tests/courseformat/sectiondelegatemodule_test.php b/mod/subsection/tests/courseformat/sectiondelegatemodule_test.php index 1d28f7eb7a2c8..16c1a8aad6791 100644 --- a/mod/subsection/tests/courseformat/sectiondelegatemodule_test.php +++ b/mod/subsection/tests/courseformat/sectiondelegatemodule_test.php @@ -40,6 +40,9 @@ final class sectiondelegatemodule_test extends \advanced_testcase { public function test_get_parent_section(): void { $this->resetAfterTest(); + $manager = \core_plugin_manager::resolve_plugininfo_class('mod'); + $manager::enable_plugin('subsection', 1); + $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 2]); $module = $this->getDataGenerator()->create_module('subsection', (object)['course' => $course->id, 'section' => 2]); @@ -63,6 +66,9 @@ public function test_get_parent_section(): void { public function test_get_cm(): void { $this->resetAfterTest(); + $manager = \core_plugin_manager::resolve_plugininfo_class('mod'); + $manager::enable_plugin('subsection', 1); + $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 1]); $module = $this->getDataGenerator()->create_module('subsection', (object)['course' => $course->id, 'section' => 1]); @@ -86,6 +92,9 @@ public function test_get_cm(): void { public function test_get_course(): void { $this->resetAfterTest(); + $manager = \core_plugin_manager::resolve_plugininfo_class('mod'); + $manager::enable_plugin('subsection', 1); + $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 1]); $module = $this->getDataGenerator()->create_module('subsection', (object)['course' => $course->id, 'section' => 1]); diff --git a/report/outline/tests/behat/subsection_reports.feature b/report/outline/tests/behat/subsection_reports.feature index e038b929337cb..29e4dd1b17de5 100644 --- a/report/outline/tests/behat/subsection_reports.feature +++ b/report/outline/tests/behat/subsection_reports.feature @@ -5,7 +5,8 @@ Feature: Subsections are shown in reports I need to see sections and subsections structure in reports Background: - Given the following "users" exist: + Given I enable "subsection" "mod" plugin + And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: From 11b276fc1b53082f3038f18428a6a60f1d9884fc Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Fri, 7 Jun 2024 16:42:26 +0200 Subject: [PATCH 043/178] MDL-43938 badges: Bagde names are not unique anymore The restriction for the badge name to be unique has been removed so, from now on, the badge names can be duplicated, even inside a course. --- badges/classes/badge.php | 4 +-- badges/classes/form/badge.php | 39 ++++++++++++------------ badges/tests/behat/manage_badges.feature | 30 ++++++++++++++++++ lang/en/badges.php | 6 +++- lang/en/deprecated.txt | 1 + 5 files changed, 57 insertions(+), 23 deletions(-) diff --git a/badges/classes/badge.php b/badges/classes/badge.php index 777655582331f..f3d5da7e8ff5d 100644 --- a/badges/classes/badge.php +++ b/badges/classes/badge.php @@ -1008,7 +1008,7 @@ public static function create_badge(stdClass $data, ?int $courseid = null): badg $fordb->id = null; $fordb->courseid = $courseid; $fordb->type = $courseid ? BADGE_TYPE_COURSE : BADGE_TYPE_SITE; - $fordb->name = $data->name; + $fordb->name = trim($data->name); $fordb->version = $data->version; $fordb->language = $data->language; $fordb->description = $data->description; @@ -1078,7 +1078,7 @@ public static function create_badge(stdClass $data, ?int $courseid = null): badg public function update(stdClass $data): bool { global $USER; - $this->name = $data->name; + $this->name = trim($data->name); $this->version = trim($data->version); $this->language = $data->language; $this->description = $data->description; diff --git a/badges/classes/form/badge.php b/badges/classes/form/badge.php index a3d6ed6fba7f2..162d84ab15790 100644 --- a/badges/classes/form/badge.php +++ b/badges/classes/form/badge.php @@ -43,6 +43,15 @@ public function definition() { $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'), ['size' => '70']); @@ -180,6 +189,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'); @@ -190,7 +204,11 @@ public function set_data($badge) { * Validates form data */ public function validation($data, $files) { - global $DB; + global $DB, $SITE; + + // 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 (badges_open_badges_backpack_api() == OPEN_BADGES_V1) { @@ -211,25 +229,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', - ['name' => $data['name'], 'deleted' => BADGE_STATUS_ARCHIVED], - ); - } else { - $duplicate = $DB->record_exists_select( - 'badge', - 'name = :name AND id != :badgeid AND status != :deleted', - ['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'); } diff --git a/badges/tests/behat/manage_badges.feature b/badges/tests/behat/manage_badges.feature index 61648dc45257a..072fb9b55bc30 100644 --- a/badges/tests/behat/manage_badges.feature +++ b/badges/tests/behat/manage_badges.feature @@ -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/lang/en/badges.php b/lang/en/badges.php index 04f87eecc42eb..eb7686e4622bd 100644 --- a/lang/en/badges.php +++ b/lang/en/badges.php @@ -302,7 +302,6 @@ $string['error:cannotdeletecriterion'] = 'This criterion cannot be deleted. '; $string['error:connectionunknownreason'] = 'The connection was unsuccessful but no reason was given.'; $string['error:clone'] = 'Cannot clone the badge.'; -$string['error:duplicatename'] = 'Badge with such name already exists in the system.'; $string['error:externalbadgedoesntexist'] = 'Badge not found'; $string['error:guestuseraccess'] = 'You are currently using guest access. To see badges you need to log in with your user account.'; $string['error:invalidcriteriatype'] = 'Invalid criteria type.'; @@ -421,6 +420,8 @@ $string['never'] = 'Never'; $string['newbackpack'] = 'Add a new backpack'; $string['newbadge'] = 'Add a new badge'; +$string['newbadgedeprecated'] = 'You have been redirected from badges/newbadge.php. Please note that badges/newbadge.php will be removed in the near future. +
Update links and bookmarks to use the current page badges/edit.php.'; $string['newimage'] = 'New image'; $string['noalignment'] = 'This badge does not have any external skills or standards specified.'; $string['noawards'] = 'This badge has not been earned yet.'; @@ -595,3 +596,6 @@ // Deprecated since Moodle 4.3. $string['backpackemail'] = 'Email address'; $string['backpackemail_help'] = 'The email address associated with your backpack. While you are connected, any badges earned on this site will be associated with this email address.'; + +// Deprecated since Moodle 4.5. +$string['error:duplicatename'] = 'Badge with such name already exists in the system.'; diff --git a/lang/en/deprecated.txt b/lang/en/deprecated.txt index b934616128dbf..37bb5465b5236 100644 --- a/lang/en/deprecated.txt +++ b/lang/en/deprecated.txt @@ -116,3 +116,4 @@ coursecalendar,core_calendar importcalendarexternal,core_calendar nocalendarsubscriptions,core_calendar datechanged,core +error:duplicatename,core_badges From cbac1d5e328e14462a99064f3ef58fc707a8dc34 Mon Sep 17 00:00:00 2001 From: Daniel Ziegenberg Date: Wed, 27 Mar 2024 20:49:09 +0100 Subject: [PATCH 044/178] MDL-73284 messages: Final deprecation MESSAGE_DEFAULT_LOGGED(OFF|IN) Signed-off-by: Daniel Ziegenberg --- .upgradenotes/MDL-73284-2024061908343534.yml | 5 ++++ .../notification_list_processor.php | 15 ----------- message/externallib.php | 19 -------------- message/lib.php | 26 +------------------ user/externallib.php | 12 --------- 5 files changed, 6 insertions(+), 71 deletions(-) create mode 100644 .upgradenotes/MDL-73284-2024061908343534.yml 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/message/classes/output/preferences/notification_list_processor.php b/message/classes/output/preferences/notification_list_processor.php index 544966d64a7d8..5ee10e550c5b7 100644 --- a/message/classes/output/preferences/notification_list_processor.php +++ b/message/classes/output/preferences/notification_list_processor.php @@ -107,7 +107,6 @@ private function is_preference_enabled($name, $locked) { /** * Export this data so it can be used as the context for a mustache template. - * @todo Remove loggedin and loggedoff from context on MDL-73284. * * @param renderer_base $output * @return stdClass @@ -129,18 +128,6 @@ public function export_for_template(\renderer_base $output) { 'name' => $processor->name, 'locked' => false, 'userconfigured' => $processor->object->is_user_configured(), - // Backward compatibility, deprecated attribute. - 'loggedin' => [ - 'name' => 'loggedin', - 'displayname' => 'loggedin', - 'checked' => false, - ], - // Backward compatibility, deprecated attribute. - 'loggedoff' => [ - 'name' => 'loggedoff', - 'displayname' => 'loggedoff', - 'checked' => false, - ], 'enabled' => false, 'enabledlabel' => get_string('sendingviaenabled', 'message', $labelparams), ]; @@ -151,8 +138,6 @@ public function export_for_template(\renderer_base $output) { } $context['enabled'] = $this->is_preference_enabled($preferencebase.'_enabled', $context['locked']); - $context['loggedoff']['checked'] = $context['enabled']; // Backward compatibility, deprecated attribute. - $context['loggedin']['checked'] = $context['enabled']; // Backward compatibility, deprecated attribute. // If settings are disallowed or forced, just display the corresponding message, if not use user settings. if ($context['locked']) { diff --git a/message/externallib.php b/message/externallib.php index 56c40012fd44f..693a9df86b7bc 100644 --- a/message/externallib.php +++ b/message/externallib.php @@ -3028,7 +3028,6 @@ protected static function validate_preferences_permissions($userid) { * * @return external_single_structure the structure * @since Moodle 3.2 - * @todo Remove loggedin and loggedoff from processors structure on MDL-73284. */ protected static function get_preferences_structure() { return new external_single_structure( @@ -3065,24 +3064,6 @@ protected static function get_preferences_structure() { 'lockedmessage' => new external_value(PARAM_TEXT, 'Text to display if locked', VALUE_OPTIONAL), 'userconfigured' => new external_value(PARAM_INT, 'Is configured?'), - 'loggedin' => new external_single_structure( - array( - 'name' => new external_value(PARAM_NOTAGS, 'Name'), - 'displayname' => new external_value(PARAM_TEXT, 'Display name'), - 'checked' => new external_value(PARAM_BOOL, 'Is checked?'), - ), - 'DEPRECATED ATTRIBUTE - - Kept for backward compatibility, use enabled instead.', - ), - 'loggedoff' => new external_single_structure( - array( - 'name' => new external_value(PARAM_NOTAGS, 'Name'), - 'displayname' => new external_value(PARAM_TEXT, 'Display name'), - 'checked' => new external_value(PARAM_BOOL, 'Is checked?'), - ), - 'DEPRECATED ATTRIBUTE - - Kept for backward compatibility, use enabled instead.', - ), 'enabled' => new external_value(PARAM_BOOL, 'Is enabled?'), ) ), diff --git a/message/lib.php b/message/lib.php index c1fb3838cec70..9d52400bcb787 100644 --- a/message/lib.php +++ b/message/lib.php @@ -35,24 +35,12 @@ * Define contants for messaging default settings population. For unambiguity of * plugin developer intentions we use 4-bit value (LSB numbering): * bit 0 - whether to send message (MESSAGE_DEFAULT_ENABLED) - * bit 1 - Deprecated: whether to send message (MESSAGE_DEFAULT_LOGGEDOFF). Used to mean only when the user is logged off. + * bit 1 - not used * bit 2..3 - messaging permission (MESSAGE_DISALLOWED|MESSAGE_PERMITTED|MESSAGE_FORCED) * * MESSAGE_PERMITTED_MASK contains the mask we use to distinguish permission setting. */ - /** - * @deprecated since Moodle 4.0. Use MESSAGE_DEFAULT_ENABLED instead. - * @todo Remove on MDL-73284. - */ -define('MESSAGE_DEFAULT_LOGGEDIN', 0x01); // 0001 - - /** - * @deprecated since Moodle 4.0 MDL-73284. Use MESSAGE_DEFAULT_ENABLED instead. - * @todo Remove on MDL-73284. - */ -define('MESSAGE_DEFAULT_LOGGEDOFF', 0x02); // 0010 - define('MESSAGE_DEFAULT_ENABLED', 0x01); // 0001. define('MESSAGE_DISALLOWED', 0x04); // 0100. @@ -61,13 +49,6 @@ define('MESSAGE_PERMITTED_MASK', 0x0c); // 1100. -/** - * Set default value for default outputs permitted setting - * @deprecated since Moodle 4.0 MDL-73284. - * @todo Remove on MDL-73284. - */ -define('MESSAGE_DEFAULT_PERMITTED', 'permitted'); - /** * Set default values for polling. */ @@ -464,7 +445,6 @@ function get_message_output_default_preferences() { * Translate message default settings from binary value to the array of string * representing the settings to be stored. Also validate the provided value and * use default if it is malformed. - * @todo Remove usage of MESSAGE_DEFAULT_LOGGEDOFF on MDL-73284. * * @param int $plugindefault Default setting suggested by plugin * @param string $processorname The name of processor @@ -503,10 +483,6 @@ function translate_message_default_setting($plugindefault, $processorname) { $locked = false; // It's equivalent to logged in. $enabled = $plugindefault & MESSAGE_DEFAULT_ENABLED == MESSAGE_DEFAULT_ENABLED; - - // MESSAGE_DEFAULT_LOGGEDOFF is deprecated but we're checking it just in case. - $loggedoff = $plugindefault & MESSAGE_DEFAULT_LOGGEDOFF == MESSAGE_DEFAULT_LOGGEDOFF; - $enabled = $enabled || $loggedoff; break; } diff --git a/user/externallib.php b/user/externallib.php index 360fba70c0fac..c0add4cb15d1d 100644 --- a/user/externallib.php +++ b/user/externallib.php @@ -410,18 +410,6 @@ public static function update_user_preferences($userid = 0, $emailstop = null, $ if (!empty($preferences)) { $userpref = ['id' => $userid]; foreach ($preferences as $preference) { - - /* - * Rename user message provider preferences to avoid orphan settings on old app versions. - * @todo Remove this "translation" block on MDL-73284. - */ - if (preg_match('/message_provider_.*_loggedin/', $preference['type']) || - preg_match('/message_provider_.*_loggedoff/', $preference['type'])) { - $nameparts = explode('_', $preference['type']); - array_pop($nameparts); - $preference['type'] = implode('_', $nameparts).'_enabled'; - } - $userpref['preference_' . $preference['type']] = $preference['value']; } useredit_update_user_preference($userpref); From b1df497321716370be7abcb45c05ff75e616fa30 Mon Sep 17 00:00:00 2001 From: David Woloszyn Date: Mon, 23 Jan 2023 11:12:00 +1100 Subject: [PATCH 045/178] MDL-58353 admin: Logout on password change default to yes --- admin/settings/security.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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'), From 03f4c8468499e15420eeb81e5a3956834289f276 Mon Sep 17 00:00:00 2001 From: Angelia Dela Cruz Date: Tue, 2 Jul 2024 15:37:55 +0800 Subject: [PATCH 046/178] MDL-81296 mod_data: Behat test for data activity readonly availability --- .../data_activity_read_only_dates.feature | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 mod/data/tests/behat/data_activity_read_only_dates.feature diff --git a/mod/data/tests/behat/data_activity_read_only_dates.feature b/mod/data/tests/behat/data_activity_read_only_dates.feature new file mode 100644 index 0000000000000..ec061bca75457 --- /dev/null +++ b/mod/data/tests/behat/data_activity_read_only_dates.feature @@ -0,0 +1,33 @@ +@mod @mod_data +Feature: Control database activity entry based on read-only dates + In order to restrict or allow student entries based on specific dates + As a teacher + I need to be able to set read-only dates for the database activity + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + + Scenario Outline: Student can add entries only when the current date falls outside the read-only date range + Given the following "activities" exist: + | activity | course | name | idnumber | timeviewfrom | timeviewto | + | data | C1 | Data Activity 1 | DB1 | | | + And the following "mod_data > fields" exist: + | database | type | name | + | DB1 | text | DB Field 1 | + When I am on the "Data Activity 1" "data activity" page logged in as student1 + # The "Add entry" button is visible only when the current date falls outside the read-only date range. + Then "Add entry" "button" exist + + Examples: + | viewfrom | viewto | btnvisibility | + | ##yesterday## | ##tomorrow## | should not | + | ##tomorrow## | ##tomorrow +1day## | should | + | ##1 week ago## | ##yesterday## | should | From 2e0579ce3ad4f734c6a6b3e42293d4ee80a1c5ed Mon Sep 17 00:00:00 2001 From: David Woloszyn Date: Thu, 20 Jun 2024 16:18:48 +1000 Subject: [PATCH 047/178] MDL-58353 report_usersessions: Ability to logout all other sessions --- lang/en/moodle.php | 4 +-- login/change_password.php | 3 +- login/change_password_form.php | 12 +++++++- login/lib.php | 2 +- login/set_password_form.php | 7 +++++ .../lang/en/report_usersessions.php | 6 +++- report/usersessions/user.php | 30 ++++++++++++++++--- user/tests/behat/edituserpassword.feature | 8 ++--- 8 files changed, 58 insertions(+), 14 deletions(-) diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 9a27682c6c309..c06715f3c50a0 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -2076,8 +2076,8 @@ $string['showthishelpinlanguage'] = 'Show this help in language: {$a}'; $string['schedule'] = 'Schedule'; $string['sidepanel'] = 'Side panel'; -$string['signoutofotherservices'] = 'Sign out everywhere'; -$string['signoutofotherservices_help'] = 'If ticked, the account will be signed out of all devices and systems which use web services, such as the mobile app.'; +$string['signoutofotherservices'] = 'Log out of all web apps'; +$string['signoutofotherservices_help'] = 'Log out of all devices and systems that use web services, such as the mobile app. Browser sessions will remain active.'; $string['since'] = 'Since'; $string['sincelast'] = 'since last login'; $string['site'] = 'Site'; diff --git a/login/change_password.php b/login/change_password.php index 560884e6a98cd..98d107d8ad1fe 100644 --- a/login/change_password.php +++ b/login/change_password.php @@ -117,7 +117,8 @@ user_add_password_history($USER->id, $data->newpassword1); - if (!empty($CFG->passwordchangelogout)) { + // Log out all other sessions if mandated by admin, or if set by the user. + if (!empty($CFG->passwordchangelogout) || !empty($data->logoutothersessions)) { \core\session\manager::kill_user_sessions($USER->id, session_id()); } diff --git a/login/change_password_form.php b/login/change_password_form.php index ce2b4c3b9ff29..b6a01a1d9dbf5 100644 --- a/login/change_password_form.php +++ b/login/change_password_form.php @@ -70,10 +70,20 @@ function definition() { $mform->addRule('newpassword2', get_string('required'), 'required', null, 'client'); $mform->setType('newpassword2', PARAM_RAW); - if (empty($CFG->passwordchangetokendeletion) and !empty(webservice::get_active_tokens($USER->id))) { + $mform->addElement('checkbox', 'logoutothersessions', get_string('logoutothersessions', 'report_usersessions')); + $mform->addHelpButton('logoutothersessions', 'logoutothersessions', 'report_usersessions'); + $mform->setDefault('logoutothersessions', 1); + if (!empty($CFG->passwordchangelogout)) { + $mform->getElement('logoutothersessions')->freeze(); + } + + if (!empty(webservice::get_active_tokens($USER->id))) { $mform->addElement('advcheckbox', 'signoutofotherservices', get_string('signoutofotherservices')); $mform->addHelpButton('signoutofotherservices', 'signoutofotherservices'); $mform->setDefault('signoutofotherservices', 1); + if (!empty($CFG->passwordchangetokendeletion)) { + $mform->getElement('signoutofotherservices')->freeze(); + } } // hidden optional params diff --git a/login/lib.php b/login/lib.php index e30e178369e5e..5cec753feb2f6 100644 --- a/login/lib.php +++ b/login/lib.php @@ -287,7 +287,7 @@ function core_login_process_password_set($token) { throw new \moodle_exception('errorpasswordupdate', 'auth'); } user_add_password_history($user->id, $data->password); - if (!empty($CFG->passwordchangelogout)) { + if (!empty($CFG->passwordchangelogout) || !empty($data->logoutothersessions)) { \core\session\manager::kill_user_sessions($user->id, session_id()); } // Reset login lockout (if present) before a new password is set. diff --git a/login/set_password_form.php b/login/set_password_form.php index 89221e70c5b52..1c9d2124d43d5 100644 --- a/login/set_password_form.php +++ b/login/set_password_form.php @@ -83,6 +83,13 @@ public function definition() { $mform->addRule('password2', get_string('required'), 'required', null, 'client'); $mform->setType('password2', PARAM_RAW); + $mform->addElement('checkbox', 'logoutothersessions', get_string('logoutothersessions', 'report_usersessions')); + $mform->addHelpButton('logoutothersessions', 'logoutothersessions', 'report_usersessions'); + $mform->setDefault('logoutothersessions', 1); + if (!empty($CFG->passwordchangelogout)) { + $mform->getElement('logoutothersessions')->freeze(); + } + // Hook for plugins to extend form definition. $user = $this->_customdata; core_login_extend_set_password_form($mform, $user); diff --git a/report/usersessions/lang/en/report_usersessions.php b/report/usersessions/lang/en/report_usersessions.php index 1fbc317a40833..14cde6675e49c 100644 --- a/report/usersessions/lang/en/report_usersessions.php +++ b/report/usersessions/lang/en/report_usersessions.php @@ -23,8 +23,12 @@ * @author Petr Skoda */ +$string['logoutothersessions'] = 'Log out all other browser sessions'; +$string['logoutothersessions_help'] = 'Log out of all browser sessions, except for this one. This does not affect web apps.'; +$string['logoutothersessionssuccess'] = 'You have been logged out of all your other sessions'; +$string['logoutsinglesessionsuccess'] = 'You have been logged out of the session at {$a}'; +$string['mysessions'] = 'My active browser sessions'; $string['navigationlink'] = 'Browser sessions'; -$string['mysessions'] = 'My active sessions'; $string['pluginname'] = 'User sessions report'; $string['thissession'] = 'Current session'; $string['usersessions:manageownsessions'] = 'Manage own browser sessions'; diff --git a/report/usersessions/user.php b/report/usersessions/user.php index 52e2d856a7f8d..e295d9012690c 100644 --- a/report/usersessions/user.php +++ b/report/usersessions/user.php @@ -43,6 +43,8 @@ require_capability('report/usersessions:manageownsessions', $context); $delete = optional_param('delete', 0, PARAM_INT); +$deleteall = optional_param('deleteall', false, PARAM_BOOL); +$lastip = cleanremoteaddr(optional_param('lastip', '', PARAM_TEXT)); $PAGE->set_url('/report/usersessions/user.php'); $PAGE->set_context($context); @@ -50,9 +52,24 @@ $PAGE->set_heading(fullname($USER)); $PAGE->set_pagelayout('admin'); -if ($delete and confirm_sesskey()) { +// Delete a specific session. +if ($delete && confirm_sesskey()) { report_usersessions_kill_session($delete); - redirect($PAGE->url); + redirect( + url: $PAGE->url, + message: get_string('logoutsinglesessionsuccess', 'report_usersessions', $lastip), + messagetype: \core\output\notification::NOTIFY_SUCCESS, + ); +} + +// Delete all sessions except current. +if ($deleteall && confirm_sesskey()) { + \core\session\manager::kill_user_sessions($USER->id, session_id()); + redirect( + url: $PAGE->url, + message: get_string('logoutothersessionssuccess', 'report_usersessions'), + messagetype: \core\output\notification::NOTIFY_SUCCESS, + ); } // Create the breadcrumb. @@ -79,7 +96,7 @@ } else { $lastaccess = report_usersessions_format_duration(time() - $session->timemodified); - $url = new moodle_url($PAGE->url, array('delete' => $session->id, 'sesskey' => sesskey())); + $url = new moodle_url($PAGE->url, ['delete' => $session->id, 'sesskey' => sesskey(), 'lastip' => $session->lastip]); $deletelink = html_writer::link($url, get_string('logout')); } $data[] = array(userdate($session->timecreated), $lastaccess, report_usersessions_format_ip($session->lastip), $deletelink); @@ -91,5 +108,10 @@ $table->data = $data; echo html_writer::table($table); -echo $OUTPUT->footer(); +// Provide button to log out all other sessions. +if (count($sessions) > 1) { + $url = new moodle_url($PAGE->url, ['deleteall' => true]); + echo $OUTPUT->single_button($url, get_string('logoutothersessions', 'report_usersessions')); +} +echo $OUTPUT->footer(); diff --git a/user/tests/behat/edituserpassword.feature b/user/tests/behat/edituserpassword.feature index 29831592cd69d..ae90f8c5d9f70 100644 --- a/user/tests/behat/edituserpassword.feature +++ b/user/tests/behat/edituserpassword.feature @@ -16,14 +16,14 @@ Feature: Edit a users password # We need to cancel/submit a form that has been modified. And I press "Create user" - Scenario: Sign out everywhere field is not present if user doesn't have active token + Scenario: Log out web apps field is not present if user doesn't have active token Given the following "users" exist: | username | firstname | lastname | email | | user01 | User | One | user01@example.com | When I am on the "user01" "user > editing" page logged in as "admin" - Then "Sign out everywhere" "field" should not exist + Then "Log out of all web apps" "field" should not exist - Scenario Outline: Sign out everywhere field is present based on expiry of active token + Scenario Outline: Log out web apps field is present based on expiry of active token Given the following "users" exist: | username | firstname | lastname | email | | user01 | User | One | user01@example.com | @@ -34,7 +34,7 @@ Feature: Edit a users password | user | service | validuntil | | user01 | mytestservice | | When I am on the "user01" "user > editing" page logged in as "admin" - Then "Sign out everywhere" "field" exist + Then "Log out of all web apps" "field" exist Examples: | validuntil | shouldornot | | ## -1 month ## | should not | From 092e2d1ec755b54764d16409ab0b891f9ed59095 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Tue, 11 Jun 2024 12:49:44 +0200 Subject: [PATCH 048/178] MDL-82057 backup: Badges independent of user data and activities The dependency of badges on user data and activities has been removed when copying, importing, or creating backups in courses. Now, badges can be imported or copied even if user data and activities are not selected: - If user data is not selected (during backup or course copying), the badge information (name, description, image, etc.) and criteria will be included, but the users who have been awarded the badge will be excluded. - If activities are not selected (during backup), the badge information (name, description, image, etc.) will be copied, but the criteria and users who have been awarded the badge will be excluded. --- backup/moodle2/backup_root_task.class.php | 2 - backup/moodle2/backup_stepslib.php | 45 ++++++++++--------- .../util/ui/tests/behat/import_course.feature | 22 +++++++++ .../behat/restore_moodle2_courses.feature | 43 ++++++++++++++++++ 4 files changed, 88 insertions(+), 24 deletions(-) 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/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 | From 5a93545c523e0251ca353eb437b85136844d503a Mon Sep 17 00:00:00 2001 From: Angelia Dela Cruz Date: Tue, 25 Jun 2024 14:00:58 +0800 Subject: [PATCH 049/178] MDL-82236 mod_data: Behat to display DB activity entry rating --- .../tests/behat/data_activity_rating.feature | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 mod/data/tests/behat/data_activity_rating.feature diff --git a/mod/data/tests/behat/data_activity_rating.feature b/mod/data/tests/behat/data_activity_rating.feature new file mode 100644 index 0000000000000..2aedb5c0037ac --- /dev/null +++ b/mod/data/tests/behat/data_activity_rating.feature @@ -0,0 +1,53 @@ +@mod @mod_data +Feature: Enable activity rating according to chosen grading scale + In order to have ratings appear in the course gradebook + As a teacher + I need to enable activity rating according to chosen grading scale + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | One | teacher1@example.com | + | student1 | Student | One | student1@example.com | + | student2 | Student | Two | student2@example.com | + And the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + | student2 | C1 | student | + And the following "activities" exist: + | activity | course | name | idnumber | + | data | C1 | DB activity 1 | db1 | + And the following "mod_data > fields" exist: + | database | type | name | + | db1 | text | DB field | + + @javascript + Scenario: View ratings in the course gradebook + Given I am on the "DB activity 1" "data activity editing" page logged in as teacher1 + And I expand all fieldsets + And I set the following fields to these values: + | Aggregate type | Count of ratings | + | scale[modgrade_type] | Point | + | scale[modgrade_point] | 10 | + And I press "Save and display" + And the following "mod_data > entries" exist: + | database | user | DB field | + | db1 | student1 | S1 entry 1 | + | db1 | student1 | S1 entry 2 | + | db1 | student2 | S2 entry 1 | + And I am on the "DB activity 1" "data activity" page + And I select "Single view" from the "jump" singleselect + And I set the field "rating" to "5" + And I follow "Next page" + And I set the field "rating" to "7" + And I follow "Next page" + And I set the field "rating" to "10" + When I am on the "Course 1" "grades > Grader report > View" page + Then the following should exist in the "user-grades" table: + | -1- | -2- | -3- | + | Student One | student1@example.com | 2.00 | + | Student Two | student2@example.com | 1.00 | From c8d32c279dc7d74ec67c1c400e173f4ce2e38575 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Tue, 2 Jul 2024 11:32:35 +0100 Subject: [PATCH 050/178] MDL-82309 comment: preserve link text property during AJAX requests. We really only need to update the count value that follows the link text content, so do that rather than overwriting the entire thing. --- comment/comment.js | 18 +++++++++--------- comment/lib.php | 3 +-- lang/en/moodle.php | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) 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/lang/en/moodle.php b/lang/en/moodle.php index 9a27682c6c309..b97251e584068 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -280,7 +280,6 @@ $string['comebacklater'] = 'Please come back later.'; $string['commentincontext'] = 'Find this comment in context'; $string['comments'] = 'Comments'; -$string['commentscount'] = 'Comments ({$a})'; $string['commentsnotenabled'] = 'Comments feature is not enabled'; $string['commentsrequirelogin'] = 'You need to log in to view the comments.'; $string['comparelanguage'] = 'Compare and edit current language'; @@ -2521,4 +2520,5 @@ * -word - don\'t include results containing this word.'; // Deprecated since Moodle 4.5. +$string['commentscount'] = 'Comments ({$a})'; $string['datechanged'] = 'Date changed'; From 0766976b610e0a33c67c0c29c4484588f511878d Mon Sep 17 00:00:00 2001 From: ferranrecio Date: Fri, 5 Jul 2024 15:34:18 +0200 Subject: [PATCH 051/178] MDL-81798 course: limit number of subsections --- .../local/service/content_item_service.php | 5 ++- .../tests/behat/subsection_limit.feature | 31 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 mod/subsection/tests/behat/subsection_limit.feature 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/mod/subsection/tests/behat/subsection_limit.feature b/mod/subsection/tests/behat/subsection_limit.feature new file mode 100644 index 0000000000000..64353d9fd2b8d --- /dev/null +++ b/mod/subsection/tests/behat/subsection_limit.feature @@ -0,0 +1,31 @@ +@mod @mod_subsection +Feature: Teacher can only add subsection when certain conditions are met + In order to limit subsections + As an teacher + I need to create subsections only when possible + + Background: + Given I enable "subsection" "mod" plugin + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | category | numsections | initsections | + | Course 1 | C1 | 0 | 5 | 1 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + + @javascript + Scenario: The activity chooser filter subsections when section limit is reached + Given the following config values are set as admin: + | maxsections | 10 | moodlecourse | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I click on "Add an activity or resource" "button" in the "Section 1" "section" + And I should see "Subsection" in the "Add an activity or resource" "dialogue" + When the following config values are set as admin: + | maxsections | 4 | moodlecourse | + And I am on "Course 1" course homepage + Then I click on "Add an activity or resource" "button" in the "Section 1" "section" + And I should not see "Subsection" in the "Add an activity or resource" "dialogue" From 5ca968fea4c45ea292078dbc05eda8a91dc9a25c Mon Sep 17 00:00:00 2001 From: ferranrecio Date: Fri, 5 Jul 2024 16:21:18 +0200 Subject: [PATCH 052/178] MDL-82324 course: fix empty section dropzone --- .../amd/build/local/content/section.min.js | 2 +- .../amd/build/local/content/section.min.js.map | 2 +- .../build/local/courseeditor/dndsection.min.js | 2 +- .../local/courseeditor/dndsection.min.js.map | 2 +- course/format/amd/src/local/content/section.js | 12 +++++++++++- .../amd/src/local/courseeditor/dndsection.js | 18 +++++++++++++++++- theme/boost/scss/moodle/course.scss | 11 +++++++++++ theme/boost/style/moodle.css | 6 ++++++ theme/classic/style/moodle.css | 6 ++++++ 9 files changed, 55 insertions(+), 6 deletions(-) diff --git a/course/format/amd/build/local/content/section.min.js b/course/format/amd/build/local/content/section.min.js index 5b97746cd2108..ab69426c5ac14 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={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}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(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})); //# 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..593f3bbd3d204 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 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 * 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 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","getLastCmFallback","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 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/src/local/content/section.js b/course/format/amd/src/local/content/section.js index 135e55f355e41..0a621301c0e9b 100644 --- a/course/format/amd/src/local/content/section.js +++ b/course/format/amd/src/local/content/section.js @@ -119,7 +119,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); @@ -139,6 +139,16 @@ export default class extends DndSection { return cms[cms.length - 1]; } + /** + * 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); + } + /** * Update a content section using the state information. * 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/theme/boost/scss/moodle/course.scss b/theme/boost/scss/moodle/course.scss index c49311a4acd67..4e0a8e3fb0508 100644 --- a/theme/boost/scss/moodle/course.scss +++ b/theme/boost/scss/moodle/course.scss @@ -257,6 +257,17 @@ border-top: 1px solid $dropzone-border; margin-top: -1px; } + + [data-for="sectioninfo"] { + // When a section is empty, the activity dropzone indicator is below + // the section info. This ensures the dropzone will not displace the content + // even if the section has no restrictions or info to display. + min-height: 1px; + } + + [data-for="sectioninfo"].drop-down { + margin-top: -1px; + } } .section .activity .activityinstance .groupinglabel { diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index e018963bac273..f6b37f0ba5287 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -28211,6 +28211,12 @@ table.calendartable caption { border-top: 1px solid #1d2125; margin-top: -1px; } +.course-content .section.dropready [data-for=sectioninfo] { + min-height: 1px; +} +.course-content .section.dropready [data-for=sectioninfo].drop-down { + margin-top: -1px; +} .section .activity .activityinstance .groupinglabel { padding-left: 30px; diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 8dce6f612b0bc..6571fbfe13139 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -28211,6 +28211,12 @@ table.calendartable caption { border-top: 1px solid #1d2125; margin-top: -1px; } +.course-content .section.dropready [data-for=sectioninfo] { + min-height: 1px; +} +.course-content .section.dropready [data-for=sectioninfo].drop-down { + margin-top: -1px; +} .section .activity .activityinstance .groupinglabel { padding-left: 30px; From a22ccfc7d08989b6b8b37f29503b30eb12d2ac8c Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Tue, 9 Jul 2024 12:07:01 +0100 Subject: [PATCH 053/178] MDL-82066 hook: new hook to extend options for default site homepage. --- .upgradenotes/MDL-82066-2024070911064670.yml | 7 ++ admin/settings/appearance.php | 6 ++ lib/classes/user.php | 7 +- user/classes/form/defaulthomepage_form.php | 21 +++--- user/classes/hook/extend_default_homepage.php | 68 +++++++++++++++++++ 5 files changed, 98 insertions(+), 11 deletions(-) create mode 100644 .upgradenotes/MDL-82066-2024070911064670.yml create mode 100644 user/classes/hook/extend_default_homepage.php 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/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/lib/classes/user.php b/lib/classes/user.php index 77ab9304f544d..4a06a58c36c23 100644 --- a/lib/classes/user.php +++ b/lib/classes/user.php @@ -1042,8 +1042,13 @@ protected static function fill_preferences_cache() { $choices[] = HOMEPAGE_MY; } $choices[] = HOMEPAGE_MYCOURSES; + + // Allow hook callbacks to extend options. + $hook = new \core_user\hook\extend_default_homepage(true); + \core\di::get(\core\hook\manager::class)->dispatch($hook); + $choices = array_merge($choices, array_keys($hook->get_options())); + $preferences['user_home_page_preference'] = [ - 'type' => PARAM_INT, 'null' => NULL_ALLOWED, 'default' => get_default_home_page(), 'choices' => $choices, diff --git a/user/classes/form/defaulthomepage_form.php b/user/classes/form/defaulthomepage_form.php index 0682929d6fae7..fe9d5ea1c8892 100644 --- a/user/classes/form/defaulthomepage_form.php +++ b/user/classes/form/defaulthomepage_form.php @@ -14,25 +14,21 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Form to allow user to set their default home page - * - * @package core_user - * @copyright 2019 Paul Holden - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace core_user\form; -use lang_string; +use core\di; +use core\hook\manager; +use core\lang_string; +use core_user\hook\extend_default_homepage; defined('MOODLE_INTERNAL') || die; require_once($CFG->dirroot . '/lib/formslib.php'); /** - * Form class + * Form to allow user to set their default home page * + * @package core_user * @copyright 2019 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -55,6 +51,11 @@ public function definition() { } $options[HOMEPAGE_MYCOURSES] = new lang_string('mycourses', 'admin'); + // Allow hook callbacks to extend options. + $hook = new extend_default_homepage(true); + di::get(manager::class)->dispatch($hook); + $options += $hook->get_options(); + $mform->addElement('select', 'defaulthomepage', get_string('defaulthomepageuser'), $options); $mform->addHelpButton('defaulthomepage', 'defaulthomepageuser'); $mform->setDefault('defaulthomepage', get_default_home_page()); diff --git a/user/classes/hook/extend_default_homepage.php b/user/classes/hook/extend_default_homepage.php new file mode 100644 index 0000000000000..114a2e8e032b8 --- /dev/null +++ b/user/classes/hook/extend_default_homepage.php @@ -0,0 +1,68 @@ +. + +declare(strict_types=1); + +namespace core_user\hook; + +use core\attribute\{label, tags}; +use core\lang_string; +use core\url; + +/** + * Hook to allow callbacks to extend the default homepage options + * + * @package core_user + * @copyright 2024 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +#[label('Allows callbacks to extend the default homepage options')] +#[tags('user')] +final class extend_default_homepage { + + /** @var array $options */ + private array $options = []; + + /** + * Hook constructor + * + * @param bool $userpreference Whether this is being selected as a user preference + */ + public function __construct( + /** @var bool $userpreference Whether this is being selected as a user preference */ + public readonly bool $userpreference = false, + ) { + } + + /** + * To be called by callback to add an option + * + * @param url $url + * @param lang_string|string $title + */ + public function add_option(url $url, lang_string|string $title): void { + $this->options[(string) $url] = $title; + } + + /** + * Returns all added options + * + * @return array + */ + public function get_options(): array { + return $this->options; + } +} From 1554c68933d220299cb68b93d9675f78456781fa Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Fri, 5 Jul 2024 12:44:45 +0100 Subject: [PATCH 054/178] MDL-82391 courseformat: correct selector element section URLs. It's required by themes that don't use the course index component, e.g. Classic theme. --- .../output/local/content/sectionselector.php | 2 +- .../tests/behat/course_sections.feature | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 theme/classic/tests/behat/course_sections.feature 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/theme/classic/tests/behat/course_sections.feature b/theme/classic/tests/behat/course_sections.feature new file mode 100644 index 0000000000000..8a20775402885 --- /dev/null +++ b/theme/classic/tests/behat/course_sections.feature @@ -0,0 +1,30 @@ +@theme_classic +Feature: Select course sections using classic theme + In order to view course sections when using the classic theme + As a teacher + I need to select the section from the section selector + + Background: + Given the following "course" exists: + | fullname | Course 1 | + | shortname | C1 | + | numsections | 3 | + | initsections | 1 | + And the following "activities" exist: + | course | activity | name | idnumber | section | + | C1 | assign | Assignment 1 | assign1 | 1 | + | C1 | assign | Assignment 2 | assign2 | 2 | + + @javascript + Scenario: Use the course section selector in classic theme + Given I am on the "C1" "Course" page logged in as "admin" + And I turn editing mode on + When I choose the "View" item in the "Edit" action menu of the "Section 1" "section" + Then I should see "Section 1" + And I should see "Assignment 1" + And I should not see "Assignment 2" + And I select "Section 2" from the "jump" singleselect + And I should see "Section 2" + And I should not see "Assignment 1" + And I should see "Assignment 2" + And the "jump" select box should contain "Section 3" From 37b6eea44f4e0130a9530cf83234da47d8a98aa2 Mon Sep 17 00:00:00 2001 From: Laurent David Date: Mon, 8 Jul 2024 10:44:39 +0200 Subject: [PATCH 055/178] MDL-81265 mod_workshop: Fix accessibility issues * Page title should be h2 * Contrasts in the table headers (for submission, assessment and grading) should be 4:1 at least --- mod/workshop/renderer.php | 2 +- mod/workshop/styles.css | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/mod/workshop/renderer.php b/mod/workshop/renderer.php index 3109026b0867e..0c33d404cf314 100644 --- a/mod/workshop/renderer.php +++ b/mod/workshop/renderer.php @@ -1261,7 +1261,7 @@ public function view_page(workshop $workshop, workshop_user_plan $userplan, stri $output = ''; $output .= $this->render_action_buttons($workshop, $userplan); - $output .= $this->heading(format_string($currentphasetitle), 3, null, 'mod_workshop-userplanheading'); + $output .= $this->heading(format_string($currentphasetitle), 2, null, 'mod_workshop-userplanheading'); $output .= $this->render($userplan); $output .= $this->view_submissions_report($workshop, $userplan, $page, $sortby, $sorthow); diff --git a/mod/workshop/styles.css b/mod/workshop/styles.css index 43164e6355008..779aa7a05295a 100644 --- a/mod/workshop/styles.css +++ b/mod/workshop/styles.css @@ -243,15 +243,16 @@ .path-mod-workshop .userplan dt { vertical-align: bottom; white-space: normal; - color: #999; - border-bottom: 1px solid #ddd; + color: #495057; + background-color: #f5f5f5; + border-bottom: 1px solid #ced4da; padding: 3px; } .path-mod-workshop .userplan dt.active { vertical-align: top; - color: black; - border: 1px solid #ddd; + color: #212529; + border: 1px solid #ced4da; border-bottom: 0; border-top: 0; background: #e7f1c3; @@ -322,7 +323,7 @@ .path-mod-workshop .userplan dd { vertical-align: top; - border-right: 1px solid #ddd; + border-right: 1px solid #ced4da; background-color: #f5f5f5; margin: 0; min-height: 180px; @@ -335,7 +336,7 @@ .path-mod-workshop .userplan dd a:hover, .path-mod-workshop .userplan dd a:visited, .path-mod-workshop .userplan dd a:active { - color: #999; + color: #495057; } .path-mod-workshop .userplan dd.active, @@ -344,7 +345,7 @@ .path-mod-workshop .userplan dd.active a:hover, .path-mod-workshop .userplan dd.active a:visited, .path-mod-workshop .userplan dd.active a:active { - color: black; + color: #212529; } .path-mod-workshop .userplan dd.lastcol { From 8b92e969aefc212008bcb3f297dae4d7273fa7ac Mon Sep 17 00:00:00 2001 From: Ilya Tregubov Date: Tue, 9 Jul 2024 17:02:04 +0800 Subject: [PATCH 056/178] MDL-81765 mod_subsection: Remove undesired index. --- mod/subsection/db/install.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/mod/subsection/db/install.xml b/mod/subsection/db/install.xml index a452b5315cf13..6d5fcfc7c50ed 100644 --- a/mod/subsection/db/install.xml +++ b/mod/subsection/db/install.xml @@ -15,9 +15,6 @@ - - - From 32e090561bbee27268f956eef7877d8189e554d8 Mon Sep 17 00:00:00 2001 From: ferranrecio Date: Tue, 7 May 2024 16:45:34 +0200 Subject: [PATCH 057/178] MDL-81886 tool_generator: allow scenario outlines in testing features The tool_generator creates a testing scenario that can execute all steps from behat_data_generators (with some limitations). However, it cannot set any admin settings, which limits the tool's ability to generate many real test scenarios. --- .../classes/local/testscenario/runner.php | 21 +++++++++- .../tests/behat/testscenario.feature | 15 ++++++-- .../fixtures/testscenario/scenario.feature | 2 +- ...tline.feature => scenario_outline.feature} | 4 +- .../tests/local/testscenario/runner_test.php | 38 +++++++++++++++++++ 5 files changed, 71 insertions(+), 9 deletions(-) rename admin/tool/generator/tests/fixtures/testscenario/{scenario_wrongoutline.feature => scenario_outline.feature} (72%) diff --git a/admin/tool/generator/classes/local/testscenario/runner.php b/admin/tool/generator/classes/local/testscenario/runner.php index 8122cf5cadc5f..0ba6514f9592f 100644 --- a/admin/tool/generator/classes/local/testscenario/runner.php +++ b/admin/tool/generator/classes/local/testscenario/runner.php @@ -22,6 +22,7 @@ use Behat\Gherkin\Parser; use Behat\Gherkin\Lexer; use Behat\Gherkin\Keywords\ArrayKeywords; +use Behat\Gherkin\Node\OutlineNode; use ReflectionClass; use ReflectionMethod; use stdClass; @@ -170,8 +171,7 @@ public function parse_feature(string $content): parsedfeature { $scenarios = $feature->getScenarios(); foreach ($scenarios as $scenario) { 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 +184,23 @@ public function parse_feature(string $content): parsedfeature { return $result; } + /** + * 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 diff --git a/admin/tool/generator/tests/behat/testscenario.feature b/admin/tool/generator/tests/behat/testscenario.feature index 1a03a8bf065f5..8f013c8453582 100644 --- a/admin/tool/generator/tests/behat/testscenario.feature +++ b/admin/tool/generator/tests/behat/testscenario.feature @@ -60,10 +60,17 @@ Feature: Create testing scenarios using generators Then I should see "The file format is not valid or contains invalid steps" @javascript - Scenario: Prevent creating a testing scenario from a scenario outline + Scenario: Create a testing scenario from a scenario outline Given I log in as "admin" And I navigate to "Development > Create testing scenarios" in site administration - When I upload "admin/tool/generator/tests/fixtures/testscenario/scenario_wrongoutline.feature" file to "Feature file" filemanager + When I upload "admin/tool/generator/tests/fixtures/testscenario/scenario_outline.feature" file to "Feature file" filemanager And I press "Import" - Then I should see "Scenario outlines are not supported" - Then I should see "There are no steps to execute in the file" + And I should see "Example: creating test scenarios using an outline (1)" + And I should see "Example: creating test scenarios using an outline (2)" + And I should see "Example: creating test scenarios using an outline (3)" + Then I am on the "C1" "Course" page + And I should see "Course 1" in the "page-header" "region" + And I am on the "C2" "Course" page + And I should see "Course 2" in the "page-header" "region" + And I am on the "C3" "Course" page + And I should see "Course 3" in the "page-header" "region" diff --git a/admin/tool/generator/tests/fixtures/testscenario/scenario.feature b/admin/tool/generator/tests/fixtures/testscenario/scenario.feature index 8993270dc694a..b9bc1edad77c8 100644 --- a/admin/tool/generator/tests/fixtures/testscenario/scenario.feature +++ b/admin/tool/generator/tests/fixtures/testscenario/scenario.feature @@ -1,4 +1,4 @@ -Feature: Prepare scenario for testing +Feature: Fixture to prepare scenario for testing Scenario: Create course content Given the following config values are set as admin: | sendcoursewelcomemessage | 0 | enrol_manual | diff --git a/admin/tool/generator/tests/fixtures/testscenario/scenario_wrongoutline.feature b/admin/tool/generator/tests/fixtures/testscenario/scenario_outline.feature similarity index 72% rename from admin/tool/generator/tests/fixtures/testscenario/scenario_wrongoutline.feature rename to admin/tool/generator/tests/fixtures/testscenario/scenario_outline.feature index 904c0a3e20988..395aaff0623cc 100644 --- a/admin/tool/generator/tests/fixtures/testscenario/scenario_wrongoutline.feature +++ b/admin/tool/generator/tests/fixtures/testscenario/scenario_outline.feature @@ -1,6 +1,6 @@ -Feature: Prepare scenario for testing +Feature: Fixture to prepare scenario for testing from an outline - Scenario Outline: test outline scenarios are not supported yet + Scenario Outline: creating test scenarios using an outline Given the following "course" exists: | fullname | | | shortname | | diff --git a/admin/tool/generator/tests/local/testscenario/runner_test.php b/admin/tool/generator/tests/local/testscenario/runner_test.php index 42e2471cae81a..09bb3404e9cd7 100644 --- a/admin/tool/generator/tests/local/testscenario/runner_test.php +++ b/admin/tool/generator/tests/local/testscenario/runner_test.php @@ -104,4 +104,42 @@ public function test_parse_and_execute_wrong_feature(): void { $this->assertFalse($result); $this->assertEquals(0, $DB->count_records('course', ['shortname' => 'C1'])); } + + /** + * Test for parse_feature. + * @covers ::parse_feature + * @covers ::execute + */ + public function test_parse_and_execute_outline_feature(): void { + global $CFG, $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + + // Call the init method to include all behat libraries and attributes. + $runner = new runner(); + $runner->init(); + + $featurefile = $CFG->dirroot . '/admin/tool/generator/tests/fixtures/testscenario/scenario_outline.feature'; + $contents = file_get_contents($featurefile); + $feature = $runner->parse_feature($contents); + + $this->assertEquals(3, count($feature->get_scenarios())); + $this->assertEquals(3, count($feature->get_all_steps())); + $this->assertTrue($feature->is_valid()); + + $result = $runner->execute($feature); + $this->assertTrue($result); + + // Validate everything is created. + $course = $DB->get_record('course', ['shortname' => 'C1']); + $this->assertEquals('C1', $course->shortname); + $this->assertEquals('Course 1', $course->fullname); + $course = $DB->get_record('course', ['shortname' => 'C2']); + $this->assertEquals('C2', $course->shortname); + $this->assertEquals('Course 2', $course->fullname); + $course = $DB->get_record('course', ['shortname' => 'C3']); + $this->assertEquals('C3', $course->shortname); + $this->assertEquals('Course 3', $course->fullname); + } } From bd0f8a058f4ff8efa64fc5ebe89d823e49bd5fb8 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Wed, 10 Jul 2024 11:41:31 +0100 Subject: [PATCH 058/178] MDL-79717 phpunit: ensure unique data provider keys in tests. Duplicate data provider keys were overwriting and/or duplicating one another, leading to some cases being skipped. Other "duplicate array key" errors were picked up by `phpcs` in this dragnet across all tests, which have also been fixed. --- backup/moodle2/tests/moodle2_test.php | 3 +- course/tests/externallib_test.php | 27 ++----------- .../local/ltiadvantage/entity/user_test.php | 16 ++++---- enrol/tests/externallib_test.php | 8 ++-- grade/report/history/tests/report_test.php | 38 +++++-------------- lib/tests/moodlelib_test.php | 12 +++--- mod/chat/tests/dates_test.php | 22 +++++------ mod/feedback/tests/external/external_test.php | 24 +----------- mod/h5pactivity/tests/generator_test.php | 14 ++----- mod/h5pactivity/tests/lib_test.php | 4 +- mod/h5pactivity/tests/xapi/handler_test.php | 16 ++++---- mod/lesson/tests/custom_completion_test.php | 8 ++-- .../ddimageortext/tests/question_test.php | 5 +-- .../type/ddmarker/tests/question_test.php | 5 +-- .../tests/upgrade_old_attempt_data_test.php | 9 +---- .../numerical/tests/privacy/provider_test.php | 6 +-- user/tests/table/participants_search_test.php | 20 +++------- 17 files changed, 75 insertions(+), 162 deletions(-) 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/course/tests/externallib_test.php b/course/tests/externallib_test.php index 89959169394b6..5510168c63dc8 100644 --- a/course/tests/externallib_test.php +++ b/course/tests/externallib_test.php @@ -39,8 +39,7 @@ * @copyright 2012 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class externallib_test extends externallib_advanced_testcase { - //core_course_externallib_testcase +final class externallib_test extends externallib_advanced_testcase { /** * Tests set up @@ -609,7 +608,7 @@ public function test_create_courses(): void { * * @return array */ - public function course_empty_field_provider(): array { + public static function course_empty_field_provider(): array { return [ [[ 'fullname' => '', @@ -3304,7 +3303,7 @@ public function test_check_updates(): void { /** * Test cases for the get_enrolled_courses_by_timeline_classification test. */ - public function get_get_enrolled_courses_by_timeline_classification_test_cases(): array { + public static function get_get_enrolled_courses_by_timeline_classification_test_cases(): array { $now = time(); $day = 86400; @@ -3561,16 +3560,6 @@ public function get_get_enrolled_courses_by_timeline_classification_test_cases() 'expectednextoffset' => 0, 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()', ], - 'all limit and offset with wrong sort direction' => [ - 'coursedata' => $coursedata, - 'classification' => 'all', - 'limit' => 5, - 'offset' => 5, - 'sort' => "ul.timeaccess abcdasc", - 'expectedcourses' => [], - 'expectednextoffset' => 0, - 'expectedexception' => 'Invalid sort direction in $sort parameter in enrol_get_my_courses()', - ], 'all limit and offset with wrong sort direction' => [ 'coursedata' => $coursedata, 'classification' => 'all', @@ -3591,16 +3580,6 @@ public function get_get_enrolled_courses_by_timeline_classification_test_cases() 'expectednextoffset' => 0, 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()', ], - 'all limit and offset with wrong field name' => [ - 'coursedata' => $coursedata, - 'classification' => 'all', - 'limit' => 5, - 'offset' => 5, - 'sort' => "ul.foobar", - 'expectedcourses' => [], - 'expectednextoffset' => 0, - 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()', - ], 'all limit and offset with wrong field separator' => [ 'coursedata' => $coursedata, 'classification' => 'all', diff --git a/enrol/lti/tests/local/ltiadvantage/entity/user_test.php b/enrol/lti/tests/local/ltiadvantage/entity/user_test.php index 69aa28e77a677..7ce0b334e49df 100644 --- a/enrol/lti/tests/local/ltiadvantage/entity/user_test.php +++ b/enrol/lti/tests/local/ltiadvantage/entity/user_test.php @@ -24,7 +24,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\entity\user */ -class user_test extends \advanced_testcase { +final class user_test extends \advanced_testcase { /** * Test creation of a user instance using the factory method. @@ -64,7 +64,7 @@ public function test_create(array $args, array $expectations): void { * * @return array the data for testing. */ - public function create_data_provider(): array { + public static function create_data_provider(): array { global $CFG; return [ 'Valid create, only required args provided' => [ @@ -272,7 +272,7 @@ public function test_create_from_resource_link(array $args, array $expectations) * * @return array the data for testing. */ - public function create_from_resource_link_data_provider(): array { + public static function create_from_resource_link_data_provider(): array { global $CFG; return [ 'Valid creation, all args provided explicitly' => [ @@ -511,7 +511,7 @@ public function test_setters_and_getters(string $methodname, $arg, array $expect * * @return array the array of test data. */ - public function setters_getters_data_provider(): array { + public static function setters_getters_data_provider(): array { global $CFG; return [ 'Testing set_resourcelinkid with valid id' => [ @@ -620,12 +620,12 @@ public function setters_getters_data_provider(): array { ], 'Testing set_maildisplay with a valid int: 2' => [ 'methodname' => 'maildisplay', - 'arg' => '1', + 'arg' => '2', 'expectations' => [ 'valid' => true, ] ], - 'Testing set_maildisplay with invalid int' => [ + 'Testing set_maildisplay with invalid int: -1' => [ 'methodname' => 'maildisplay', 'arg' => '-1', 'expectations' => [ @@ -634,7 +634,7 @@ public function setters_getters_data_provider(): array { 'exceptionmessage' => "Invalid maildisplay value '-1'. Must be in the range {0..2}." ] ], - 'Testing set_maildisplay with invalid int' => [ + 'Testing set_maildisplay with invalid int: 3' => [ 'methodname' => 'maildisplay', 'arg' => '3', 'expectations' => [ @@ -659,7 +659,7 @@ public function setters_getters_data_provider(): array { 'exceptionmessage' => 'Invalid lang value. Cannot be an empty string.' ] ], - 'Testing set_lang with an empty string' => [ + 'Testing set_lang with invalid lang code' => [ 'methodname' => 'lang', 'arg' => 'ff', 'expectations' => [ diff --git a/enrol/tests/externallib_test.php b/enrol/tests/externallib_test.php index 17619314301a1..02c8c8e7dadd0 100644 --- a/enrol/tests/externallib_test.php +++ b/enrol/tests/externallib_test.php @@ -38,12 +38,12 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.4 */ -class externallib_test extends externallib_advanced_testcase { +final class externallib_test extends externallib_advanced_testcase { /** * dataProvider for test_get_enrolled_users_visibility(). */ - public function get_enrolled_users_visibility_provider() { + public static function get_enrolled_users_visibility_provider(): array { return array( 'Course without groups, default behavior (not filtering by cap, group, active)' => array( @@ -232,7 +232,7 @@ public function get_enrolled_users_visibility_provider() { ), ), - 'Course with separate groups, filtering by withcapability (having moodle/role:review)' => + 'Course with separate groups, filtering by withcapability (having moodle/role:review & moodle/course:bulkmessaging)' => array( 'settings' => array( 'coursegroupmode' => SEPARATEGROUPS, @@ -1125,7 +1125,7 @@ public function test_get_enrolled_users_with_capability_including_lastcourseacce /** * dataProvider for test_submit_user_enrolment_form(). */ - public function submit_user_enrolment_form_provider() { + public static function submit_user_enrolment_form_provider(): array { $now = new \DateTime(); $nextmonth = clone($now); diff --git a/grade/report/history/tests/report_test.php b/grade/report/history/tests/report_test.php index 013adb895df3d..3d4689c2276f6 100644 --- a/grade/report/history/tests/report_test.php +++ b/grade/report/history/tests/report_test.php @@ -25,7 +25,7 @@ * @copyright 2014 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class report_test extends \advanced_testcase { +final class report_test extends \advanced_testcase { /** * Create some grades. @@ -251,7 +251,7 @@ public function test_get_users(): void { * * @return array List of data sets (test cases) */ - public function get_users_with_profile_fields_provider(): array { + public static function get_users_with_profile_fields_provider(): array { return [ // User identity check boxes, 'email', 'profile_field_lang' and 'profile_field_height' are checked. 'show email,lang and height;search for all users' => @@ -264,24 +264,18 @@ public function get_users_with_profile_fields_provider(): array { ['email,profile_field_lang,profile_field_height', '.uk', ['u3']], 'show email,lang and height,search for Spanish speakers' => ['email,profile_field_lang,profile_field_height', 'spanish', ['u1', 'u4']], - 'show email,lang and height,search for Spanish speakers' => + 'show email,lang and height,search for Spanish speakers (using spa)' => ['email,profile_field_lang,profile_field_height', 'spa', ['u1', 'u4']], 'show email,lang and height,search for German speakers' => ['email,profile_field_lang,profile_field_height', 'german', ['u2']], - 'show email,lang and height,search for German speakers' => + 'show email,lang and height,search for German speakers (using ger)' => ['email,profile_field_lang,profile_field_height', 'ger', ['u2']], 'show email,lang and height,search for English speakers' => ['email,profile_field_lang,profile_field_height', 'english', ['u3']], - 'show email,lang and height,search for English speakers' => + 'show email,lang and height,search for English speakers (using eng)' => ['email,profile_field_lang,profile_field_height', 'eng', ['u3']], - 'show email,lang and height,search for English speakers' => - ['email,profile_field_lang,profile_field_height', 'ish', ['u3']], - 'show email,lang and height,search for users with height 180cm' => - ['email,profile_field_lang,profile_field_height', '180cm', ['u2', 'u3', 'u4']], 'show email,lang and height,search for users with height 180cm' => ['email,profile_field_lang,profile_field_height', '180', ['u2', 'u3', 'u4']], - 'show email,lang and height,search for users with height 170cm' => - ['email,profile_field_lang,profile_field_height', '170cm', ['u1']], 'show email,lang and height,search for users with height 170cm' => ['email,profile_field_lang,profile_field_height', '170', ['u1']], @@ -292,25 +286,15 @@ public function get_users_with_profile_fields_provider(): array { ['email,profile_field_height', '.com', []], 'show email and height;search for users on .co' => ['email,profile_field_height', '.co', ['u3']], - 'show email and height,search for Spanish/German/English speakers' => + 'show email and height,search for Spanish speakers' => ['email,profile_field_height', 'spanish', []], - 'show email and height,search for Spanish/German/English speakers' => + 'show email and height,search for German speakers' => ['email,profile_field_height', 'german', []], - 'show email and height,search for Spanish/German/English speakers' => - ['email,profile_field_height', 'english', []], - 'show email,lang and height,search for English speakers' => - ['email,profile_field_height', 'english', []], 'show email and height,search for English speakers' => - ['email,profile_field_height', 'eng', []], - 'show email and height,search for English speakers' => - ['email,profile_field_height', 'ish', []], + ['email,profile_field_height', 'english', []], 'show email and height,search for users with height 180cm' => - ['email,profile_field_height', '180cm', ['u2', 'u3', 'u4']], - 'show email,lang and height,search for users with height 180cm' => ['email,profile_field_height', '180', ['u2', 'u3', 'u4']], - 'show email,lang and height,search for users with height 170cm' => - ['email,profile_field_height', '170cm', ['u1']], - 'show email,lang and height,search for users with height 170cm' => + 'show email and height,search for users with height 170cm' => ['email,profile_field_height', '170', ['u1']], // User identity check boxes, only 'email' is checked. @@ -322,9 +306,7 @@ public function get_users_with_profile_fields_provider(): array { 'show email only;search for Spanish speakers' => ['email', 'spanish', []], 'show email only;search for German speakers' => ['email', 'german', []], 'show email only;search for English speakers' => ['email', 'english', []], - 'show email only;search for users with height 180cm' => ['email', '180cm', []], 'show email only;search for users with height 180cm' => ['email', '180', []], - 'show email only;search for users with height 170cm' => ['email', '170cm', []], 'show email only;search for users with height 170cm' => ['email', '170', []], ]; } @@ -401,7 +383,7 @@ public function test_get_users_with_profile_fields(string $showuseridentity, str /** * Data provider method for \gradereport_history_report_testcase::test_get_users_with_groups() */ - public function get_users_provider() { + public static function get_users_provider(): array { return [ 'Visible groups, non-editing teacher, not in any group' => [ VISIBLEGROUPS, 'teacher', ['g1', 'g2'], ['s1', 's2', 's3', 's4', 's5'] diff --git a/lib/tests/moodlelib_test.php b/lib/tests/moodlelib_test.php index 6a3ce1cbeae14..8b569e79cfce0 100644 --- a/lib/tests/moodlelib_test.php +++ b/lib/tests/moodlelib_test.php @@ -25,7 +25,7 @@ * @author T.J.Hunt@open.ac.uk * @author nicolas@moodle.com */ -class moodlelib_test extends \advanced_testcase { +final class moodlelib_test extends \advanced_testcase { /** * Define a local decimal separator. @@ -858,13 +858,15 @@ public function test_clean_param_timezone(): void { '-13.5' => '', '0.2' => '', '' => '', - null => '', ); foreach ($testvalues as $testvalue => $expectedvalue) { $actualvalue = clean_param($testvalue, PARAM_TIMEZONE); $this->assertEquals($expectedvalue, $actualvalue); } + + // Test for null. + $this->assertEquals('', clean_param(null, PARAM_TIMEZONE)); } /** @@ -3575,9 +3577,9 @@ public function test_setnew_password_and_mail(): void { /** * Data provider for test_generate_confirmation_link - * @return Array of confirmation urls and expected resultant confirmation links + * @return array Confirmation urls and expected resultant confirmation links */ - public function generate_confirmation_link_provider() { + public static function generate_confirmation_link_provider(): array { global $CFG; return [ "Simple name" => [ @@ -3645,7 +3647,7 @@ public function generate_confirmation_link_provider() { "confirmationurl" => "http://moodle.org/ext.php?with=some¶m=eters", "expected" => "http://moodle.org/ext.php?with=some¶m=eters&data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E" ], - "Custom external confirmation url with parameters" => [ + "Custom external confirmation url with parameters (again)" => [ "username" => "many_-.@characters@_@-..-..", "confirmationurl" => "http://moodle.org/ext.php?with=some&data=test", "expected" => "http://moodle.org/ext.php?with=some&data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E" diff --git a/mod/chat/tests/dates_test.php b/mod/chat/tests/dates_test.php index 8bfacdbec3db6..41e31d0981363 100644 --- a/mod/chat/tests/dates_test.php +++ b/mod/chat/tests/dates_test.php @@ -14,15 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Contains unit tests for mod_chat\dates. - * - * @package mod_chat - * @category test - * @copyright 2021 Dongsheng Cai - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - declare(strict_types=1); namespace mod_chat; @@ -33,8 +24,13 @@ /** * Class for unit testing mod_chat\dates. + * + * @package mod_chat + * @category test + * @copyright 2021 Dongsheng Cai + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class dates_test extends advanced_testcase { +final class dates_test extends advanced_testcase { /** * Setup testcase. @@ -50,7 +46,7 @@ public function setUp(): void { * Data provider for get_dates_for_module(). * @return array[] */ - public function get_dates_for_module_provider(): array { + public static function get_dates_for_module_provider(): array { global $CFG; require_once($CFG->dirroot . '/mod/chat/lib.php'); @@ -62,10 +58,10 @@ public function get_dates_for_module_provider(): array { $weeklynextchattime = $past + 7 * DAYSECS; $label = get_string('nextchattime', 'mod_chat'); return [ - 'chattime in the past' => [ + 'chattime in the past (no schedule)' => [ $past, CHAT_SCHEDULE_NONE, [] ], - 'chattime in the past' => [ + 'chattime in the past (single schedule)' => [ $past, CHAT_SCHEDULE_SINGLE, [] ], 'chattime in the future' => [ diff --git a/mod/feedback/tests/external/external_test.php b/mod/feedback/tests/external/external_test.php index 5c46942486113..6d72b05534304 100644 --- a/mod/feedback/tests/external/external_test.php +++ b/mod/feedback/tests/external/external_test.php @@ -14,16 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Feedback module external functions tests - * - * @package mod_feedback - * @category external - * @copyright 2017 Juan Leyva - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 3.3 - */ - namespace mod_feedback\external; use core_external\external_api; @@ -49,7 +39,7 @@ * @since Moodle 3.3 * @covers \mod_feedback_external */ -class external_test extends externallib_advanced_testcase { +final class external_test extends externallib_advanced_testcase { // TODO These should be removed. // Testcase classes should not have any properties or store state. @@ -460,7 +450,7 @@ public function test_get_items_validation(string $role, array $info, ?string $wa * * @return array */ - public function items_provider(): array { + public static function items_provider(): array { return [ 'Valid feedback (as student)' => [ 'role' => 'student', @@ -477,11 +467,6 @@ public function items_provider(): array { 'info' => ['empty' => true], 'warning' => get_string('no_items_available_yet', 'feedback'), ], - 'Closed feedback (as student)' => [ - 'role' => 'student', - 'info' => ['closed' => true], - 'warning' => get_string('feedback_is_not_open', 'feedback'), - ], 'Cannot complete feedback (as student)' => [ 'role' => 'student', 'info' => ['complete' => false], @@ -502,11 +487,6 @@ public function items_provider(): array { 'info' => ['empty' => true], 'warning' => null, ], - 'Closed feedback (as teacher)' => [ - 'role' => 'teacher', - 'info' => ['closed' => true], - 'warning' => null, - ], 'Cannot complete feedback (as teacher)' => [ 'role' => 'teacher', 'info' => ['complete' => false], diff --git a/mod/h5pactivity/tests/generator_test.php b/mod/h5pactivity/tests/generator_test.php index f6bc87cc4453b..fbe6a95733aed 100644 --- a/mod/h5pactivity/tests/generator_test.php +++ b/mod/h5pactivity/tests/generator_test.php @@ -26,7 +26,7 @@ * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class generator_test extends \advanced_testcase { +final class generator_test extends \advanced_testcase { /** * Test on H5P activity creation. @@ -158,7 +158,7 @@ public function test_create_attempt(array $tracks, int $attempts, int $results, * * @return array */ - public function create_attempt_data(): array { + public static function create_attempt_data(): array { return [ 'Compound statement' => [ [ @@ -224,14 +224,6 @@ public function create_attempt_data(): array { ], ], 1, 1, false, ], - 'Other statement' => [ - [ - [ - 'interactiontype' => 'other', 'attempt' => 1, 'rawscore' => 2, - 'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0 - ], - ], 1, 1, false, - ], 'No graded statement' => [ [ [ @@ -351,7 +343,7 @@ public function test_create_attempt_exceptions(bool $validmod, bool $validuser): * * @return array */ - public function create_attempt_exceptions_data(): array { + public static function create_attempt_exceptions_data(): array { return [ 'Invalid user' => [true, false], 'Invalid activity' => [false, true], diff --git a/mod/h5pactivity/tests/lib_test.php b/mod/h5pactivity/tests/lib_test.php index 038cb084907e0..7633f21670f79 100644 --- a/mod/h5pactivity/tests/lib_test.php +++ b/mod/h5pactivity/tests/lib_test.php @@ -27,7 +27,7 @@ * @copyright 2021 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class lib_test extends advanced_testcase { +final class lib_test extends advanced_testcase { /** * Load required test libraries @@ -80,7 +80,6 @@ public function test_h5pactivity_delete_instance(): void { 'filearea' => 'package', 'itemid' => 0, 'filepath' => '/', - 'filepath' => '/', 'filename' => 'dummy.h5p', 'addxapistate' => true, ]; @@ -374,7 +373,6 @@ public function test_h5pactivity_reset_userdata(): void { 'filearea' => 'package', 'itemid' => 0, 'filepath' => '/', - 'filepath' => '/', 'filename' => 'dummy.h5p', 'addxapistate' => true, ]; diff --git a/mod/h5pactivity/tests/xapi/handler_test.php b/mod/h5pactivity/tests/xapi/handler_test.php index 82ff40a23cb91..acc585be693a0 100644 --- a/mod/h5pactivity/tests/xapi/handler_test.php +++ b/mod/h5pactivity/tests/xapi/handler_test.php @@ -35,7 +35,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \mod_h5pactivity\xapi\handler */ -class handler_test extends \advanced_testcase { +final class handler_test extends \advanced_testcase { /** * Setup to ensure that fixtures are loaded. @@ -203,7 +203,7 @@ public function test_xapi_handler_errors(bool $hasverb, bool $hasdefinition, boo * * @return array */ - public function xapi_handler_errors_data(): array { + public static function xapi_handler_errors_data(): array { return [ // Invalid Definitions and results possibilities. 'Invalid definition and result' => [ @@ -222,17 +222,17 @@ public function xapi_handler_errors_data(): array { 'Invalid verb and result' => [ false, true, false, true, true, false ], - 'Invalid verb and result' => [ + 'Invalid verb and definition' => [ false, false, true, true, true, false ], // Invalid context possibilities. 'Invalid definition, result and context' => [ true, false, false, false, true, false ], - 'Invalid result' => [ + 'Invalid result and context' => [ true, true, false, false, true, false ], - 'Invalid result and context' => [ + 'Invalid definition and context' => [ true, false, true, false, true, false ], 'Invalid verb, definition result and context' => [ @@ -241,7 +241,7 @@ public function xapi_handler_errors_data(): array { 'Invalid verb, result and context' => [ false, true, false, false, true, false ], - 'Invalid verb, result and context' => [ + 'Invalid verb, definition and context' => [ false, false, true, false, true, false ], // Invalid user possibilities. @@ -260,7 +260,7 @@ public function xapi_handler_errors_data(): array { 'Invalid verb, result and user' => [ false, true, false, true, false, false ], - 'Invalid verb, result and user' => [ + 'Invalid verb, definition and user' => [ false, false, true, true, false, false ], 'Invalid definition, result, context and user' => [ @@ -278,7 +278,7 @@ public function xapi_handler_errors_data(): array { 'Invalid verb, result, context and user' => [ false, true, false, false, false, false ], - 'Invalid verb, result, context and user' => [ + 'Invalid verb, definition, context and user' => [ false, false, true, false, false, false ], ]; diff --git a/mod/lesson/tests/custom_completion_test.php b/mod/lesson/tests/custom_completion_test.php index 07c8c83618e2c..f0e1a14dd6616 100644 --- a/mod/lesson/tests/custom_completion_test.php +++ b/mod/lesson/tests/custom_completion_test.php @@ -36,14 +36,14 @@ * @copyright 2021 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class custom_completion_test extends advanced_testcase { +final class custom_completion_test extends advanced_testcase { /** * Data provider for get_state(). * * @return array[] */ - public function get_state_provider(): array { + public static function get_state_provider(): array { return [ 'Undefined completion requirement' => [ 'somenonexistentrule', COMPLETION_ENABLED, 3, null, coding_exception::class @@ -66,7 +66,7 @@ public function get_state_provider(): array { 'User must reach end of lesson, has not met completion requirement' => [ 'completionendreached', 1, false, COMPLETION_INCOMPLETE, null ], - 'User must reach end of lesson, has not met completion requirement' => [ + 'User must reach end of lesson, has met completion requirement' => [ 'completionendreached', 1, true, COMPLETION_COMPLETE, null ], ]; @@ -197,7 +197,7 @@ public function test_is_defined(): void { * * @return array[] */ - public function get_available_custom_rules_provider(): array { + public static function get_available_custom_rules_provider(): array { return [ 'No completion conditions enabled' => [ [ diff --git a/question/type/ddimageortext/tests/question_test.php b/question/type/ddimageortext/tests/question_test.php index 6dd3cb69147d9..97a7c6f100892 100644 --- a/question/type/ddimageortext/tests/question_test.php +++ b/question/type/ddimageortext/tests/question_test.php @@ -26,7 +26,6 @@ require_once($CFG->dirroot . '/question/engine/tests/helpers.php'); require_once($CFG->dirroot . '/question/type/ddimageortext/tests/helper.php'); - /** * Unit tests for the matching question definition class. * @@ -35,7 +34,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers qtype_ddimageortext_question */ -class question_test extends \basic_testcase { +final class question_test extends \basic_testcase { public function test_get_question_summary(): void { $dd = \test_question_maker::make_question('ddimageortext'); @@ -224,7 +223,7 @@ public function test_is_gradable_response(): void { $this->assertFalse($dd->is_gradable_response(array())); $this->assertFalse($dd->is_gradable_response( - array('p1' => '', 'p2' => '', 'p3' => '', 'p3' => ''))); + array('p1' => '', 'p2' => '', 'p3' => ''))); $this->assertTrue($dd->is_gradable_response( array('p1' => '1', 'p2' => '1', 'p3' => ''))); $this->assertTrue($dd->is_gradable_response(array('p1' => '1'))); diff --git a/question/type/ddmarker/tests/question_test.php b/question/type/ddmarker/tests/question_test.php index cba7f05d999b3..98c82d994734f 100644 --- a/question/type/ddmarker/tests/question_test.php +++ b/question/type/ddmarker/tests/question_test.php @@ -26,7 +26,6 @@ require_once($CFG->dirroot . '/question/engine/tests/helpers.php'); require_once($CFG->dirroot . '/question/type/ddmarker/tests/helper.php'); - /** * Unit tests for the drag-and-drop markers question definition class. * @@ -34,7 +33,7 @@ * @copyright 2012 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class question_test extends \basic_testcase { +final class question_test extends \basic_testcase { public function test_get_question_summary(): void { $dd = \test_question_maker::make_question('ddmarker'); @@ -242,7 +241,7 @@ public function test_is_gradable_response(): void { $this->assertFalse($dd->is_gradable_response(array())); $this->assertFalse($dd->is_gradable_response( - array('c1' => '', 'c2' => '', 'c3' => '', 'c3' => ''))); + array('c1' => '', 'c2' => '', 'c3' => ''))); $this->assertTrue($dd->is_gradable_response( array('c1' => '300,300', 'c2' => '300,300', 'c3' => ''))); $this->assertTrue($dd->is_gradable_response(array('c1' => '300,300'))); diff --git a/question/type/multichoice/tests/upgrade_old_attempt_data_test.php b/question/type/multichoice/tests/upgrade_old_attempt_data_test.php index 38b353d2cbd50..784c6b45e99d2 100644 --- a/question/type/multichoice/tests/upgrade_old_attempt_data_test.php +++ b/question/type/multichoice/tests/upgrade_old_attempt_data_test.php @@ -21,7 +21,6 @@ global $CFG; require_once($CFG->dirroot . '/question/engine/upgrade/tests/helper.php'); - /** * Testing the upgrade of multichoice question attempts. * @@ -29,7 +28,7 @@ * @copyright 2009 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class upgrade_old_attempt_data_test extends \question_attempt_upgrader_test_base { +final class upgrade_old_attempt_data_test extends \question_attempt_upgrader_test_base { public function test_multichoice_deferredfeedback_history960(): void { $quiz = (object) array( @@ -37,13 +36,10 @@ public function test_multichoice_deferredfeedback_history960(): void { 'course' => '3', 'name' => 'Test quiz 1', 'intro' => '', - 'introformat' => FORMAT_HTML, - 'questiondecimalpoints' => '-1', - 'showuserpicture' => '1', + 'introformat' => FORMAT_MOODLE, 'showblocks' => '1', 'timeopen' => '0', 'timeclose' => '0', - 'preferredbehaviour' => 'deferredfeedback', 'attempts' => '0', 'attemptonlast' => '0', 'grademethod' => '1', @@ -63,7 +59,6 @@ public function test_multichoice_deferredfeedback_history960(): void { 'grade' => '10.00000', 'questiondecimalpoints' => '-1', 'showuserpicture' => '0', - 'introformat' => '0', 'preferredbehaviour' => 'deferredfeedback', 'reviewattempt' => '69904', 'reviewcorrectness' => '69904', diff --git a/question/type/numerical/tests/privacy/provider_test.php b/question/type/numerical/tests/privacy/provider_test.php index 2c0f920b2cbcb..2fe715d8f2e94 100644 --- a/question/type/numerical/tests/privacy/provider_test.php +++ b/question/type/numerical/tests/privacy/provider_test.php @@ -41,7 +41,7 @@ * @copyright 2021 The Open university * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class provider_test extends \core_privacy\tests\provider_testcase { +final class provider_test extends \core_privacy\tests\provider_testcase { // Include the privacy helper which has assertions on it. public function test_get_metadata(): void { @@ -91,7 +91,7 @@ public function test_export_user_preferences($name, $value, $expected): void { * * @return array Array of valid user preferences. */ - public function user_preference_provider() { + public static function user_preference_provider(): array { return [ 'default mark 1.5' => ['defaultmark', 1.5, 1.5], 'penalty 20%' => ['penalty', 0.2000000, '20%'], @@ -113,7 +113,7 @@ public function user_preference_provider() { 'multichoice display select menu' => ['multichoicedisplay', \qtype_numerical::UNITSELECT, get_string('unitselect', 'qtype_numerical')], 'unitsleft left example' => ['unitsleft', '1', get_string('leftexample', 'qtype_numerical')], - 'unitsleft left example' => ['unitsleft', '0', get_string('rightexample', 'qtype_numerical')] + 'unitsleft right example' => ['unitsleft', '0', get_string('rightexample', 'qtype_numerical')], ]; } } diff --git a/user/tests/table/participants_search_test.php b/user/tests/table/participants_search_test.php index 5d3a96b6d6b30..d3470db902d9d 100644 --- a/user/tests/table/participants_search_test.php +++ b/user/tests/table/participants_search_test.php @@ -14,15 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Provides {@link core_user_table_participants_search_test} class. - * - * @package core_user - * @category test - * @copyright 2020 Andrew Nicols - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - declare(strict_types=1); namespace core_user\table; @@ -41,10 +32,12 @@ /** * Tests for the implementation of {@link core_user_table_participants_search} class. * + * @package core_user + * @category test * @copyright 2020 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class participants_search_test extends advanced_testcase { +final class participants_search_test extends advanced_testcase { /** * Helper to convert a moodle_recordset to an array of records. @@ -190,7 +183,7 @@ public function test_roles_filter(array $usersdata, array $testroles, int $joint * * @return array */ - public function role_provider(): array { + public static function role_provider(): array { $tests = [ // Users who only have one role each. 'Users in each role' => (object) [ @@ -693,16 +686,15 @@ public function role_provider(): array { 'NONE: Filter on student, teacher' => (object) [ 'roles' => ['student', 'teacher'], 'jointype' => filter::JOINTYPE_NONE, - 'count' => 5, + 'count' => 4, 'expectedusers' => [ 'c', 'd', - 'e', 'g', 'h', ], ], - 'NONE: Filter on student, teacher' => (object) [ + 'NONE: Filter on teacher, editingteacher' => (object) [ 'roles' => ['teacher', 'editingteacher'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 3, From 9d621f5da1af4b7546aec5bd7fa0e83091151a4e Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Tue, 9 Jul 2024 16:33:47 +0100 Subject: [PATCH 059/178] MDL-82066 user: define methods for handling default homepage URLs. Per the previous commit, default homepage configuration (set either for the site or as a user preference) can now be extended by third party hook callbacks, in which case a URL is stored. --- .upgradenotes/MDL-82066-2024070915322412.yml | 12 +++++ index.php | 12 +++-- lib/moodlelib.php | 37 ++++++++++++++- lib/navigationlib.php | 22 ++++----- lib/tests/moodlelib_test.php | 47 +++++++++++++++++-- login/lib.php | 3 ++ user/tests/behat/contact_site_support.feature | 4 +- 7 files changed, 114 insertions(+), 23 deletions(-) create mode 100644 .upgradenotes/MDL-82066-2024070915322412.yml 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/index.php b/index.php index 8e0058c35e539..b707628772789 100644 --- a/index.php +++ b/index.php @@ -33,10 +33,13 @@ redirect_if_major_upgrade_required(); +// Redirect logged-in users to homepage if required. +$redirect = optional_param('redirect', 1, PARAM_BOOL); + $urlparams = array(); if (!empty($CFG->defaulthomepage) && ($CFG->defaulthomepage == HOMEPAGE_MY || $CFG->defaulthomepage == HOMEPAGE_MYCOURSES) && - optional_param('redirect', 1, PARAM_BOOL) === 0 + $redirect === 0 ) { $urlparams['redirect'] = 0; } @@ -68,9 +71,8 @@ // If site registration needs updating, redirect. \core\hub\registration::registration_reminder('/index.php'); -if (get_home_page() != HOMEPAGE_SITE) { - // Redirect logged-in users to My Moodle overview if required. - $redirect = optional_param('redirect', 1, PARAM_BOOL); +$homepage = get_home_page(); +if ($homepage != HOMEPAGE_SITE) { if (optional_param('setdefaulthome', false, PARAM_BOOL)) { set_user_preference('user_home_page_preference', HOMEPAGE_SITE); } else if (!empty($CFG->defaulthomepage) && ($CFG->defaulthomepage == HOMEPAGE_MY) && $redirect === 1) { @@ -78,6 +80,8 @@ redirect($CFG->wwwroot .'/my/'); } else if (!empty($CFG->defaulthomepage) && ($CFG->defaulthomepage == HOMEPAGE_MYCOURSES) && $redirect === 1) { redirect($CFG->wwwroot .'/my/courses.php'); + } else if ($homepage == HOMEPAGE_URL) { + redirect(get_default_home_page_url()); } else if (!empty($CFG->defaulthomepage) && ($CFG->defaulthomepage == HOMEPAGE_USER)) { $frontpagenode = $PAGE->settingsnav->find('frontpage', null); if ($frontpagenode) { diff --git a/lib/moodlelib.php b/lib/moodlelib.php index c3c1f4471a4b4..196cebe068866 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -533,6 +533,10 @@ * The home page should be the users my courses page */ define('HOMEPAGE_MYCOURSES', 3); +/** + * The home page is defined as a URL + */ +define('HOMEPAGE_URL', 4); /** * URL of the Moodle sites registration portal. @@ -9910,12 +9914,16 @@ function get_home_page() { } else if ($CFG->defaulthomepage == HOMEPAGE_MYCOURSES && !isguestuser()) { return HOMEPAGE_MYCOURSES; } else if ($CFG->defaulthomepage == HOMEPAGE_USER && !isguestuser()) { - $userhomepage = (int) get_user_preferences('user_home_page_preference', $defaultpage); + $userhomepage = get_user_preferences('user_home_page_preference', $defaultpage); if (empty($CFG->enabledashboard) && $userhomepage == HOMEPAGE_MY) { // If the user was using the dashboard but it's disabled, return the default home page. $userhomepage = $defaultpage; + } else if (clean_param($userhomepage, PARAM_LOCALURL)) { + return HOMEPAGE_URL; } - return $userhomepage; + return (int) $userhomepage; + } else if (clean_param($CFG->defaulthomepage, PARAM_LOCALURL)) { + return HOMEPAGE_URL; } } return HOMEPAGE_SITE; @@ -9933,6 +9941,31 @@ function get_default_home_page(): int { return (!isset($CFG->enabledashboard) || $CFG->enabledashboard) ? HOMEPAGE_MY : HOMEPAGE_MYCOURSES; } +/** + * Get the default home page as a URL where it has been configured as one via site configuration or user preference + * + * It is assumed that callers have already checked that {@see get_home_page} returns {@see HOMEPAGE_URL} prior to + * calling this method + * + * @return \core\url|null + */ +function get_default_home_page_url(): ?\core\url { + global $CFG; + + if ($defaulthomepage = clean_param($CFG->defaulthomepage, PARAM_LOCALURL)) { + return new \core\url($defaulthomepage); + } + + if ($CFG->defaulthomepage == HOMEPAGE_USER) { + $userhomepage = get_user_preferences('user_home_page_preference'); + if ($userhomepage = clean_param($userhomepage, PARAM_LOCALURL)) { + return new \core\url($userhomepage); + } + } + + return null; +} + /** * Gets the name of a course to be displayed when showing a list of courses. * By default this is just $course->fullname but user can configure it. The diff --git a/lib/navigationlib.php b/lib/navigationlib.php index 60ddb51602efd..24a175bb3214b 100644 --- a/lib/navigationlib.php +++ b/lib/navigationlib.php @@ -1352,14 +1352,14 @@ public function __construct(moodle_page $page) { } $homepage = get_home_page(); - if ($homepage == HOMEPAGE_SITE) { - // We are using the site home for the root element. + if ($homepage == HOMEPAGE_MY) { + // We are using the users my moodle for the root element. $properties = array( - 'key' => 'home', + 'key' => 'myhome', 'type' => navigation_node::TYPE_SYSTEM, - 'text' => get_string('home'), - 'action' => new moodle_url('/'), - 'icon' => new pix_icon('i/home', '') + 'text' => get_string('myhome'), + 'action' => new moodle_url('/my/'), + 'icon' => new pix_icon('i/dashboard', ''), ); } else if ($homepage == HOMEPAGE_MYCOURSES) { // We are using the user's course summary page for the root element. @@ -1371,13 +1371,13 @@ public function __construct(moodle_page $page) { 'icon' => new pix_icon('i/course', '') ); } else { - // We are using the users my moodle for the root element. + // We are using the site home for the root element. $properties = array( - 'key' => 'myhome', + 'key' => 'home', 'type' => navigation_node::TYPE_SYSTEM, - 'text' => get_string('myhome'), - 'action' => new moodle_url('/my/'), - 'icon' => new pix_icon('i/dashboard', '') + 'text' => get_string('home'), + 'action' => new moodle_url('/'), + 'icon' => new pix_icon('i/home', ''), ); } diff --git a/lib/tests/moodlelib_test.php b/lib/tests/moodlelib_test.php index 6a3ce1cbeae14..0330edd598d3d 100644 --- a/lib/tests/moodlelib_test.php +++ b/lib/tests/moodlelib_test.php @@ -5162,18 +5162,18 @@ public function get_list_of_plugins_provider(): array { * @dataProvider get_home_page_provider * @param string $user Whether the user is logged, guest or not logged. * @param int $expected Expected value after calling the get_home_page method. - * @param int|null $defaulthomepage The $CFG->defaulthomepage setting value. + * @param int|string|null $defaulthomepage The $CFG->defaulthomepage setting value. * @param int|null $enabledashboard Whether the dashboard should be enabled or not. - * @param int|null $userpreference User preference for the home page setting. + * @param int|string|null $userpreference User preference for the home page setting. * $param int|null $allowguestmymoodle The $CFG->allowguestmymoodle setting value. * @covers ::get_home_page */ public function test_get_home_page( string $user, int $expected, - ?int $defaulthomepage = null, + int|string|null $defaulthomepage = null, ?int $enabledashboard = null, - ?int $userpreference = null, + int|string|null $userpreference = null, ?int $allowguestmymoodle = null, ): void { global $CFG, $USER; @@ -5210,6 +5210,8 @@ public function test_get_home_page( * @return array */ public static function get_home_page_provider(): array { + global $CFG; + return [ 'No logged user' => [ 'user' => 'nologged', @@ -5271,6 +5273,11 @@ public static function get_home_page_provider(): array { 'defaulthomepage' => HOMEPAGE_SITE, 'enabledashboard' => 0, ], + 'Logged user. URL set as default home page.' => [ + 'user' => 'logged', + 'expected' => HOMEPAGE_URL, + 'defaulthomepage' => "{$CFG->wwwroot}/home", + ], 'Logged user. User preference set as default page with dashboard enabled and user preference set to dashboard' => [ 'user' => 'logged', 'expected' => HOMEPAGE_MY, @@ -5299,6 +5306,13 @@ public static function get_home_page_provider(): array { 'enabledashboard' => 0, 'userpreference' => HOMEPAGE_MYCOURSES, ], + 'Logged user. User preference set as default page with user preference set to URL.' => [ + 'user' => 'logged', + 'expected' => HOMEPAGE_URL, + 'defaulthomepage' => HOMEPAGE_USER, + 'enabledashboard' => null, + 'userpreference' => "{$CFG->wwwroot}/home", + ], ]; } @@ -5321,6 +5335,31 @@ public function test_get_default_home_page(): void { $this->assertEquals(HOMEPAGE_MYCOURSES, $default); } + /** + * Test getting default home page for {@see HOMEPAGE_URL} + * + * @covers ::get_default_home_page_url + */ + public function test_get_default_home_page_url(): void { + global $CFG; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $this->assertNull(get_default_home_page_url()); + + // Site configuration. + $CFG->defaulthomepage = "{$CFG->wwwroot}/home"; + $this->assertEquals($CFG->defaulthomepage, get_default_home_page_url()); + + // User preference. + $CFG->defaulthomepage = HOMEPAGE_USER; + + $userpreference = "{$CFG->wwwroot}/about"; + set_user_preference('user_home_page_preference', $userpreference); + $this->assertEquals($userpreference, get_default_home_page_url()); + } + /** * Tests the get_performance_info function with regard to locks. * diff --git a/login/lib.php b/login/lib.php index e30e178369e5e..9927580cfe04b 100644 --- a/login/lib.php +++ b/login/lib.php @@ -362,6 +362,9 @@ function core_login_get_return_url() { $urltogo = $CFG->wwwroot.'/my/courses.php'; } } + if ($homepage === HOMEPAGE_URL) { + $urltogo = (string) get_default_home_page_url(); + } } return $urltogo; } diff --git a/user/tests/behat/contact_site_support.feature b/user/tests/behat/contact_site_support.feature index 9d610aa73f396..784254a5b9e81 100644 --- a/user/tests/behat/contact_site_support.feature +++ b/user/tests/behat/contact_site_support.feature @@ -52,7 +52,7 @@ Feature: Contact site support method and availability can be customised Scenario: Contact site support can be disabled Given the following config values are set as admin: | supportavailability | 0 | - | defaulthomepage | home | + | defaulthomepage | 0 | # Confirm unauthenticated visitor cannot see the option. When I am on site homepage Then I should not see "Contact site support" in the "page-footer" "region" @@ -98,7 +98,7 @@ Feature: Contact site support method and availability can be customised Given the following config values are set as admin: | supportavailability | 0 | | supportpage | profile.php | - | defaulthomepage | home | + | defaulthomepage | 0 | When I log in as "user1" And I am on the "user > Contact Site Support" page Then I should see "Acceptance test site" in the "page-header" "region" From 8413f612604450ffc1f410636d0496124c7f2ed2 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Wed, 10 Jul 2024 09:09:21 +0100 Subject: [PATCH 060/178] MDL-82431 phpunit: correct unit test filename and classname. Standardise the same test cases, e.g. make final, covers notation, static data providers, namespaces, etc. Once the tests run, a couple of them failed. They required changes to assertions to make them pass. --- ..._inquiry_activities_completed_by_test.php} | 20 ++++++------- ...urse_module_instance_list_viewed_test.php} | 23 ++++++++------- .../course_module_viewed_test.php} | 28 ++++++++++++------- ...delete_entry.php => delete_entry_test.php} | 15 +++------- ...epare_entry.php => prepare_entry_test.php} | 15 +++------- ...update_entry.php => update_entry_test.php} | 19 ++++--------- ..._viewed.php => log_report_viewed_test.php} | 18 ++++-------- ...ction.php => userlist_collection_test.php} | 18 ++++-------- ...se.php => report_progress_helper_test.php} | 16 +++++------ 9 files changed, 72 insertions(+), 100 deletions(-) rename analytics/tests/{community_of_inquiry_activities_completed_by.php => community_of_inquiry_activities_completed_by_test.php} (96%) rename lib/tests/{event_course_module_instance_list_viewed.php => event/course_module_instance_list_viewed_test.php} (87%) rename lib/tests/{event_course_module_viewed.php => event/course_module_viewed_test.php} (85%) rename mod/glossary/tests/external/{delete_entry.php => delete_entry_test.php} (91%) rename mod/glossary/tests/external/{prepare_entry.php => prepare_entry_test.php} (89%) rename mod/glossary/tests/external/{update_entry.php => update_entry_test.php} (96%) rename mod/h5pactivity/tests/external/{log_report_viewed.php => log_report_viewed_test.php} (92%) rename privacy/tests/{userlist_collection.php => userlist_collection_test.php} (94%) rename report/progress/tests/{report_progress_helper_testcase.php => report_progress_helper_test.php} (95%) diff --git a/analytics/tests/community_of_inquiry_activities_completed_by.php b/analytics/tests/community_of_inquiry_activities_completed_by_test.php similarity index 96% rename from analytics/tests/community_of_inquiry_activities_completed_by.php rename to analytics/tests/community_of_inquiry_activities_completed_by_test.php index c8b4df5fa4db4..afa8df8f25824 100644 --- a/analytics/tests/community_of_inquiry_activities_completed_by.php +++ b/analytics/tests/community_of_inquiry_activities_completed_by_test.php @@ -14,31 +14,29 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Unit tests for activities completed by classification. - * - * @package core_analytics - * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ +namespace core_analytics; -defined('MOODLE_INTERNAL') || die(); +use advanced_testcase; +use ReflectionClass; +use ReflectionMethod; +use stdClass; /** * Unit tests for activities completed by classification. * * @package core_analytics + * @covers \core_analytics\local\indicator\community_of_inquiry_activity * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class community_of_inquiry_activities_completed_by_testcase extends advanced_testcase { +final class community_of_inquiry_activities_completed_by_test extends advanced_testcase { /** * availability_levels * * @return array */ - public function availability_levels() { + public static function availability_levels(): array { return array( 'activity' => array('activity'), 'section' => array('section'), @@ -147,7 +145,7 @@ public function test_get_activities_with_weeks() { $second = $startdate + WEEKSECS; $third = $startdate + (WEEKSECS * 2); $forth = $startdate + (WEEKSECS * 3); - $this->assertCount(1, $method->invoke($indicator, $first, $first + WEEKSECS, $stu1)); + $this->assertCount(2, $method->invoke($indicator, $first, $first + WEEKSECS, $stu1)); $this->assertCount(1, $method->invoke($indicator, $second, $second + WEEKSECS, $stu1)); $this->assertCount(0, $method->invoke($indicator, $third, $third + WEEKSECS, $stu1)); $this->assertCount(2, $method->invoke($indicator, $forth, $forth + WEEKSECS, $stu1)); diff --git a/lib/tests/event_course_module_instance_list_viewed.php b/lib/tests/event/course_module_instance_list_viewed_test.php similarity index 87% rename from lib/tests/event_course_module_instance_list_viewed.php rename to lib/tests/event/course_module_instance_list_viewed_test.php index fd3bb9aba004b..0420e7a095b5b 100644 --- a/lib/tests/event_course_module_instance_list_viewed.php +++ b/lib/tests/event/course_module_instance_list_viewed_test.php @@ -14,24 +14,27 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace core\event; + +use advanced_testcase; +use context_course; +use context_system; +use Exception; +use moodle_url; + +defined('MOODLE_INTERNAL') || die(); +require_once(__DIR__.'/../fixtures/event_mod_fixtures.php'); + /** * Tests for base course module instance list viewed event. * * @package core * @category phpunit + * @covers \core\event\course_module_instance_list_viewed * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -defined('MOODLE_INTERNAL') || die(); -require_once(__DIR__.'/fixtures/event_mod_fixtures.php'); - -/** - * Class core_event_course_module_instance_list_viewed_testcase - * - * Tests for event \core\event\course_module_instance_list_viewed_testcase - */ -class core_event_course_module_instance_list_viewed_testcase extends advanced_testcase { +final class course_module_instance_list_viewed_test extends advanced_testcase { /** * Test event properties and methods. diff --git a/lib/tests/event_course_module_viewed.php b/lib/tests/event/course_module_viewed_test.php similarity index 85% rename from lib/tests/event_course_module_viewed.php rename to lib/tests/event/course_module_viewed_test.php index 7f77eaa75903c..04845c1f8aba7 100644 --- a/lib/tests/event_course_module_viewed.php +++ b/lib/tests/event/course_module_viewed_test.php @@ -14,24 +14,27 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace core\event; + +use advanced_testcase; +use coding_exception; +use context_module; +use stdClass; +use moodle_url; + +defined('MOODLE_INTERNAL') || die(); +require_once(__DIR__.'/../fixtures/event_fixtures.php'); + /** * Tests for base course module viewed event. * * @package core * @category phpunit + * @covers \core\event\course_module_viewed * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -defined('MOODLE_INTERNAL') || die(); -require_once(__DIR__.'/fixtures/event_fixtures.php'); - -/** - * Class core_event_course_module_viewed_testcase - * - * Tests for event \core\event\course_module_viewed - */ -class core_event_course_module_viewed_testcase extends advanced_testcase { +final class course_module_viewed_test extends advanced_testcase { /** * Test event properties and methods. @@ -80,6 +83,9 @@ public function test_event_validations() { $this->assertStringContainsString("course_module_viewed event must define objectid and object table.", $e->getMessage()); } + $this->assertDebuggingCalled('Inconsistent courseid - context combination detected.'); + $this->resetDebugging(); + try { \core_tests\event\course_module_viewed::create(array( 'contextid' => 1, @@ -88,5 +94,7 @@ public function test_event_validations() { } catch (coding_exception $e) { $this->assertStringContainsString("course_module_viewed event must define objectid and object table.", $e->getMessage()); } + + $this->assertDebuggingCalled('Inconsistent courseid - context combination detected.'); } } diff --git a/mod/glossary/tests/external/delete_entry.php b/mod/glossary/tests/external/delete_entry_test.php similarity index 91% rename from mod/glossary/tests/external/delete_entry.php rename to mod/glossary/tests/external/delete_entry_test.php index 3e2c07041da01..c8482130e185b 100644 --- a/mod/glossary/tests/external/delete_entry.php +++ b/mod/glossary/tests/external/delete_entry_test.php @@ -14,16 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * External function test for delete_entry. - * - * @package mod_glossary - * @category external - * @since Moodle 3.10 - * @copyright 2020 Juan Leyva - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace mod_glossary\external; defined('MOODLE_INTERNAL') || die(); @@ -38,10 +28,13 @@ * External function test for delete_entry. * * @package mod_glossary + * @category external + * @covers \mod_glossary\external\delete_entry + * @since Moodle 3.10 * @copyright 2020 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class delete_entry_testcase extends externallib_advanced_testcase { +final class delete_entry_test extends externallib_advanced_testcase { /** * Test the behaviour of delete_entry(). diff --git a/mod/glossary/tests/external/prepare_entry.php b/mod/glossary/tests/external/prepare_entry_test.php similarity index 89% rename from mod/glossary/tests/external/prepare_entry.php rename to mod/glossary/tests/external/prepare_entry_test.php index 1ba657a88045f..cccf19b944903 100644 --- a/mod/glossary/tests/external/prepare_entry.php +++ b/mod/glossary/tests/external/prepare_entry_test.php @@ -14,16 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * External function test for prepare_entry. - * - * @package mod_glossary - * @category external - * @since Moodle 3.10 - * @copyright 2020 Juan Leyva - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace mod_glossary\external; defined('MOODLE_INTERNAL') || die(); @@ -38,10 +28,13 @@ * External function test for prepare_entry. * * @package mod_glossary + * @category external + * @covers \mod_glossary\external\prepare_entry + * @since Moodle 3.10 * @copyright 2020 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class prepare_entry_testcase extends externallib_advanced_testcase { +final class prepare_entry_test extends externallib_advanced_testcase { /** * test_prepare_entry diff --git a/mod/glossary/tests/external/update_entry.php b/mod/glossary/tests/external/update_entry_test.php similarity index 96% rename from mod/glossary/tests/external/update_entry.php rename to mod/glossary/tests/external/update_entry_test.php index bb5d862c0384b..ced2eadee9e7a 100644 --- a/mod/glossary/tests/external/update_entry.php +++ b/mod/glossary/tests/external/update_entry_test.php @@ -14,16 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * External function test for update_entry. - * - * @package mod_glossary - * @category external - * @since Moodle 3.10 - * @copyright 2020 Juan Leyva - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace mod_glossary\external; defined('MOODLE_INTERNAL') || die(); @@ -42,10 +32,13 @@ * External function test for update_entry. * * @package mod_glossary + * @category external + * @covers \mod_glossary\external\update_entry + * @since Moodle 3.10 * @copyright 2020 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class update_entry_testcase extends externallib_advanced_testcase { +final class update_entry_test extends externallib_advanced_testcase { /** * test_update_entry_without_optional_settings @@ -152,7 +145,7 @@ public function test_update_entry_with_aliases() { $aliases = $DB->get_records('glossary_alias', ['entryid' => $entryid]); $this->assertCount(2, $aliases); foreach ($aliases as $alias) { - $this->assertContains($alias->alias, $newaliases); + $this->assertStringContainsString($alias->alias, $newaliases); } } @@ -193,7 +186,7 @@ public function test_update_entry_in_categories() { $categories = $DB->get_records('glossary_entries_categories', ['entryid' => $entryid]); $this->assertCount(2, $categories); foreach ($categories as $category) { - $this->assertContains($category->categoryid, $newcategories); + $this->assertStringContainsString($category->categoryid, $newcategories); } } diff --git a/mod/h5pactivity/tests/external/log_report_viewed.php b/mod/h5pactivity/tests/external/log_report_viewed_test.php similarity index 92% rename from mod/h5pactivity/tests/external/log_report_viewed.php rename to mod/h5pactivity/tests/external/log_report_viewed_test.php index b05ef119b98be..6220ce62214b2 100644 --- a/mod/h5pactivity/tests/external/log_report_viewed.php +++ b/mod/h5pactivity/tests/external/log_report_viewed_test.php @@ -14,16 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * External function test for log_report_viewed. - * - * @package mod_h5pactivity - * @category external - * @since Moodle 3.11 - * @copyright 2021 Ilya Tregubov - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace mod_h5pactivity\external; defined('MOODLE_INTERNAL') || die(); @@ -39,10 +29,14 @@ * External function test for log_report_viewed. * * @package mod_h5pactivity + * @category external + * @covers \mod_h5pactivity\external\log_report_viewed + * @since Moodle 3.11 * @copyright 2021 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class log_report_viewed_testcase extends externallib_advanced_testcase { +final class log_report_viewed_test extends externallib_advanced_testcase { + /** * Test the behaviour of log_report_viewed. * @@ -110,7 +104,7 @@ public function test_execute(int $enabletracking, int $reviewmode, string $login * * @return array */ - public function execute_data(): array { + public static function execute_data(): array { return [ 'Student reviewing own attempt' => [ 1, manager::REVIEWCOMPLETION, 'student', 'student' diff --git a/privacy/tests/userlist_collection.php b/privacy/tests/userlist_collection_test.php similarity index 94% rename from privacy/tests/userlist_collection.php rename to privacy/tests/userlist_collection_test.php index fd5d368fdc66c..fa358fdf3446f 100644 --- a/privacy/tests/userlist_collection.php +++ b/privacy/tests/userlist_collection_test.php @@ -14,19 +14,9 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Unit Tests for a the collection of userlists class - * - * @package core_privacy - * @category test - * @copyright 2018 Andrew Nicols - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -defined('MOODLE_INTERNAL') || die(); - -global $CFG; +namespace core_privacy; +use advanced_testcase; use \core_privacy\local\request\userlist_collection; use \core_privacy\local\request\userlist; use \core_privacy\local\request\approved_userlist; @@ -34,11 +24,13 @@ /** * Tests for the \core_privacy API's userlist collection functionality. * + * @package core_privacy + * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_privacy\local\request\userlist_collection */ -class userlist_collection_test extends advanced_testcase { +final class userlist_collection_test extends advanced_testcase { /** * A userlist_collection should support the userlist type. diff --git a/report/progress/tests/report_progress_helper_testcase.php b/report/progress/tests/report_progress_helper_test.php similarity index 95% rename from report/progress/tests/report_progress_helper_testcase.php rename to report/progress/tests/report_progress_helper_test.php index 02bce7c70dd1e..790bf6028b8e9 100644 --- a/report/progress/tests/report_progress_helper_testcase.php +++ b/report/progress/tests/report_progress_helper_test.php @@ -14,23 +14,21 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Tests for the progress report sorting. - * - * @package report_progress - * @copyright 2021 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. - */ +namespace report_progress; -defined('MOODLE_INTERNAL') || die(); +use advanced_testcase; +use completion_info; +use testing_data_generator; /** * Class for testing report progress helper. * + * @package report_progress + * @covers \report_progress\local\helper * @copyright 2021 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ -class report_progress_helper_testcase extends advanced_testcase { +final class report_progress_helper_test extends advanced_testcase { /** @var testing_data_generator data generator.*/ protected $generator; From 4da240f2a29d85b2a1888092b693ca8ae03ec8c8 Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Thu, 11 Jul 2024 09:22:12 +0800 Subject: [PATCH 061/178] MDL-66994 upgrade: Move upgrade step after 4.4.0 release upgrade line --- lib/db/upgrade.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 423bab7b6845b..c5e13751ca346 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -1167,6 +1167,9 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2024041200.00); } + // Automatically generated Moodle v4.4.0 release upgrade line. + // Put any upgrade step following this. + if ($oldversion < 2024070500.01) { // Remove the site_contactable config of the hub plugin from config plugin table. unset_config('site_contactable', 'hub'); @@ -1175,8 +1178,5 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2024070500.01); } - // Automatically generated Moodle v4.4.0 release upgrade line. - // Put any upgrade step following this. - return true; } From 814753e558cb09e281cbc37ea7ee28ed3b10ad41 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Tue, 18 Jun 2024 16:51:20 +0200 Subject: [PATCH 062/178] MDL-82168 badges: Add WS to enable/disable badges --- .upgradenotes/MDL-82168-2024062815382042.yml | 5 + badges/classes/external/disable_badges.php | 126 +++++++ badges/classes/external/enable_badges.php | 183 ++++++++++ badges/renderer.php | 33 +- badges/tests/external/disable_badges_test.php | 224 +++++++++++++ badges/tests/external/enable_badges_test.php | 312 ++++++++++++++++++ lib/db/services.php | 12 + version.php | 2 +- 8 files changed, 888 insertions(+), 9 deletions(-) create mode 100644 .upgradenotes/MDL-82168-2024062815382042.yml create mode 100644 badges/classes/external/disable_badges.php create mode 100644 badges/classes/external/enable_badges.php create mode 100644 badges/tests/external/disable_badges_test.php create mode 100644 badges/tests/external/enable_badges_test.php 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/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/renderer.php b/badges/renderer.php index 8740f66f0f350..94154370c1c64 100644 --- a/badges/renderer.php +++ b/badges/renderer.php @@ -573,17 +573,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/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/lib/db/services.php b/lib/db/services.php index feb0ce346c4d9..27622ce9b9fcf 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -119,6 +119,18 @@ 'ajax' => true, 'loginrequired' => true, ), + 'core_badges_disable_badges' => [ + 'classname' => 'core_badges\external\disable_badges', + 'description' => 'Disable badges', + 'type' => 'write', + 'ajax' => true, + ], + 'core_badges_enable_badges' => [ + 'classname' => 'core_badges\external\enable_badges', + 'description' => 'Enable badges', + 'type' => 'write', + 'ajax' => true, + ], 'core_badges_get_user_badges' => array( 'classname' => 'core_badges_external', 'methodname' => 'get_user_badges', diff --git a/version.php b/version.php index 95c817a0cc371..544e8680d14da 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2024070500.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2024070500.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '4.5dev (Build: 20240705)'; // Human-friendly version name From 8c006300e57aed0d594aef1f5123e7d1ae2d41f8 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Tue, 18 Jun 2024 16:52:15 +0200 Subject: [PATCH 063/178] MDL-82168 badges: Move enable/disable badges to modal Co-authored-by: Ferran Recio --- badges/action.php | 64 ------ badges/amd/build/actions.min.js | 10 + badges/amd/build/actions.min.js.map | 1 + badges/amd/build/selectors.min.js | 10 +- badges/amd/build/selectors.min.js.map | 2 +- badges/amd/src/actions.js | 194 ++++++++++++++++++ badges/amd/src/selectors.js | 2 + .../local/systemreports/badges.php | 33 ++- badges/index.php | 1 + badges/overview.php | 4 +- badges/renderer.php | 1 + lang/en/badges.php | 14 +- lang/en/deprecated.txt | 2 + 13 files changed, 246 insertions(+), 92 deletions(-) create mode 100644 badges/amd/build/actions.min.js create mode 100644 badges/amd/build/actions.min.js.map create mode 100644 badges/amd/src/actions.js 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/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/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/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 94154370c1c64..182229e688141 100644 --- a/badges/renderer.php +++ b/badges/renderer.php @@ -242,6 +242,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); diff --git a/lang/en/badges.php b/lang/en/badges.php index eb7686e4622bd..83a5b9a0bf074 100644 --- a/lang/en/badges.php +++ b/lang/en/badges.php @@ -26,7 +26,7 @@ $string['actions'] = 'Actions'; $string['activate'] = 'Enable access'; -$string['activatesuccess'] = 'Access to the badges was successfully enabled.'; +$string['activatesuccess'] = 'Access to badge \'{$a}\' enabled.'; $string['addalignment'] = 'Add external skill or standard'; $string['addbadge'] = 'Add badges'; $string['addbadge_help'] = 'Select all badges that should be added to this badge requirement. Hold CTRL key to select multiple items.'; @@ -78,7 +78,7 @@ $string['award'] = 'Award badge'; $string['awardedto'] = 'Awarded to {$a}'; $string['awardedtoyou'] = 'Issued to me'; -$string['awardoncron'] = 'Access to the badges was successfully enabled. Too many users can instantly earn this badge. To ensure site performance, this action will take some time to process.'; +$string['awardoncron'] = 'A high number of users are being awarded the badge \'{$a->badgename}\'. It may take some time for all users to receive it.'; $string['awards'] = 'Recipients'; $string['backpackavailability'] = 'External badge verification'; $string['backpackconnectionok'] = 'Backpack connection successfully established'; @@ -272,7 +272,7 @@ $string['dateearned'] = 'Date: {$a}'; $string['day'] = 'Day(s)'; $string['deactivate'] = 'Disable access'; -$string['deactivatesuccess'] = 'Access to the badges was successfully disabled.'; +$string['deactivatesuccess'] = 'Access to badge \'{$a}\' disabled.'; $string['defaultissuercontact'] = 'Badge issuer email address'; $string['defaultissuercontact_desc'] = 'An email address associated with the badge issuer. For an Open Badges v2.0 backpack, this is used for authentication when publishing badges to a backpack.'; $string['defaultissuerpassword'] = 'Badge issuer password'; @@ -296,7 +296,6 @@ $string['error:backpackemailnotfound'] = 'The email \'{$a}\' is not associated with a backpack. You need to create a backpack for that account or sign in with another email address.'; $string['error:badgeawardnotfound'] = 'Cannot verify this awarded badge. This badge may have been revoked.'; $string['error:badgenotfound'] = 'Badge not found'; -$string['error:cannotact'] = 'Cannot activate the badge. '; $string['error:cannotawardbadge'] = 'Cannot award badge to a user.'; $string['error:cannotrevokebadge'] = 'Cannot revoke badge from a user.'; $string['error:cannotdeletecriterion'] = 'This criterion cannot be deleted. '; @@ -461,8 +460,8 @@ $string['notifyevery'] = 'Every time'; $string['notifymonthly'] = 'Monthly'; $string['notifyweekly'] = 'Weekly'; -$string['numawards'] = 'This badge has been issued to {$a->count} user(s).'; -$string['numawardstat'] = 'This badge has been issued {$a} user(s).'; +$string['numawards'] = 'Users awarded badge \'{$a->badgename}\': {$a->count}.'; +$string['numawardstat'] = 'Users awarded badge \'{$a->badgename}\': {$a->awards}.'; $string['overallcrit'] = 'of the selected criteria are complete.'; $string['oauth2issuer'] = 'OAuth 2 services'; $string['openbadgesv1'] = 'Open Badges v1.0'; @@ -516,7 +515,6 @@ $string['requiredcompetency'] = 'At least one competency should be added to the competency criterion.'; $string['requiredcourse'] = 'At least one course should be added to the courseset criterion.'; $string['requiredbadge'] = 'At least one badge should be added to the badge criterion.'; -$string['reviewbadge'] = 'Changes in badge access'; $string['reviewconfirm'] = '

This will make your badge visible to users and allow them to start earning it.

It is possible that some users already meet this badge\'s criteria and will be issued this badge immediately after you enable it.

@@ -598,4 +596,6 @@ $string['backpackemail_help'] = 'The email address associated with your backpack. While you are connected, any badges earned on this site will be associated with this email address.'; // Deprecated since Moodle 4.5. +$string['error:cannotact'] = 'Cannot activate the badge. '; $string['error:duplicatename'] = 'Badge with such name already exists in the system.'; +$string['reviewbadge'] = 'Changes in badge access'; diff --git a/lang/en/deprecated.txt b/lang/en/deprecated.txt index 37bb5465b5236..a9da929469df2 100644 --- a/lang/en/deprecated.txt +++ b/lang/en/deprecated.txt @@ -117,3 +117,5 @@ importcalendarexternal,core_calendar nocalendarsubscriptions,core_calendar datechanged,core error:duplicatename,core_badges +error:cannotact,core_badges +reviewbadge,core_badges From e559a3c325f1fb932917138a01ffe6411b7c5fd6 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Tue, 18 Jun 2024 17:45:34 +0200 Subject: [PATCH 064/178] MDL-82168 badges: Fix and improve behat tests --- badges/tests/behat/award_badge.feature | 225 +++++++++--------- badges/tests/behat/award_badge_groups.feature | 26 +- badges/tests/behat/badge_navigation.feature | 2 +- badges/tests/behat/criteria_activity.feature | 6 +- badges/tests/behat/criteria_cohort.feature | 24 +- .../tests/behat/criteria_competency.feature | 6 +- badges/tests/behat/criteria_profile.feature | 2 +- badges/tests/behat/manage_badges.feature | 10 +- badges/tests/behat/nobadge_navigation.feature | 4 +- badges/tests/behat/view_badge.feature | 42 ++-- .../tests/behat/block_badges_course.feature | 38 +-- .../behat/block_badges_dashboard.feature | 27 +-- .../behat/block_badges_frontpage.feature | 26 +- 13 files changed, 211 insertions(+), 227 deletions(-) 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/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 072fb9b55bc30..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" 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..adac0083adf08 100644 --- a/badges/tests/behat/view_badge.feature +++ b/badges/tests/behat/view_badge.feature @@ -9,26 +9,26 @@ Feature: Display badges | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | # Create system badge and define a criterion. + Given the following "core_badges > Badge" exists: + | name | Testing system badge | + | description | Testing system badge description | + | image | badges/tests/behat/badge.png | + | version | 1.1 | + | status | 0 | + | type | 1 | + | language | ca | + And the following "core_badges > Criteria" exists: + | badge | Testing system badge | + | role | 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 + And I follow "Testing system badge" + And I select "Criteria" from the "jump" singleselect Scenario: Display badge without expired date # Enable the badge. Given 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" @@ -53,7 +53,7 @@ Feature: Display badges 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" @@ -81,7 +81,7 @@ Feature: Display badges 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 @@ -101,8 +101,10 @@ Feature: Display badges 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" @@ -123,8 +125,10 @@ Feature: Display badges 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/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)" From edafd17fc4dcae1c4728c2c2fa752f73e99f4db6 Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Thu, 11 Jul 2024 11:49:20 +0800 Subject: [PATCH 065/178] MDL-81265 mod_workshop: Verify "Setup phase" heading with "should exist" --- mod/workshop/tests/behat/submission_types.feature | 10 +++++----- mod/workshop/tests/behat/workshop_edit_form.feature | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mod/workshop/tests/behat/submission_types.feature b/mod/workshop/tests/behat/submission_types.feature index e72c111792ad6..ea3411c49c6e4 100644 --- a/mod/workshop/tests/behat/submission_types.feature +++ b/mod/workshop/tests/behat/submission_types.feature @@ -38,30 +38,30 @@ Feature: Submission types | submissiontypefileavailable | 1 | | submissiontypefilerequired | 1 | And I press "Save and display" - Then I should see "Setup phase" in the "h3#mod_workshop-userplanheading" "css_element" + Then "Setup phase" "heading" should exist When I navigate to "Settings" in current page administration And I set the following fields to these values: | submissiontypetextrequired | 0 | And I press "Save and display" - Then I should see "Setup phase" in the "h3#mod_workshop-userplanheading" "css_element" + Then "Setup phase" "heading" should exist When I navigate to "Settings" in current page administration And I set the following fields to these values: | submissiontypetextrequired | 1 | | submissiontypefilerequired | 0 | And I press "Save and display" - Then I should see "Setup phase" in the "h3#mod_workshop-userplanheading" "css_element" + Then "Setup phase" "heading" should exist When I navigate to "Settings" in current page administration And I set the following fields to these values: | submissiontypefileavailable | 0 | And I press "Save and display" - Then I should see "Setup phase" in the "h3#mod_workshop-userplanheading" "css_element" + Then "Setup phase" "heading" should exist When I navigate to "Settings" in current page administration And I set the following fields to these values: | submissiontypefileavailable | 1 | | submissiontypefilerequired | 1 | | submissiontypetextavailable | 0 | And I press "Save and display" - Then I should see "Setup phase" in the "h3#mod_workshop-userplanheading" "css_element" + Then "Setup phase" "heading" should exist @javascript @_file_upload Scenario: All submission fields required diff --git a/mod/workshop/tests/behat/workshop_edit_form.feature b/mod/workshop/tests/behat/workshop_edit_form.feature index 6026735386a69..b9c4922b7a0f2 100644 --- a/mod/workshop/tests/behat/workshop_edit_form.feature +++ b/mod/workshop/tests/behat/workshop_edit_form.feature @@ -42,4 +42,4 @@ Feature: Workshop assessment with grade to pass And I should see "You must enter a number here." And I set the field "Assessment grade to pass" to "10" And I press "Save and display" - Then I should see "Setup phase" in the "h3#mod_workshop-userplanheading" "css_element" + Then "Setup phase" "heading" should exist From e28937ccbcf77ae6f9c2907782a07132eb55a08e Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Thu, 11 Jul 2024 11:16:44 +0700 Subject: [PATCH 066/178] MDL-80489 qtype_ddtos: Update language string --- question/type/ddwtos/lang/en/qtype_ddwtos.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/question/type/ddwtos/lang/en/qtype_ddwtos.php b/question/type/ddwtos/lang/en/qtype_ddwtos.php index 6ab1e157265f0..1fc534392157e 100644 --- a/question/type/ddwtos/lang/en/qtype_ddwtos.php +++ b/question/type/ddwtos/lang/en/qtype_ddwtos.php @@ -27,7 +27,7 @@ $string['blank'] = 'blank'; $string['blanknumber'] = 'Blank {$a}'; $string['correctansweris'] = 'The correct answer is: {$a}'; -$string['choicesacceptedtext'] = 'Write the answer to be dragged into each gap. You can include extra answers to increase difficulty.
+$string['choicesacceptedtext'] = 'Write the answers to be dragged into the gaps. You can include extra answers to increase difficulty.
Accepted text formatting: <sub>, <sup>, <b>, <i>, <em>, <strong>. TeX is also accepted, using $$ at the start and at the end.'; $string['errorlimitedchoice'] = 'Choice [[{$a}]] has been used more than once without being set to "Unlimited". Please recheck this question.'; $string['infinite'] = 'Unlimited'; From a0394ce78fa6decc4c2fe867a885aedd300b8a17 Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Thu, 11 Jul 2024 15:15:34 +0700 Subject: [PATCH 067/178] MDL-80489 qtype_ddtos: Update Behat to match language string --- question/type/ddwtos/tests/behat/edit.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/question/type/ddwtos/tests/behat/edit.feature b/question/type/ddwtos/tests/behat/edit.feature index 4e9b9f0da7e20..9d5c885d27f4c 100644 --- a/question/type/ddwtos/tests/behat/edit.feature +++ b/question/type/ddwtos/tests/behat/edit.feature @@ -27,7 +27,7 @@ Feature: Test editing a drag and drop into text questions And I should see "Choice [[1]]" And I should see "Choice [[2]]" And I should see "Choice [[3]]" - And I should see "Write the answer to be dragged into each gap. You can include extra answers to increase difficulty." in the "Choices" "fieldset" + And I should see "Write the answers to be dragged into the gaps. You can include extra answers to increase difficulty." in the "Choices" "fieldset" And I set the following fields to these values: | Question name | Edited question name | And I press "id_submitbutton" From 2a2130e9d733d316fc488faeb761af4e59d9b2b5 Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Tue, 23 May 2023 18:31:13 +0200 Subject: [PATCH 068/178] MDL-78293 core_user: New Web Services to manage private files --- .upgradenotes/MDL-78293-2024071102223792.yml | 7 + lib/db/services.php | 14 ++ .../prepare_private_files_for_edition.php | 99 ++++++++++ .../classes/external/update_private_files.php | 112 +++++++++++ user/classes/form/private_files.php | 4 +- ...prepare_private_files_for_edition_test.php | 81 ++++++++ .../external/update_private_files_test.php | 176 ++++++++++++++++++ 7 files changed, 491 insertions(+), 2 deletions(-) create mode 100644 .upgradenotes/MDL-78293-2024071102223792.yml create mode 100644 user/classes/external/prepare_private_files_for_edition.php create mode 100644 user/classes/external/update_private_files.php create mode 100644 user/tests/external/prepare_private_files_for_edition_test.php create mode 100644 user/tests/external/update_private_files_test.php 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/lib/db/services.php b/lib/db/services.php index feb0ce346c4d9..2cb1a01c52599 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -2008,6 +2008,20 @@ 'capabilities' => 'moodle/user:manageownfiles', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), + 'core_user_prepare_private_files_for_edition' => [ + 'classname' => '\core_user\external\prepare_private_files_for_edition', + 'description' => 'Prepares the draft area for user private files.', + 'type' => 'write', + 'capabilities' => 'moodle/user:manageownfiles', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], + 'core_user_update_private_files' => [ + 'classname' => '\core_user\external\update_private_files', + 'description' => 'Copy files from a draft area to users private files area.', + 'type' => 'write', + 'capabilities' => 'moodle/user:manageownfiles', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], // Competencies functions. 'core_competency_create_competency_framework' => array( diff --git a/user/classes/external/prepare_private_files_for_edition.php b/user/classes/external/prepare_private_files_for_edition.php new file mode 100644 index 0000000000000..e7d9b152e3a5d --- /dev/null +++ b/user/classes/external/prepare_private_files_for_edition.php @@ -0,0 +1,99 @@ +. + +namespace core_user\external; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_multiple_structure; +use core_external\external_single_structure; +use core_external\external_value; +use core_external\external_warnings; +use context_user; + +/** + * Prepares the draft area for user private files. + * + * @package core_user + * @category external + * @copyright 2024 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class prepare_private_files_for_edition extends external_api { + + /** + * Describes the external function parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([]); + } + + /** + * Prepare a draft area for private files. + * + * @throws \moodle_exception; + * @return array + */ + public static function execute(): array { + global $USER; + + $usercontext = context_user::instance($USER->id); + self::validate_context($usercontext); + + $form = new \core_user\form\private_files(); + // Permission checks. + $form->check_access_for_dynamic_submission(); + + $areaoptions = $form->get_options(); + $draftitemid = 0; + file_prepare_draft_area($draftitemid, $usercontext->id, 'user', 'private', 0, $areaoptions); + + // Just get a structure compatible with external API. + array_walk($areaoptions, function(&$item, $key) { + $item = ['name' => $key, 'value' => $item]; + }); + + return [ + 'draftitemid' => $draftitemid, + 'areaoptions' => $areaoptions, + '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( + [ + 'draftitemid' => new external_value(PARAM_INT, 'Draft item id for the file area.'), + 'areaoptions' => new external_multiple_structure( + new external_single_structure( + [ + 'name' => new external_value(PARAM_RAW, 'Name of option.'), + 'value' => new external_value(PARAM_RAW, 'Value of option.'), + ] + ), 'Draft file area options.' + ), + 'warnings' => new external_warnings(), + ] + ); + } +} diff --git a/user/classes/external/update_private_files.php b/user/classes/external/update_private_files.php new file mode 100644 index 0000000000000..fd5bb746fd0a2 --- /dev/null +++ b/user/classes/external/update_private_files.php @@ -0,0 +1,112 @@ +. + +namespace core_user\external; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_single_structure; +use core_external\external_value; +use core_external\external_warnings; +use context_user; + +/** + * Updates current user private files. + * + * @package core_user + * @category external + * @copyright 2024 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class update_private_files extends external_api { + + /** + * Describes the external function parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters( + [ + 'draftitemid' => new external_value(PARAM_INT, 'The draft item id with the files.'), + ] + ); + } + + /** + * Updates current user private files. + * + * @param int $draftitemid The draft item id with the files. + * @throws \moodle_exception; + * @return array + */ + public static function execute(int $draftitemid): array { + global $USER; + + $params = self::validate_parameters(self::execute_parameters(), [ + 'draftitemid' => $draftitemid, + ]); + $warnings = []; + + $usercontext = context_user::instance($USER->id); + self::validate_context($usercontext); + + $fs = get_file_storage(); + if (empty($fs->get_area_files($usercontext->id, 'user', 'draft', $params['draftitemid']))) { + throw new \moodle_exception('Invalid draft item id.'); + } + + // Data structure for the draft item id. + $data = ['files_filemanager' => $params['draftitemid']]; + // Use existing form for validation. + $form = new \core_user\form\private_files(); + $form->check_access_for_dynamic_submission(); + $errors = $form->validation($data, []); + + if (!empty($errors)) { + $status = false; + foreach ($errors as $itemname => $message) { + $warnings[] = [ + 'item' => $itemname, + 'itemid' => 0, + 'warningcode' => 'fielderror', + 'message' => s($message), + ]; + } + } else { + file_postupdate_standard_filemanager((object) $data, 'files', + $form->get_options(), $usercontext, 'user', 'private', 0); + $status = true; + } + + return [ + 'status' => $status, + '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([ + 'status' => new external_value(PARAM_BOOL, 'The update result, true if everything went well.'), + 'warnings' => new external_warnings(), + ]); + } +} diff --git a/user/classes/form/private_files.php b/user/classes/form/private_files.php index 4aabd374e146b..e58cc8462f238 100644 --- a/user/classes/form/private_files.php +++ b/user/classes/form/private_files.php @@ -108,7 +108,7 @@ protected function get_emaillink() { * If necessary, form data is available in $this->_ajaxformdata or * by calling $this->optional_param() */ - protected function check_access_for_dynamic_submission(): void { + public function check_access_for_dynamic_submission(): void { require_capability('moodle/user:manageownfiles', $this->get_context_for_dynamic_submission()); } @@ -131,7 +131,7 @@ protected function get_context_for_dynamic_submission(): \context { * @return array * @throws \coding_exception */ - protected function get_options(): array { + public function get_options(): array { global $CFG; $maxbytes = $CFG->userquota; diff --git a/user/tests/external/prepare_private_files_for_edition_test.php b/user/tests/external/prepare_private_files_for_edition_test.php new file mode 100644 index 0000000000000..67c6bb4e53e88 --- /dev/null +++ b/user/tests/external/prepare_private_files_for_edition_test.php @@ -0,0 +1,81 @@ +. + +namespace core_user\external; + +use core_external\external_api; + +/** + * Tests for the prepare_private_files_for_edition class. + * + * @package core_user + * @category external + * @copyright 2024 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \core_user\external\prepare_private_files_for_edition + */ +final class prepare_private_files_for_edition_test extends \advanced_testcase { + + public function test_execute(): void { + global $USER; + $this->resetAfterTest(); + $this->setAdminUser(); + + // Create some files in the user private file area. + $filename = 'faketxt.txt'; + $filerecordinline = [ + 'contextid' => \context_user::instance($USER->id)->id, + 'component' => 'user', + 'filearea' => 'private', + 'itemid' => 0, + 'filepath' => '/', + 'filename' => $filename, + ]; + $fs = get_file_storage(); + $fs->create_file_from_string($filerecordinline, 'fake txt contents.'); + + $result = prepare_private_files_for_edition::execute(); + + $result = external_api::clean_returnvalue(prepare_private_files_for_edition::execute_returns(), $result); + $draftitemid = $result['draftitemid']; + $this->assertGreaterThan(0, $draftitemid); + $this->assertCount(5, $result['areaoptions']); + $this->assertEmpty($result['warnings']); + + // Check we get the expected user private files in the draft area. + $files = file_get_drafarea_files($draftitemid); + $this->assertCount(1, $files->list); + $this->assertEquals($filename, $files->list[0]->filename); + } + + /** + * Test missing capabilities. + */ + public function test_execute_missing_capabilities(): void { + global $CFG, $DB; + $this->resetAfterTest(); + + $authrole = $DB->get_record('role', ['id' => $CFG->defaultuserroleid]); + unassign_capability('moodle/user:manageownfiles', $authrole->id); + accesslib_clear_all_caches_for_unit_testing(); + + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + $this->expectException('moodle_exception'); + $result = prepare_private_files_for_edition::execute(0); + } +} diff --git a/user/tests/external/update_private_files_test.php b/user/tests/external/update_private_files_test.php new file mode 100644 index 0000000000000..5b58afee806f5 --- /dev/null +++ b/user/tests/external/update_private_files_test.php @@ -0,0 +1,176 @@ +. + +namespace core_user\external; + +use core_external\external_api; + +/** + * Tests for the \core_user\external\update_private_files class. + * + * @package core_user + * @category external + * @copyright 2024 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \core_user\external\update_private_files + */ +final class update_private_files_test extends \advanced_testcase { + + /** + * Test base cases. + */ + public function test_execute(): void { + global $CFG; + require_once($CFG->dirroot . '/files/externallib.php'); + + $this->resetAfterTest(); + $user = $this->getDataGenerator()->create_user(); + $anotheruser = $this->getDataGenerator()->create_user(); + $this->setUser($user); + $context = \context_user::instance($user->id); + + // Create one file in the user private file area. + $filename = 'faketxt.txt'; + $filerecordinline = [ + 'contextid' => $context->id, + 'component' => 'user', + 'filearea' => 'private', + 'itemid' => 0, + 'filepath' => '/', + 'filename' => $filename, + ]; + $fs = get_file_storage(); + $fs->create_file_from_string($filerecordinline, 'fake txt contents.'); + + // Retrieve draft area with existing files. + $result = prepare_private_files_for_edition::execute(); + $result = external_api::clean_returnvalue(prepare_private_files_for_edition::execute_returns(), $result); + $draftitemid = $result['draftitemid']; + + // Add one file to the draft area reusing previous structure. + $newfilename = 'newfaketxt.txt'; + $filerecordinline['itemid'] = $draftitemid; + $filerecordinline['filearea'] = 'draft'; + $filerecordinline['filename'] = $newfilename; + $fs->create_file_from_string($filerecordinline, 'new fake txt contents.'); + $files = file_get_drafarea_files($draftitemid); + $this->assertCount(2, $files->list); // 2 files in the draft area. + + // Update the private files with the new files. + $result = update_private_files::execute($draftitemid); + $result = external_api::clean_returnvalue(update_private_files::execute_returns(), $result); + $this->assertTrue($result['status']); + $this->assertEmpty($result['warnings']); + // Check that the new files are in the private file area. + $files = \core_files_external::get_files($context->id, 'user', 'private', 0, '/', ''); + $this->assertCount(2, $files['files']); // 2 files in the private area. + + // Now, try deleting one. + $result = prepare_private_files_for_edition::execute(); + $result = external_api::clean_returnvalue(prepare_private_files_for_edition::execute_returns(), $result); + $draftitemid = $result['draftitemid']; + + \core_files\external\delete\draft::execute($draftitemid, [ + [ + 'filepath' => '/', + 'filename' => $newfilename, + ], + ]); + // Update to force deletion. + $result = update_private_files::execute($draftitemid); + $result = external_api::clean_returnvalue(update_private_files::execute_returns(), $result); + $this->assertTrue($result['status']); + $this->assertEmpty($result['warnings']); + // Check we only have one now. + $files = \core_files_external::get_files($context->id, 'user', 'private', 0, '/', ''); + $this->assertCount(1, $files['files']); + $this->assertEquals($filename, $files['files'][0]['filename']); + + // Use other's user draft item area. + $result = prepare_private_files_for_edition::execute(); + $result = external_api::clean_returnvalue(prepare_private_files_for_edition::execute_returns(), $result); + $draftitemid = $result['draftitemid']; + + $this->setUser($anotheruser); + $this->expectException('moodle_exception'); + update_private_files::execute($draftitemid); + } + + /** + * Test invalid draftitemid. + */ + public function test_execute_invalid_draftitemid(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $this->expectException('moodle_exception'); + update_private_files::execute(0); + } + + /** + * Test quota reached. + */ + public function test_execute_quota_reached(): void { + global $CFG; + $this->resetAfterTest(); + + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + $context = \context_user::instance($user->id); + + // Force the quota so we are sure it won't be space to add the new file. + $fileareainfo = file_get_file_area_info($context->id, 'user', 'private'); + $CFG->userquota = 1; + + $result = prepare_private_files_for_edition::execute(); + $result = external_api::clean_returnvalue(prepare_private_files_for_edition::execute_returns(), $result); + $draftitemid = $result['draftitemid']; + $filerecordinline = [ + 'contextid' => $context->id, + 'component' => 'user', + 'filearea' => 'draft', + 'itemid' => $draftitemid, + 'filepath' => '/', + 'filename' => 'faketxt.txt', + ]; + $fs = get_file_storage(); + $fs->create_file_from_string($filerecordinline, 'new fake txt contents.'); + + $result = update_private_files::execute($draftitemid); + $result = external_api::clean_returnvalue(update_private_files::execute_returns(), $result); + // We should get a warning as because of quota we can add files. + $this->assertFalse($result['status']); + $this->assertNotEmpty($result['warnings']); + } + + /** + * Test missing capabilities. + */ + public function test_execute_missing_capabilities(): void { + global $CFG, $DB; + $this->resetAfterTest(); + + $authrole = $DB->get_record('role', ['id' => $CFG->defaultuserroleid]); + unassign_capability('moodle/user:manageownfiles', $authrole->id); + accesslib_clear_all_caches_for_unit_testing(); + + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + $this->expectException('moodle_exception'); + $result = update_private_files::execute(0); + } +} From eaca2203a007c5c5285719d3e6ff33d5b93cde99 Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Fri, 12 Jul 2024 08:41:05 +0700 Subject: [PATCH 069/178] NOBUG: Fixed SVG browser compatibility --- mod/subsection/pix/monologo.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod/subsection/pix/monologo.svg b/mod/subsection/pix/monologo.svg index b3bef3ef0587b..aba879f99273c 100644 --- a/mod/subsection/pix/monologo.svg +++ b/mod/subsection/pix/monologo.svg @@ -1,3 +1,3 @@ - + From abcac5bc8fd96d493a75acdb36dcea7b1c0dfd46 Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Fri, 12 Jul 2024 08:41:57 +0700 Subject: [PATCH 070/178] NOBUG: Add upgrade notes --- UPGRADING.md | 51 +++++++++++++++++++++++++++---------- backup/util/ui/UPGRADING.md | 1 - badges/UPGRADING.md | 16 ++++++++++++ lib/table/UPGRADING.md | 1 - message/UPGRADING.md | 6 +++++ mod/assign/UPGRADING.md | 3 +++ 6 files changed, 62 insertions(+), 16 deletions(-) create mode 100644 badges/UPGRADING.md diff --git a/UPGRADING.md b/UPGRADING.md index f98a25070cc3c..24171e2f4193c 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -8,6 +8,20 @@ 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) + +#### 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 @@ -255,6 +269,9 @@ 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) ### report @@ -280,6 +297,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 @@ -401,20 +438,6 @@ 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) - -#### 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) - ### editor_tiny #### Changed diff --git a/backup/util/ui/UPGRADING.md b/backup/util/ui/UPGRADING.md index 1bbf593c80546..209c4671f42fe 100644 --- a/backup/util/ui/UPGRADING.md +++ b/backup/util/ui/UPGRADING.md @@ -7,4 +7,3 @@ - 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) - diff --git a/badges/UPGRADING.md b/badges/UPGRADING.md new file mode 100644 index 0000000000000..52b554e467a4a --- /dev/null +++ b/badges/UPGRADING.md @@ -0,0 +1,16 @@ +# 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) + +### 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/lib/table/UPGRADING.md b/lib/table/UPGRADING.md index e9eb81afec2e4..729e35ec446e6 100644 --- a/lib/table/UPGRADING.md +++ b/lib/table/UPGRADING.md @@ -8,4 +8,3 @@ This property allows you to control whether the table is rendered as a responsive table. For more information see [MDL-80748](https://tracker.moodle.org/browse/MDL-80748) - diff --git a/message/UPGRADING.md b/message/UPGRADING.md index e74b08ceaf963..ce470eeb861a0 100644 --- a/message/UPGRADING.md +++ b/message/UPGRADING.md @@ -2,6 +2,12 @@ ## 4.5dev +### 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 diff --git a/mod/assign/UPGRADING.md b/mod/assign/UPGRADING.md index 28dc88fa2b26e..ddf12e846d536 100644 --- a/mod/assign/UPGRADING.md +++ b/mod/assign/UPGRADING.md @@ -27,3 +27,6 @@ - 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) From fd487cd3f2001453c22418cce396c9c8e5d4a2a5 Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Fri, 12 Jul 2024 08:41:58 +0700 Subject: [PATCH 071/178] weekly release 4.5dev --- version.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.php b/version.php index 544e8680d14da..de49c37e85a83 100644 --- a/version.php +++ b/version.php @@ -29,9 +29,9 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2024070500.01; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2024071200.00; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. -$release = '4.5dev (Build: 20240705)'; // Human-friendly version name +$release = '4.5dev (Build: 20240712)'; // Human-friendly version name $branch = '405'; // This version's branch. $maturity = MATURITY_ALPHA; // This version's maturity level. From 5617445d4254af6bd60503a2ac06802a9e091080 Mon Sep 17 00:00:00 2001 From: ferranrecio Date: Wed, 8 May 2024 10:37:17 +0200 Subject: [PATCH 072/178] MDL-81888 tool_generator: add cleanup scenario tag The tool_generator creates a testing scenario that can execute all steps from behat_data_generators (with some limitations) and admin settings. With the patch the scenario file can incorporate a cleanup steps to be executed when the manual test is done. --- .../generator/classes/form/featureimport.php | 7 +++ .../classes/local/testscenario/runner.php | 57 ++++++++++++++++++- .../classes/output/parsingresult.php | 6 +- admin/tool/generator/cli/runtestscenario.php | 13 ++++- .../tool/generator/lang/en/tool_generator.php | 3 + admin/tool/generator/runtestscenario.php | 6 +- .../templates/parsingresult.mustache | 4 +- .../tests/behat/testscenario.feature | 22 +++++++ .../fixtures/testscenario/scenario.feature | 34 +++++++---- .../testscenario/scenario_cleanup.feature | 22 +++++++ .../testscenario/scenario_outline.feature | 6 ++ course/tests/behat/behat_course.php | 11 ++++ user/tests/behat/behat_user.php | 15 +++++ 13 files changed, 188 insertions(+), 18 deletions(-) create mode 100644 admin/tool/generator/tests/fixtures/testscenario/scenario_cleanup.feature 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 0ba6514f9592f..3f8d2dc47833a 100644 --- a/admin/tool/generator/classes/local/testscenario/runner.php +++ b/admin/tool/generator/classes/local/testscenario/runner.php @@ -19,6 +19,8 @@ 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; @@ -49,6 +51,7 @@ public function init() { $this->include_composer_libraries(); $this->include_behat_libraries(); $this->load_generator(); + $this->load_cleanup(); } /** @@ -77,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; } @@ -97,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. @@ -159,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(); @@ -170,6 +210,13 @@ 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') { $this->parse_scenario_outline($scenario, $result); continue; @@ -184,6 +231,15 @@ 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. @@ -209,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}} diff --git a/lib/templates/notification_base.mustache b/lib/templates/notification_base.mustache index df377029342a9..feb114ef5da35 100644 --- a/lib/templates/notification_base.mustache +++ b/lib/templates/notification_base.mustache @@ -45,7 +45,7 @@ }}{{# announce }} role="alert" data-aria-autofocus="true"{{/ announce }}> {{{ message }}} {{# closebutton }}{{! - }}{{! diff --git a/message/templates/message_drawer_view_conversation_header_edit_mode.mustache b/message/templates/message_drawer_view_conversation_header_edit_mode.mustache index 31df07817c564..beed0d08c7453 100644 --- a/message/templates/message_drawer_view_conversation_header_edit_mode.mustache +++ b/message/templates/message_drawer_view_conversation_header_edit_mode.mustache @@ -38,7 +38,7 @@
{{#str}} messagesselected:, core_message {{/str}} 1 - diff --git a/theme/boost/scss/moodle/bs5-bridge.scss b/theme/boost/scss/moodle/bs5-bridge.scss index 02de1cc0d6031..d33fbe819b1b8 100644 --- a/theme/boost/scss/moodle/bs5-bridge.scss +++ b/theme/boost/scss/moodle/bs5-bridge.scss @@ -5,3 +5,7 @@ .g-0 { @extend .no-gutters; } + +.btn-close { + @extend .close; +} diff --git a/theme/boost/scss/moodle/core.scss b/theme/boost/scss/moodle/core.scss index 6ae47838c92a3..2bb620ebc9263 100644 --- a/theme/boost/scss/moodle/core.scss +++ b/theme/boost/scss/moodle/core.scss @@ -148,7 +148,7 @@ input[type="image"], .sr-only-focusable, a.dropdown-toggle, .moodle-dialogue-base .closebutton, -button.close, +button.btn-close, .form-autocomplete-selection, [role="treeitem"]:not([aria-expanded="true"]) { &.focus, @@ -2612,7 +2612,7 @@ $picker-emojis-per-row: 7 !default; color: darken(theme-color-level($color, $alert-color-level), 10%); } // Darken the close button text colour inside notification alerts for better contrast. - .close { + .btn-close { color: darken(theme-color-level($color, $alert-color-level), 20%); opacity: 0.6; } diff --git a/theme/boost/scss/moodle/modal.scss b/theme/boost/scss/moodle/modal.scss index d06f572527851..37edd49aa1b06 100644 --- a/theme/boost/scss/moodle/modal.scss +++ b/theme/boost/scss/moodle/modal.scss @@ -17,7 +17,7 @@ } // Override Bootstrap .close for better accessibility. - .close { + .btn-close { // Adjust the margins so the focus outline does not look clipped. margin: -0.8rem -0.8rem -0.8rem auto; diff --git a/theme/boost/scss/moodle/search.scss b/theme/boost/scss/moodle/search.scss index 7dd9797cc9420..8bd7eac7144dc 100644 --- a/theme/boost/scss/moodle/search.scss +++ b/theme/boost/scss/moodle/search.scss @@ -39,6 +39,9 @@ } .btn-close { right: 2.2rem; + opacity: inherit; + font-size: inherit; + line-height: inherit; } .btn-submit { background-color: $gray-100; diff --git a/theme/boost/scss/moodle/toasts.scss b/theme/boost/scss/moodle/toasts.scss index 7393105387e73..96cbf3ec2112c 100644 --- a/theme/boost/scss/moodle/toasts.scss +++ b/theme/boost/scss/moodle/toasts.scss @@ -43,7 +43,7 @@ } } - .close { + .btn-close { color: inherit; } } diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index de87e20c7d0d0..1f7e8a6e9cbca 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -18092,7 +18092,7 @@ a.badge-dark:focus, a.badge-dark.focus { .alert-dismissible { padding-right: 3.90625rem; } -.alert-dismissible .close { +.alert-dismissible .close, .alert-dismissible .btn-close { position: absolute; top: 0; right: 0; @@ -18554,7 +18554,7 @@ a.badge-dark:focus, a.badge-dark.focus { border-color: #1b1e21; } -.close { +.close, .btn-close { float: right; font-size: 1.40625rem; font-weight: 700; @@ -18564,25 +18564,25 @@ a.badge-dark:focus, a.badge-dark.focus { opacity: 0.5; } @media (max-width: 1200px) { - .close { + .close, .btn-close { font-size: calc(0.950625rem + 0.6075vw); } } -.close:hover { +.close:hover, .btn-close:hover { color: #000; text-decoration: none; } -.close:not(:disabled):not(.disabled):hover, .close:not(:disabled):not(.disabled):focus { +.close:not(:disabled):not(.disabled):hover, .btn-close:not(:disabled):not(.disabled):hover, .close:not(:disabled):not(.disabled):focus, .btn-close:not(:disabled):not(.disabled):focus { opacity: 0.75; } -button.close { +button.close, button.btn-close { padding: 0; background-color: transparent; border: 0; } -a.close.disabled { +a.close.disabled, a.disabled.btn-close { pointer-events: none; } @@ -18747,7 +18747,7 @@ a.close.disabled { border-top-left-radius: calc(0.6rem - 1px); border-top-right-radius: calc(0.6rem - 1px); } -.modal-header .close { +.modal-header .close, .modal-header .btn-close { padding: 1rem 1rem; margin: -1rem -1rem -1rem auto; } @@ -23147,8 +23147,8 @@ a.dropdown-toggle.focus, a.dropdown-toggle:focus, .moodle-dialogue-base .closebutton.focus, .moodle-dialogue-base .closebutton:focus, -button.close.focus, -button.close:focus, +button.btn-close.focus, +button.btn-close:focus, .form-autocomplete-selection.focus, .form-autocomplete-selection:focus, [role=treeitem]:not([aria-expanded=true]).focus, @@ -23170,7 +23170,7 @@ input[type=image]:focus:hover, .sr-only-focusable:focus:hover, a.dropdown-toggle:focus:hover, .moodle-dialogue-base .closebutton:focus:hover, -button.close:focus:hover, +button.btn-close:focus:hover, .form-autocomplete-selection:focus:hover, [role=treeitem]:not([aria-expanded=true]):focus:hover { text-decoration: none; @@ -25581,7 +25581,7 @@ input[disabled] { .alert-primary a { color: #041d34; } -.alert-primary .close { +.alert-primary .btn-close { color: #000305; opacity: 0.6; } @@ -25589,7 +25589,7 @@ input[disabled] { .alert-secondary a { color: #525557; } -.alert-secondary .close { +.alert-secondary .btn-close { color: #393b3d; opacity: 0.6; } @@ -25597,7 +25597,7 @@ input[disabled] { .alert-success a, .environmenttable .ok a { color: #0c1b0b; } -.alert-success .close, .environmenttable .ok .close { +.alert-success .btn-close, .environmenttable .ok .btn-close { color: black; opacity: 0.6; } @@ -25605,7 +25605,7 @@ input[disabled] { .alert-info a { color: #00171b; } -.alert-info .close { +.alert-info .btn-close { color: black; opacity: 0.6; } @@ -25613,7 +25613,7 @@ input[disabled] { .alert-warning a, .environmenttable .warn a { color: #573e1c; } -.alert-warning .close, .environmenttable .warn .close { +.alert-warning .btn-close, .environmenttable .warn .btn-close { color: #302310; opacity: 0.6; } @@ -25621,7 +25621,7 @@ input[disabled] { .alert-danger a, .environmenttable .error a { color: #3d0f0a; } -.alert-danger .close, .environmenttable .error .close { +.alert-danger .btn-close, .environmenttable .error .btn-close { color: #110403; opacity: 0.6; } @@ -25629,7 +25629,7 @@ input[disabled] { .alert-light a { color: #686868; } -.alert-light .close { +.alert-light .btn-close { color: #4e4e4f; opacity: 0.6; } @@ -25637,7 +25637,7 @@ input[disabled] { .alert-dark a { color: #040505; } -.alert-dark .close { +.alert-dark .btn-close { color: black; opacity: 0.6; } @@ -32895,6 +32895,9 @@ body.path-question-type .mform fieldset.hidden { } .simplesearchform .btn-close { right: 2.2rem; + opacity: inherit; + font-size: inherit; + line-height: inherit; } .simplesearchform .btn-submit { background-color: #f8f9fa; @@ -37009,10 +37012,10 @@ span[data-flexitour=container][x-placement=right] div[data-role=arrow]:after, sp left: 50%; transform: translate(-50%, -50%); } -.modal .close { +.modal .btn-close { margin: -0.8rem -0.8rem -0.8rem auto; } -.modal .close:not(:disabled):not(.disabled):hover, .modal .close:not(:disabled):not(.disabled):focus { +.modal .btn-close:not(:disabled):not(.disabled):hover, .modal .btn-close:not(:disabled):not(.disabled):focus { opacity: inherit; } @@ -37763,7 +37766,7 @@ div.editor_atto_toolbar button .icon { margin: 2px 5px 0 0; content: "\f06a"; } -.toast .close { +.toast .btn-close { color: inherit; } diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index d01407a435eb9..e8f678021b784 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -18092,7 +18092,7 @@ a.badge-dark:focus, a.badge-dark.focus { .alert-dismissible { padding-right: 3.90625rem; } -.alert-dismissible .close { +.alert-dismissible .close, .alert-dismissible .btn-close { position: absolute; top: 0; right: 0; @@ -18554,7 +18554,7 @@ a.badge-dark:focus, a.badge-dark.focus { border-color: #1b1e21; } -.close { +.close, .btn-close { float: right; font-size: 1.40625rem; font-weight: 700; @@ -18564,25 +18564,25 @@ a.badge-dark:focus, a.badge-dark.focus { opacity: 0.5; } @media (max-width: 1200px) { - .close { + .close, .btn-close { font-size: calc(0.950625rem + 0.6075vw); } } -.close:hover { +.close:hover, .btn-close:hover { color: #000; text-decoration: none; } -.close:not(:disabled):not(.disabled):hover, .close:not(:disabled):not(.disabled):focus { +.close:not(:disabled):not(.disabled):hover, .btn-close:not(:disabled):not(.disabled):hover, .close:not(:disabled):not(.disabled):focus, .btn-close:not(:disabled):not(.disabled):focus { opacity: 0.75; } -button.close { +button.close, button.btn-close { padding: 0; background-color: transparent; border: 0; } -a.close.disabled { +a.close.disabled, a.disabled.btn-close { pointer-events: none; } @@ -18747,7 +18747,7 @@ a.close.disabled { border-top-left-radius: calc(0.3rem - 1px); border-top-right-radius: calc(0.3rem - 1px); } -.modal-header .close { +.modal-header .close, .modal-header .btn-close { padding: 1rem 1rem; margin: -1rem -1rem -1rem auto; } @@ -23147,8 +23147,8 @@ a.dropdown-toggle.focus, a.dropdown-toggle:focus, .moodle-dialogue-base .closebutton.focus, .moodle-dialogue-base .closebutton:focus, -button.close.focus, -button.close:focus, +button.btn-close.focus, +button.btn-close:focus, .form-autocomplete-selection.focus, .form-autocomplete-selection:focus, [role=treeitem]:not([aria-expanded=true]).focus, @@ -23170,7 +23170,7 @@ input[type=image]:focus:hover, .sr-only-focusable:focus:hover, a.dropdown-toggle:focus:hover, .moodle-dialogue-base .closebutton:focus:hover, -button.close:focus:hover, +button.btn-close:focus:hover, .form-autocomplete-selection:focus:hover, [role=treeitem]:not([aria-expanded=true]):focus:hover { text-decoration: none; @@ -25581,7 +25581,7 @@ input[disabled] { .alert-primary a { color: #041d34; } -.alert-primary .close { +.alert-primary .btn-close { color: #000305; opacity: 0.6; } @@ -25589,7 +25589,7 @@ input[disabled] { .alert-secondary a { color: #525557; } -.alert-secondary .close { +.alert-secondary .btn-close { color: #393b3d; opacity: 0.6; } @@ -25597,7 +25597,7 @@ input[disabled] { .alert-success a, .environmenttable .ok a { color: #0c1b0b; } -.alert-success .close, .environmenttable .ok .close { +.alert-success .btn-close, .environmenttable .ok .btn-close { color: black; opacity: 0.6; } @@ -25605,7 +25605,7 @@ input[disabled] { .alert-info a { color: #00171b; } -.alert-info .close { +.alert-info .btn-close { color: black; opacity: 0.6; } @@ -25613,7 +25613,7 @@ input[disabled] { .alert-warning a, .environmenttable .warn a { color: #573e1c; } -.alert-warning .close, .environmenttable .warn .close { +.alert-warning .btn-close, .environmenttable .warn .btn-close { color: #302310; opacity: 0.6; } @@ -25621,7 +25621,7 @@ input[disabled] { .alert-danger a, .environmenttable .error a { color: #3d0f0a; } -.alert-danger .close, .environmenttable .error .close { +.alert-danger .btn-close, .environmenttable .error .btn-close { color: #110403; opacity: 0.6; } @@ -25629,7 +25629,7 @@ input[disabled] { .alert-light a { color: #686868; } -.alert-light .close { +.alert-light .btn-close { color: #4e4e4f; opacity: 0.6; } @@ -25637,7 +25637,7 @@ input[disabled] { .alert-dark a { color: #040505; } -.alert-dark .close { +.alert-dark .btn-close { color: black; opacity: 0.6; } @@ -32895,6 +32895,9 @@ body.path-question-type .mform fieldset.hidden { } .simplesearchform .btn-close { right: 2.2rem; + opacity: inherit; + font-size: inherit; + line-height: inherit; } .simplesearchform .btn-submit { background-color: #f8f9fa; @@ -36943,10 +36946,10 @@ span[data-flexitour=container][x-placement=right] div[data-role=arrow]:after, sp left: 50%; transform: translate(-50%, -50%); } -.modal .close { +.modal .btn-close { margin: -0.8rem -0.8rem -0.8rem auto; } -.modal .close:not(:disabled):not(.disabled):hover, .modal .close:not(:disabled):not(.disabled):focus { +.modal .btn-close:not(:disabled):not(.disabled):hover, .modal .btn-close:not(:disabled):not(.disabled):focus { opacity: inherit; } @@ -37697,7 +37700,7 @@ div.editor_atto_toolbar button .icon { margin: 2px 5px 0 0; content: "\f06a"; } -.toast .close { +.toast .btn-close { color: inherit; } From 7d030bba7719af38f321159753a824d168b32a2a Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Thu, 11 Jul 2024 21:09:08 +0100 Subject: [PATCH 080/178] MDL-73662 mod_quiz: fix initialization arguments to close button. Co-authored-by: Sergio Rabellino --- mod/quiz/classes/output/renderer.php | 2 +- mod/quiz/module.js | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/mod/quiz/classes/output/renderer.php b/mod/quiz/classes/output/renderer.php index 4e70395f1774e..a3d6c91ef5603 100644 --- a/mod/quiz/classes/output/renderer.php +++ b/mod/quiz/classes/output/renderer.php @@ -705,7 +705,7 @@ public function close_attempt_popup($url, $message = '') { $delay = 0; } $this->page->requires->js_init_call('M.mod_quiz.secure_window.close', - [$url, $delay], false, quiz_get_js_module()); + [$url->out(false), $delay], false, quiz_get_js_module()); $output .= $this->box_end(); $output .= $this->footer(); diff --git a/mod/quiz/module.js b/mod/quiz/module.js index 1921b6e43e127..c116172f5b7a9 100644 --- a/mod/quiz/module.js +++ b/mod/quiz/module.js @@ -398,13 +398,26 @@ M.mod_quiz.secure_window = { e.halt(); }, + /** + * Initialize the event listener for the secure window close button + * + * @param {Object} Y YUI instance. When called from renderer, this parameter precedes the others + * @param {String} url + */ init_close_button: function(Y, url) { Y.on('click', function(e) { - M.mod_quiz.secure_window.close(url, 0) + M.mod_quiz.secure_window.close(Y, url, 0); }, '#secureclosebutton'); }, - close: function(url, delay) { + /** + * Close the secure window, or redirect to URL if the opener is no longer present + * + * @param {Object} Y YUI instance. When called from renderer, this parameter precedes the others + * @param {String} url + * @param {Number} delay + */ + close: function(Y, url, delay) { setTimeout(function() { if (window.opener) { window.opener.document.location.reload(); From 0844f9c88a26224d85f2dc3c43a74c5f66cc09b0 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Thu, 11 Jul 2024 11:56:02 +0100 Subject: [PATCH 081/178] MDL-82444 filter_tidy: advertise required PHP extension. --- filter/tidy/environment.xml | 12 ++++++++++++ filter/tidy/lang/en/filter_tidy.php | 1 + 2 files changed, 13 insertions(+) create mode 100644 filter/tidy/environment.xml diff --git a/filter/tidy/environment.xml b/filter/tidy/environment.xml new file mode 100644 index 0000000000000..7eb9ba656e6b9 --- /dev/null +++ b/filter/tidy/environment.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/filter/tidy/lang/en/filter_tidy.php b/filter/tidy/lang/en/filter_tidy.php index 90b6151fe8248..b039211ccc2bf 100644 --- a/filter/tidy/lang/en/filter_tidy.php +++ b/filter/tidy/lang/en/filter_tidy.php @@ -25,3 +25,4 @@ $string['filtername'] = 'HTML tidy'; $string['privacy:metadata'] = 'The HTML tidy plugin does not store any personal data.'; +$string['tidyextensionrequired'] = 'In order to use this filter, the \'tidy\' PHP extension must be installed'; From 9b624edba9a99c38e5d792d737039a889c8c3f03 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Fri, 12 Jul 2024 13:48:28 +0100 Subject: [PATCH 082/178] MDL-82444 core: skip environment test on optional plugin extensions. --- lib/tests/environment_test.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/tests/environment_test.php b/lib/tests/environment_test.php index 340ccbe76abe4..0d65180e7c6c2 100644 --- a/lib/tests/environment_test.php +++ b/lib/tests/environment_test.php @@ -26,7 +26,7 @@ * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class environment_test extends \advanced_testcase { +final class environment_test extends \advanced_testcase { /** * Test the environment check status. @@ -47,7 +47,7 @@ public function test_environment_check_status(): void { * * @return array */ - public function environment_provider() { + public static function environment_provider(): array { global $CFG; require_once($CFG->libdir.'/environmentlib.php'); @@ -75,6 +75,13 @@ public function test_environment($result): void { $this->markTestSkipped('OPCache extension is not necessary for unit testing.'); } + if ($result->part === 'php_extension' + && $result->getPluginName() !== '' + && $result->getLevel() === 'optional' + && $result->getStatus() === false) { + $this->markTestSkipped('Optional plugin extension is not necessary for unit testing.'); + } + if ($result->part === 'custom_check' && $result->getLevel() === 'optional' && $result->getStatus() === false) { From c72de818279eab7c479072d9509f706ab7f9d471 Mon Sep 17 00:00:00 2001 From: Eric Merrill Date: Fri, 21 Jun 2024 16:07:45 -0400 Subject: [PATCH 083/178] MDL-82193 scorm: Don't store header with multiline AICC content --- mod/scorm/aicc.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/mod/scorm/aicc.php b/mod/scorm/aicc.php index d101a8eca8df3..b334d0d50bfdd 100644 --- a/mod/scorm/aicc.php +++ b/mod/scorm/aicc.php @@ -181,7 +181,13 @@ } echo 'Lesson_Mode='.$userdata->mode."\r\n"; if (isset($userdata->{'cmi.suspend_data'})) { - echo "[Core_Lesson]\r\n".rawurldecode($userdata->{'cmi.suspend_data'})."\r\n"; + $decoded = rawurldecode($userdata->{'cmi.suspend_data'}); + $header = "[Core_Lesson]\r\n"; + if (stripos($decoded, $header) === 0) { + // The header may have been stored with the content. If it was, trim it off the front. + $decoded = core_text::substr($decoded, core_text::strlen($header)); + } + echo "[Core_Lesson]\r\n".$decoded."\r\n"; } else { echo "[Core_Lesson]\r\n"; } @@ -229,8 +235,9 @@ // An element was passed by the external AICC package is not one we care about. continue; } + } else { + $multirowvalue .= $datarow . "\r\n"; } - $multirowvalue .= $datarow."\r\n"; if (isset($datarows[$did + 1]) && substr($datarows[$did + 1], 0, 1) != '[') { // This is a multiline row, we haven't found the end yet. continue; From 2c8acd3462423711e6efd7c5c73320b08f010e60 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 7 Feb 2024 21:04:57 +0800 Subject: [PATCH 084/178] MDL-66903 testing: Reset CFG and component after test This change moves the reset of global test state to the finally section rather than doing it only if the test passes. Previously if a test which modifies the `core_component` internals failed, it would not reset the internal state and impact subsequent tests. --- lib/phpunit/classes/advanced_testcase.php | 24 +++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/phpunit/classes/advanced_testcase.php b/lib/phpunit/classes/advanced_testcase.php index 2068ce650fd0f..000e07a4c3133 100644 --- a/lib/phpunit/classes/advanced_testcase.php +++ b/lib/phpunit/classes/advanced_testcase.php @@ -57,7 +57,7 @@ final public function __construct($name = null, array $data = [], $dataname = '' * Runs the bare test sequence. */ final public function runBare(): void { - global $DB; + global $CFG, $DB; if (phpunit_util::$lastdbwrites != $DB->perf_get_writes()) { // This happens when previous test does not reset, we can not use transactions. @@ -70,20 +70,17 @@ final public function runBare(): void { try { $this->setCurrentTimeStart(); parent::runBare(); - // Set DB reference in case somebody mocked it in test. - $DB = phpunit_util::get_global_backup('DB'); - - // Deal with any debugging messages. - $debugerror = phpunit_util::display_debugging_messages(true); - $this->resetDebugging(); - if (!empty($debugerror)) { - trigger_error('Unexpected debugging() call detected.' . "\n" . $debugerror, E_USER_NOTICE); - } } catch (Exception $ex) { $e = $ex; } catch (Throwable $ex) { // Engine errors in PHP7 throw exceptions of type Throwable (this "catch" will be ignored in PHP5). $e = $ex; + } finally { + // Reset global state after test and test failure. + $CFG = phpunit_util::get_global_backup('CFG'); + $DB = phpunit_util::get_global_backup('DB'); + // This is _hacky_. We need to reset the autoloader, and this is the only way to do so right now. + (new ReflectionProperty(\core_component::class, 'plugintypes'))->setValue(null, null); } if (isset($e)) { @@ -92,6 +89,13 @@ final public function runBare(): void { throw $e; } + // Deal with any debugging messages. + $debugerror = phpunit_util::display_debugging_messages(true); + $this->resetDebugging(); + if (!empty($debugerror)) { + trigger_error('Unexpected debugging() call detected.' . "\n" . $debugerror, E_USER_NOTICE); + } + if (!$this->testdbtransaction || $this->testdbtransaction->is_disposed()) { $this->testdbtransaction = null; } From 04f8f11d355897a7a3e0e3c91179c1e7888f8618 Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Mon, 1 Jul 2024 13:42:16 +0700 Subject: [PATCH 085/178] MDL-81272 calendar: Improve permission check for separate group mode --- calendar/lib.php | 36 ++++---- calendar/tests/lib_test.php | 176 ++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 19 deletions(-) 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/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. */ From 3c03e9448c0ca185e61e523e959ee79834af89d4 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 7 Feb 2024 21:07:05 +0800 Subject: [PATCH 086/178] MDL-66903 testing: Fix whitespace --- lib/tests/component_test.php | 216 +++++++++++++++++------------------ 1 file changed, 108 insertions(+), 108 deletions(-) diff --git a/lib/tests/component_test.php b/lib/tests/component_test.php index ebc70af8e7dc6..98ca138454df4 100644 --- a/lib/tests/component_test.php +++ b/lib/tests/component_test.php @@ -730,54 +730,54 @@ public static function classloader_provider(): array { 'overlap' => 'lib/tests/fixtures/component/overlap', ]; return [ - 'PSR-0 Classloading - Root' => [ - 'psr0' => $psr0, - 'psr4' => $psr4, - 'classname' => 'psr0_main', - 'includedfiles' => "{$directory}psr0/main.php", - ], - 'PSR-0 Classloading - Sub namespace - underscores' => [ - 'psr0' => $psr0, - 'psr4' => $psr4, - 'classname' => 'psr0_subnamespace_example', - 'includedfiles' => "{$directory}psr0/subnamespace/example.php", - ], - 'PSR-0 Classloading - Sub namespace - slashes' => [ - 'psr0' => $psr0, - 'psr4' => $psr4, - 'classname' => 'psr0\\subnamespace\\slashes', - 'includedfiles' => "{$directory}psr0/subnamespace/slashes.php", - ], - 'PSR-4 Classloading - Root' => [ - 'psr0' => $psr0, - 'psr4' => $psr4, - 'classname' => 'psr4\\main', - 'includedfiles' => "{$directory}psr4/main.php", - ], - 'PSR-4 Classloading - Sub namespace' => [ - 'psr0' => $psr0, - 'psr4' => $psr4, - 'classname' => 'psr4\\subnamespace\\example', - 'includedfiles' => "{$directory}psr4/subnamespace/example.php", - ], - 'PSR-4 Classloading - Ensure underscores are not converted to paths' => [ - 'psr0' => $psr0, - 'psr4' => $psr4, - 'classname' => 'psr4\\subnamespace\\underscore_example', - 'includedfiles' => "{$directory}psr4/subnamespace/underscore_example.php", - ], - 'Overlap - Ensure no unexpected problems with PSR-4 when overlapping namespaces.' => [ - 'psr0' => $psr0, - 'psr4' => $psr4, - 'classname' => 'overlap\\subnamespace\\example', - 'includedfiles' => "{$directory}overlap/subnamespace/example.php", - ], - 'Overlap - Ensure no unexpected problems with PSR-0 overlapping namespaces.' => [ - 'psr0' => $psr0, - 'psr4' => $psr4, - 'classname' => 'overlap_subnamespace_example2', - 'includedfiles' => "{$directory}overlap/subnamespace/example2.php", - ], + 'PSR-0 Classloading - Root' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr0_main', + 'includedfiles' => "{$directory}psr0/main.php", + ], + 'PSR-0 Classloading - Sub namespace - underscores' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr0_subnamespace_example', + 'includedfiles' => "{$directory}psr0/subnamespace/example.php", + ], + 'PSR-0 Classloading - Sub namespace - slashes' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr0\\subnamespace\\slashes', + 'includedfiles' => "{$directory}psr0/subnamespace/slashes.php", + ], + 'PSR-4 Classloading - Root' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr4\\main', + 'includedfiles' => "{$directory}psr4/main.php", + ], + 'PSR-4 Classloading - Sub namespace' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr4\\subnamespace\\example', + 'includedfiles' => "{$directory}psr4/subnamespace/example.php", + ], + 'PSR-4 Classloading - Ensure underscores are not converted to paths' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr4\\subnamespace\\underscore_example', + 'includedfiles' => "{$directory}psr4/subnamespace/underscore_example.php", + ], + 'Overlap - Ensure no unexpected problems with PSR-4 when overlapping namespaces.' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'overlap\\subnamespace\\example', + 'includedfiles' => "{$directory}overlap/subnamespace/example.php", + ], + 'Overlap - Ensure no unexpected problems with PSR-0 overlapping namespaces.' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'overlap_subnamespace_example2', + 'includedfiles' => "{$directory}overlap/subnamespace/example2.php", + ], ]; } @@ -829,66 +829,66 @@ public static function psr_classloader_provider(): array { 'overlap' => 'lib/tests/fixtures/component/overlap', ]; return [ - 'PSR-0 Classloading - Root' => [ - 'psr0' => $psr0, - 'psr4' => $psr4, - 'classname' => 'psr0_main', - 'file' => "{$directory}psr0/main.php", - ], - 'PSR-0 Classloading - Sub namespace - underscores' => [ - 'psr0' => $psr0, - 'psr4' => $psr4, - 'classname' => 'psr0_subnamespace_example', - 'file' => "{$directory}psr0/subnamespace/example.php", - ], - 'PSR-0 Classloading - Sub namespace - slashes' => [ - 'psr0' => $psr0, - 'psr4' => $psr4, - 'classname' => 'psr0\\subnamespace\\slashes', - 'file' => "{$directory}psr0/subnamespace/slashes.php", - ], - 'PSR-0 Classloading - non-existent file' => [ - 'psr0' => $psr0, - 'psr4' => $psr4, - 'classname' => 'psr0_subnamespace_nonexistent_file', - 'file' => false, - ], - 'PSR-4 Classloading - Root' => [ - 'psr0' => $psr0, - 'psr4' => $psr4, - 'classname' => 'psr4\\main', - 'file' => "{$directory}psr4/main.php", - ], - 'PSR-4 Classloading - Sub namespace' => [ - 'psr0' => $psr0, - 'psr4' => $psr4, - 'classname' => 'psr4\\subnamespace\\example', - 'file' => "{$directory}psr4/subnamespace/example.php", - ], - 'PSR-4 Classloading - Ensure underscores are not converted to paths' => [ - 'psr0' => $psr0, - 'psr4' => $psr4, - 'classname' => 'psr4\\subnamespace\\underscore_example', - 'file' => "{$directory}psr4/subnamespace/underscore_example.php", - ], - 'PSR-4 Classloading - non-existent file' => [ - 'psr0' => $psr0, - 'psr4' => $psr4, - 'classname' => 'psr4\\subnamespace\\nonexistent', - 'file' => false, - ], - 'Overlap - Ensure no unexpected problems with PSR-4 when overlapping namespaces.' => [ - 'psr0' => $psr0, - 'psr4' => $psr4, - 'classname' => 'overlap\\subnamespace\\example', - 'file' => "{$directory}overlap/subnamespace/example.php", - ], - 'Overlap - Ensure no unexpected problems with PSR-0 overlapping namespaces.' => [ - 'psr0' => $psr0, - 'psr4' => $psr4, - 'classname' => 'overlap_subnamespace_example2', - 'file' => "{$directory}overlap/subnamespace/example2.php", - ], + 'PSR-0 Classloading - Root' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr0_main', + 'file' => "{$directory}psr0/main.php", + ], + 'PSR-0 Classloading - Sub namespace - underscores' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr0_subnamespace_example', + 'file' => "{$directory}psr0/subnamespace/example.php", + ], + 'PSR-0 Classloading - Sub namespace - slashes' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr0\\subnamespace\\slashes', + 'file' => "{$directory}psr0/subnamespace/slashes.php", + ], + 'PSR-0 Classloading - non-existent file' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr0_subnamespace_nonexistent_file', + 'file' => false, + ], + 'PSR-4 Classloading - Root' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr4\\main', + 'file' => "{$directory}psr4/main.php", + ], + 'PSR-4 Classloading - Sub namespace' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr4\\subnamespace\\example', + 'file' => "{$directory}psr4/subnamespace/example.php", + ], + 'PSR-4 Classloading - Ensure underscores are not converted to paths' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr4\\subnamespace\\underscore_example', + 'file' => "{$directory}psr4/subnamespace/underscore_example.php", + ], + 'PSR-4 Classloading - non-existent file' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr4\\subnamespace\\nonexistent', + 'file' => false, + ], + 'Overlap - Ensure no unexpected problems with PSR-4 when overlapping namespaces.' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'overlap\\subnamespace\\example', + 'file' => "{$directory}overlap/subnamespace/example.php", + ], + 'Overlap - Ensure no unexpected problems with PSR-0 overlapping namespaces.' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'overlap_subnamespace_example2', + 'file' => "{$directory}overlap/subnamespace/example2.php", + ], 'PSR-4 namespaces can come from multiple sources - first source' => [ 'psr0' => $psr0, 'psr4' => [ From c115b6d6998174ddb214023e584cc4f2026f30d2 Mon Sep 17 00:00:00 2001 From: Laurent David Date: Mon, 8 Jul 2024 12:42:07 +0200 Subject: [PATCH 087/178] MDL-82319 core_courseformat: Remove redundant lock icon in section --- .../templates/local/content/section/content.mustache | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/course/format/templates/local/content/section/content.mustache b/course/format/templates/local/content/section/content.mustache index d0bc3553a006e..d4db117e78e2c 100644 --- a/course/format/templates/local/content/section/content.mustache +++ b/course/format/templates/local/content/section/content.mustache @@ -100,11 +100,13 @@ {{> core_courseformat/local/content/section/header }} {{/ core_courseformat/local/content/section/header }} {{/header}} - {{#restrictionlock}} -
- {{#pix}}t/unlock, core{{/pix}} -
- {{/restrictionlock}} + {{^singleheader}} + {{#restrictionlock}} +
+ {{#pix}}t/unlock, core{{/pix}} +
+ {{/restrictionlock}} + {{/singleheader}}
{{$ core_courseformat/local/content/section/badges }} {{> core_courseformat/local/content/section/badges }} From 406dcd25668041e44374b038306562e94b1847c6 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 19 Dec 2023 22:59:15 +0800 Subject: [PATCH 088/178] MDL-66903 testing: Add support for a \tests\ namespace during tests This commit: - introduces a \tests\ sub-namespace for use in unit tests only - the path to this the tests/classes directory of the owning parent - files here are excluded from unit test runs This is agreed per policy in MDL-80855. --- .upgradenotes/MDL-66903-2024070502035383.yml | 10 +++ lib/classes/component.php | 32 +++++++++ lib/phpunit/classes/util.php | 2 + lib/tests/component_test.php | 63 ++++++++++++++++++ phpunit.xml.dist | 70 ++++++++++++++++++++ 5 files changed, 177 insertions(+) create mode 100644 .upgradenotes/MDL-66903-2024070502035383.yml diff --git a/.upgradenotes/MDL-66903-2024070502035383.yml b/.upgradenotes/MDL-66903-2024070502035383.yml new file mode 100644 index 0000000000000..255dd885601b7 --- /dev/null +++ b/.upgradenotes/MDL-66903-2024070502035383.yml @@ -0,0 +1,10 @@ +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 + diff --git a/lib/classes/component.php b/lib/classes/component.php index 0c6063c9ecaaf..a33117f09f9b4 100644 --- a/lib/classes/component.php +++ b/lib/classes/component.php @@ -172,6 +172,38 @@ class_alias($newclassname, $classname); require($file); return; } + + if (PHPUNIT_TEST) { + // For unit tests we support classes in `\frankenstyle_component\tests\` to be loaded from + // `path/to/frankenstyle/component/tests/classes` directory. + // Note: We do *not* support the legacy `\frankenstyle_component_tests_style_classnames`. + if ($component = self::get_component_from_classname($classname)) { + $pathoptions = [ + '/tests/classes' => "{$component}\\tests\\", + '/tests/behat' => "{$component}\\behat\\", + ]; + foreach ($pathoptions as $path => $testnamespace) { + if (preg_match("#^" . preg_quote($testnamespace) . "#", $classname)) { + $path = self::get_component_directory($component) . $path; + $relativeclassname = str_replace( + $testnamespace, + '', + $classname, + ); + $file = sprintf( + "%s/%s.php", + $path, + str_replace('\\', '/', $relativeclassname), + ); + if (!empty($file) && file_exists($file)) { + require($file); + return; + } + break; + } + } + } + } } /** diff --git a/lib/phpunit/classes/util.php b/lib/phpunit/classes/util.php index 0e0bb2d1cb678..1ad2fde17a67c 100644 --- a/lib/phpunit/classes/util.php +++ b/lib/phpunit/classes/util.php @@ -529,6 +529,7 @@ public static function build_config_file() { $template = << @dir@ + @dir@/classes EOF; @@ -621,6 +622,7 @@ public static function build_component_config_files() { . + ./classes EOT; diff --git a/lib/tests/component_test.php b/lib/tests/component_test.php index 98ca138454df4..894a3e052276f 100644 --- a/lib/tests/component_test.php +++ b/lib/tests/component_test.php @@ -914,6 +914,69 @@ public static function psr_classloader_provider(): array { ]; } + /** + * Test that the classloader can load from the test namespaces. + */ + public function test_classloader_tests_namespace(): void { + global $CFG; + + $this->resetAfterTest(); + + $getclassfilecontent = function (string $classname, ?string $namespace): string { + if ($namespace) { + $content = " [ + 'classes' => [ + 'example.php' => $getclassfilecontent('example', 'core'), + ], + 'tests' => [ + 'classes' => [ + 'example_classname.php' => $getclassfilecontent('example_classname', \core\tests::class), + ], + 'behat' => [ + 'example_classname.php' => $getclassfilecontent('example_classname', \core\behat::class), + ], + ], + ], + ]); + + // Note: This is pretty hacky, but it's the only way to test the classloader. + // We have to override the dirroot and libdir, and then reset the plugintypes property. + $CFG->dirroot = $vfileroot->url(); + $CFG->libdir = $vfileroot->url() . '/lib'; + (new ReflectionProperty('core_component', 'plugintypes'))->setValue(null, null); + + // Existing classes do not break. + $this->assertTrue( + class_exists(\core\example::class), + ); + + // Test and behat classes work. + $this->assertTrue( + class_exists(\core\tests\example_classname::class), + ); + $this->assertTrue( + class_exists(\core\behat\example_classname::class), + ); + + // Non-existent classes do not do anything. + $this->assertFalse( + class_exists(\core\tests\example_classname_not_found::class), + ); + } + + public function tearDown(): void { + $plugintypes = new ReflectionProperty('core_component', 'plugintypes'); + $plugintypes->setValue(null, null); + } + /** * Test the PSR classloader. * diff --git a/phpunit.xml.dist b/phpunit.xml.dist index fd7faeecc081b..de77041fcf28c 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -35,54 +35,74 @@ lib/phpunit/tests + lib/phpunit/tests/classes lib/testing/tests + lib/testing/tests/classes lib/ddl/tests + lib/ddl/tests/classes lib/dml/tests + lib/dml/tests/classes lib/tests + lib/tests/classes lib/external/tests + lib/external/tests/classes favourites/tests + favourites/tests/classes lib/form/tests + lib/form/tests/classes lib/filestorage/tests lib/filebrowser/tests files/tests + lib/filestorage/tests/classes + lib/filebrowser/tests/classes + files/tests/classes filter/tests + filter/tests/classes admin/roles/tests + admin/roles/tests/classes cohort/tests + cohort/tests/classes lib/grade/tests grade/tests grade/grading/tests grade/import/csv/tests + lib/grade/tests/classes + grade/tests/classes + grade/grading/tests/classes + grade/import/csv/tests/classes analytics/tests + analytics/tests/classes availability/tests + availability/tests/classes backup/controller/tests @@ -90,135 +110,185 @@ backup/moodle2/tests backup/tests backup/util + backup/controller/tests/classes + backup/converter/moodle1/tests/classes + backup/moodle2/tests/classes + backup/tests/classes + backup/util/classes badges/tests + badges/tests/classes blog/tests + blog/tests/classes customfield/tests + customfield/tests/classes iplookup/tests + iplookup/tests/classes course/tests + course/tests/classes course/format/tests + course/format/tests/classes privacy/tests + privacy/tests/classes question/engine/tests question/tests question/type/tests question/engine/upgrade/tests + question/engine/tests/classes + question/tests/classes + question/type/tests/classes + question/engine/upgrade/tests/classes cache/tests + cache/tests/classes calendar/tests + calendar/tests/classes enrol/tests + enrol/tests/classes group/tests + group/tests/classes message/tests + message/tests/classes notes/tests + notes/tests/classes tag/tests + tag/tests/classes rating/tests + rating/tests/classes repository/tests + repository/tests/classes lib/userkey/tests + lib/userkey/tests/classes user/tests + user/tests/classes webservice/tests + webservice/tests/classes mnet/tests + mnet/tests/classes completion/tests + completion/tests/classes comment/tests + comment/tests/classes search/tests + search/tests/classes competency/tests + competency/tests/classes my/tests + my/tests/classes auth/tests + auth/tests/classes blocks/tests + blocks/tests/classes login/tests + login/tests/classes plagiarism/tests + plagiarism/tests/classes portfolio/tests + portfolio/tests/classes lib/editor/tests + lib/editor/tests/classes rss/tests + rss/tests/classes lib/table/tests + lib/table/tests/classes h5p/tests + h5p/tests/classes lib/xapi/tests + lib/xapi/tests/classes contentbank/tests + contentbank/tests/classes payment/tests + payment/tests/classes reportbuilder/tests + reportbuilder/tests/classes admin/presets/tests + admin/presets/tests/classes admin/tests + admin/tests/classes communication/tests + communication/tests/classes \ No newline at end of file From 5c135b614d3aa54772a2c9ed9465f676442f70e8 Mon Sep 17 00:00:00 2001 From: David Carrillo Date: Thu, 18 Jul 2024 16:45:41 +0200 Subject: [PATCH 147/178] MDL-82529 reportbuilder: Move functions from datasource to base class - 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 --- .upgradenotes/MDL-82529-2024071803142477.yml | 9 ++ reportbuilder/classes/datasource.php | 94 -------------------- reportbuilder/classes/local/report/base.php | 94 ++++++++++++++++++++ 3 files changed, 103 insertions(+), 94 deletions(-) create mode 100644 .upgradenotes/MDL-82529-2024071803142477.yml 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/reportbuilder/classes/datasource.php b/reportbuilder/classes/datasource.php index 9fa4d04a04c3c..f25e49498310b 100644 --- a/reportbuilder/classes/datasource.php +++ b/reportbuilder/classes/datasource.php @@ -47,41 +47,6 @@ abstract class datasource extends base { /** @var array $activeconditions */ private $activeconditions; - /** - * Add columns from the given entity name to be available to use in a custom report - * - * Wildcard matching is supported with '*' in both $include and $exclude, e.g. ['customfield*'] - * - * @param string $entityname - * @param string[] $include Include only these columns, if omitted then include all - * @param string[] $exclude Exclude these columns, if omitted then exclude none - * @throws coding_exception If both $include and $exclude are non-empty - */ - final protected function add_columns_from_entity(string $entityname, array $include = [], array $exclude = []): void { - if (!empty($include) && !empty($exclude)) { - throw new coding_exception('Cannot specify columns to include and exclude simultaneously'); - } - - $entity = $this->get_entity($entityname); - - // Retrieve filtered columns from entity, respecting given $include/$exclude parameters. - $columns = array_filter($entity->get_columns(), function(column $column) use ($include, $exclude): bool { - if (!empty($include)) { - return $this->report_element_search($column->get_name(), $include); - } - - if (!empty($exclude)) { - return !$this->report_element_search($column->get_name(), $exclude); - } - - return true; - }); - - foreach ($columns as $column) { - $this->add_column($column); - } - } - /** * Add default datasource columns to the report * @@ -180,41 +145,6 @@ public function get_active_columns(): array { return $this->activecolumns['values']; } - /** - * Add filters from the given entity name to be available to use in a custom report - * - * Wildcard matching is supported with '*' in both $include and $exclude, e.g. ['customfield*'] - * - * @param string $entityname - * @param string[] $include Include only these filters, if omitted then include all - * @param string[] $exclude Exclude these filters, if omitted then exclude none - * @throws coding_exception If both $include and $exclude are non-empty - */ - final protected function add_filters_from_entity(string $entityname, array $include = [], array $exclude = []): void { - if (!empty($include) && !empty($exclude)) { - throw new coding_exception('Cannot specify filters to include and exclude simultaneously'); - } - - $entity = $this->get_entity($entityname); - - // Retrieve filtered filters from entity, respecting given $include/$exclude parameters. - $filters = array_filter($entity->get_filters(), function(filter $filter) use ($include, $exclude): bool { - if (!empty($include)) { - return $this->report_element_search($filter->get_name(), $include); - } - - if (!empty($exclude)) { - return !$this->report_element_search($filter->get_name(), $exclude); - } - - return true; - }); - - foreach ($filters as $filter) { - $this->add_filter($filter); - } - } - /** * Add default datasource filters to the report * @@ -421,28 +351,4 @@ final protected function add_all_from_entities(array $entitynames = []): void { final public static function report_elements_modified(int $reportid): void { self::$elementsmodified[$reportid] = microtime(true); } - - /** - * Search for given element within list of search items, supporting '*' wildcards - * - * @param string $element - * @param string[] $search - * @return bool - */ - private function report_element_search(string $element, array $search): bool { - foreach ($search as $item) { - // Simple matching. - if ($element === $item) { - return true; - } - - // Wildcard matching. - if (strpos($item, '*') !== false) { - $pattern = '/^' . str_replace('\*', '.*', preg_quote($item)) . '$/'; - return (bool) preg_match($pattern, $element); - } - } - - return false; - } } diff --git a/reportbuilder/classes/local/report/base.php b/reportbuilder/classes/local/report/base.php index 5e7857505d1c2..c54145b149d06 100644 --- a/reportbuilder/classes/local/report/base.php +++ b/reportbuilder/classes/local/report/base.php @@ -374,6 +374,41 @@ final protected function add_column_from_entity(string $uniqueidentifier): colum return $this->add_column($this->get_entity($entityname)->get_column($columnname)); } + /** + * Add columns from the given entity name to be available to use in a custom report + * + * Wildcard matching is supported with '*' in both $include and $exclude, e.g. ['customfield*'] + * + * @param string $entityname + * @param string[] $include Include only these columns, if omitted then include all + * @param string[] $exclude Exclude these columns, if omitted then exclude none + * @throws coding_exception If both $include and $exclude are non-empty + */ + final protected function add_columns_from_entity(string $entityname, array $include = [], array $exclude = []): void { + if (!empty($include) && !empty($exclude)) { + throw new coding_exception('Cannot specify columns to include and exclude simultaneously'); + } + + $entity = $this->get_entity($entityname); + + // Retrieve filtered columns from entity, respecting given $include/$exclude parameters. + $columns = array_filter($entity->get_columns(), function(column $column) use ($include, $exclude): bool { + if (!empty($include)) { + return $this->report_element_search($column->get_name(), $include); + } + + if (!empty($exclude)) { + return !$this->report_element_search($column->get_name(), $exclude); + } + + return true; + }); + + foreach ($columns as $column) { + $this->add_column($column); + } + } + /** * Add given columns to the report from one or more entities * @@ -636,6 +671,41 @@ final protected function add_filter_from_entity(string $uniqueidentifier): filte return $this->add_filter($this->get_entity($entityname)->get_filter($filtername)); } + /** + * Add filters from the given entity name to be available to use in a custom report + * + * Wildcard matching is supported with '*' in both $include and $exclude, e.g. ['customfield*'] + * + * @param string $entityname + * @param string[] $include Include only these filters, if omitted then include all + * @param string[] $exclude Exclude these filters, if omitted then exclude none + * @throws coding_exception If both $include and $exclude are non-empty + */ + final protected function add_filters_from_entity(string $entityname, array $include = [], array $exclude = []): void { + if (!empty($include) && !empty($exclude)) { + throw new coding_exception('Cannot specify filters to include and exclude simultaneously'); + } + + $entity = $this->get_entity($entityname); + + // Retrieve filtered filters from entity, respecting given $include/$exclude parameters. + $filters = array_filter($entity->get_filters(), function(filter $filter) use ($include, $exclude): bool { + if (!empty($include)) { + return $this->report_element_search($filter->get_name(), $include); + } + + if (!empty($exclude)) { + return !$this->report_element_search($filter->get_name(), $exclude); + } + + return true; + }); + + foreach ($filters as $filter) { + $this->add_filter($filter); + } + } + /** * Add given filters to the report from one or more entities * @@ -829,4 +899,28 @@ public function add_attributes(array $attributes): self { public function get_attributes(): array { return $this->attributes; } + + /** + * Search for given element within list of search items, supporting '*' wildcards + * + * @param string $element + * @param string[] $search + * @return bool + */ + final protected function report_element_search(string $element, array $search): bool { + foreach ($search as $item) { + // Simple matching. + if ($element === $item) { + return true; + } + + // Wildcard matching. + if (strpos($item, '*') !== false) { + $pattern = '/^' . str_replace('\*', '.*', preg_quote($item)) . '$/'; + return (bool) preg_match($pattern, $element); + } + } + + return false; + } } From 78153dd73a2aaf86841b19dfe56cfe3b57903790 Mon Sep 17 00:00:00 2001 From: David Carrillo Date: Thu, 18 Jul 2024 16:46:55 +0200 Subject: [PATCH 148/178] MDL-82529 cohort: Add custom field filters to Cohorts system report --- .../local/systemreports/cohorts.php | 7 +---- cohort/tests/behat/view_cohorts.feature | 26 +++++++++++++++++-- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/cohort/classes/reportbuilder/local/systemreports/cohorts.php b/cohort/classes/reportbuilder/local/systemreports/cohorts.php index 84e2d151b0844..152838ed3132a 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*']); } /** 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 | From 26c42bf02dccd51048732d2b1db7224de03fc00f Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Fri, 19 Jul 2024 13:43:13 +0100 Subject: [PATCH 149/178] MDL-58287 courseformat: ensure all format plugins are returned. --- course/lib.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/course/lib.php b/course/lib.php index 3eca82d2fa232..d2c6222052d08 100644 --- a/course/lib.php +++ b/course/lib.php @@ -2982,7 +2982,8 @@ function include_course_editor(course_format $format) { */ function get_sorted_course_formats($enabledonly = false) { global $CFG; - $formats = core_component::get_plugin_list('format'); + + $formats = core_plugin_manager::instance()->get_installed_plugins('format'); if (!empty($CFG->format_plugins_sortorder)) { $order = explode(',', $CFG->format_plugins_sortorder); @@ -2996,7 +2997,9 @@ function get_sorted_course_formats($enabledonly = false) { } $sortedformats = array(); foreach ($order as $formatname) { - if (!get_config('format_'.$formatname, 'disabled')) { + $component = "format_{$formatname}"; + $componentdir = core_component::get_component_directory($component); + if ($componentdir !== null && !get_config($component, 'disabled')) { $sortedformats[] = $formatname; } } From 730c5413ceea4c4c6a84a16aa548a03cd3ac7e00 Mon Sep 17 00:00:00 2001 From: David Carrillo Date: Fri, 19 Jul 2024 17:09:58 +0200 Subject: [PATCH 150/178] MDL-82512 files: Add Author filter to files Report Builder entity --- files/classes/reportbuilder/local/entities/file.php | 10 ++++++++++ files/tests/reportbuilder/datasource/files_test.php | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/files/classes/reportbuilder/local/entities/file.php b/files/classes/reportbuilder/local/entities/file.php index f378f784c12ce..5a61fdf248bd2 100644 --- a/files/classes/reportbuilder/local/entities/file.php +++ b/files/classes/reportbuilder/local/entities/file.php @@ -393,6 +393,16 @@ protected function get_all_filters(): array { return $mimetypes; }); + // Author. + $filters[] = (new filter( + text::class, + 'author', + new lang_string('author', 'core_repository'), + $this->get_entity_name(), + "{$filesalias}.author" + )) + ->add_joins($this->get_joins()); + // License (consider null = 'unknown/license not specified' for filtering purposes). $filters[] = (new filter( select::class, diff --git a/files/tests/reportbuilder/datasource/files_test.php b/files/tests/reportbuilder/datasource/files_test.php index 8bc26ac8a3b8a..d1eac55236b7f 100644 --- a/files/tests/reportbuilder/datasource/files_test.php +++ b/files/tests/reportbuilder/datasource/files_test.php @@ -231,6 +231,12 @@ public static function datasource_filters_provider(): array { 'file:type_operator' => select::EQUAL_TO, 'file:type_value' => 'image/png', ], 0], + 'Filter author' => ['file:author', [ + 'file:author_operator' => text::IS_EMPTY, + ], 4], + 'Filter author (non match)' => ['file:author', [ + 'file:author_operator' => text::IS_NOT_EMPTY, + ], 0], 'Filter license' => ['file:license', [ 'file:license_operator' => select::EQUAL_TO, 'file:license_value' => 'unknown', From c3df4078e81a38f52401bc7e67d16f3699788f4f Mon Sep 17 00:00:00 2001 From: Daniel Ziegenberg Date: Fri, 19 Jul 2024 17:51:45 +0200 Subject: [PATCH 151/178] MDL-77167 javascript: Remove deprecation layer for YUI Events The deprecation layer was introduced with MDL-70990. Signed-off-by: Daniel Ziegenberg --- .upgradenotes/MDL-77167-2024062111140275.yml | 5 ++ blocks/amd/build/events.min.js | 2 +- blocks/amd/build/events.min.js.map | 2 +- blocks/amd/src/events.js | 17 ----- filter/amd/build/events.min.js | 5 +- filter/amd/build/events.min.js.map | 2 +- filter/amd/src/events.js | 21 ------- lib/amd/build/event.min.js | 5 +- lib/amd/build/event.min.js.map | 2 +- lib/amd/src/event.js | 23 ------- lib/editor/amd/build/events.min.js | 5 +- lib/editor/amd/build/events.min.js.map | 2 +- lib/editor/amd/src/events.js | 22 ------- lib/form/amd/build/events.min.js | 5 +- lib/form/amd/build/events.min.js.map | 2 +- lib/form/amd/src/events.js | 65 -------------------- 16 files changed, 23 insertions(+), 162 deletions(-) create mode 100644 .upgradenotes/MDL-77167-2024062111140275.yml 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/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/filter/amd/build/events.min.js b/filter/amd/build/events.min.js index 67d07e4ad2314..f30bd20922e04 100644 --- a/filter/amd/build/events.min.js +++ b/filter/amd/build/events.min.js @@ -1,4 +1,4 @@ -define("core_filters/events",["exports","core/event_dispatcher","core/normalise","jquery"],(function(_exports,_event_dispatcher,_normalise,_jquery){var obj; +define("core_filters/events",["exports","core/event_dispatcher","core/normalise"],(function(_exports,_event_dispatcher,_normalise){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.notifyFilterContentUpdated=_exports.notifyFilterContentRenderingComplete=_exports.eventTypes=void 0; /** * Javascript events for the `core_filters` subsystem. * @@ -13,6 +13,7 @@ define("core_filters/events",["exports","core/event_dispatcher","core/normalise" * document.addEventListener(filterEventTypes.filterContentUpdated, e => { * window.console.log(e.detail.nodes); // A list of the HTMLElements whose content was updated * }); - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.notifyFilterContentUpdated=_exports.notifyFilterContentRenderingComplete=_exports.eventTypes=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};const eventTypes={filterContentUpdated:"core_filters/contentUpdated",filterContentRenderingComplete:"core_filters/contentRenderingComplete"};_exports.eventTypes=eventTypes;_exports.notifyFilterContentUpdated=nodes=>(nodes=(0,_normalise.getList)(nodes),(0,_event_dispatcher.dispatchEvent)(eventTypes.filterContentUpdated,{nodes:nodes}));_exports.notifyFilterContentRenderingComplete=nodes=>(0,_event_dispatcher.dispatchEvent)(eventTypes.filterContentRenderingComplete,{nodes:nodes});let legacyEventsRegistered=!1;legacyEventsRegistered||(Y.use("event","moodle-core-event",(()=>{document.addEventListener(eventTypes.filterContentUpdated,(e=>{(0,_jquery.default)(document).trigger(M.core.event.FILTER_CONTENT_UPDATED,[(0,_jquery.default)(e.detail.nodes)]),Y.fire(M.core.event.FILTER_CONTENT_UPDATED,{nodes:new Y.NodeList(e.detail.nodes)})}))})),legacyEventsRegistered=!0)})); + */ +const eventTypes={filterContentUpdated:"core_filters/contentUpdated",filterContentRenderingComplete:"core_filters/contentRenderingComplete"};_exports.eventTypes=eventTypes;_exports.notifyFilterContentUpdated=nodes=>(nodes=(0,_normalise.getList)(nodes),(0,_event_dispatcher.dispatchEvent)(eventTypes.filterContentUpdated,{nodes:nodes}));_exports.notifyFilterContentRenderingComplete=nodes=>(0,_event_dispatcher.dispatchEvent)(eventTypes.filterContentRenderingComplete,{nodes:nodes})})); //# sourceMappingURL=events.min.js.map \ No newline at end of file diff --git a/filter/amd/build/events.min.js.map b/filter/amd/build/events.min.js.map index 108e0ff59a06b..c57784d22f4fb 100644 --- a/filter/amd/build/events.min.js.map +++ b/filter/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_filters` subsystem.\n *\n * @module core_filters/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 filter event.\n * import {eventTypes as filterEventTypes} from 'core_filters/events';\n *\n * document.addEventListener(filterEventTypes.filterContentUpdated, e => {\n * window.console.log(e.detail.nodes); // A list of the HTMLElements whose content was updated\n * });\n */\n\nimport {dispatchEvent} from 'core/event_dispatcher';\nimport {getList as normalistNodeList} from 'core/normalise';\nimport jQuery from 'jquery';\n\n/**\n * Events for the `core_filters` subsystem.\n *\n * @constant\n * @property {String} filterContentUpdated See {@link event:filterContentUpdated}\n * @property {String} filterContentRenderingComplete See {@link event:filterContentRenderingComplete}\n */\nexport const eventTypes = {\n /**\n * An event triggered when page content is updated and must be processed by the filter system.\n *\n * An example of this is loading user text that could have equations in it. MathJax can typeset the equations but\n * only if it is notified that there are new nodes in the page that need processing.\n *\n * @event filterContentUpdated\n * @type {CustomEvent}\n * @property {object} detail\n * @property {NodeElement[]} detail.nodes The list of parent nodes which were updated\n */\n filterContentUpdated: 'core_filters/contentUpdated',\n\n /**\n * An event triggered when filter system have done rendering the content using the filter system.\n *\n * @event filterContentRenderingComplete\n * @type {CustomEvent}\n * @property {object} detail\n */\n filterContentRenderingComplete: 'core_filters/contentRenderingComplete',\n};\n\n/**\n * Trigger an event to indicate that the specified nodes were updated and should be processed by the filter system.\n *\n * @method notifyFilterContentUpdated\n * @param {jQuery|Array} nodes\n * @returns {CustomEvent}\n * @fires filterContentUpdated\n */\nexport const notifyFilterContentUpdated = nodes => {\n // Historically this could be a jQuery Object.\n // Normalise the list of nodes to a NodeList.\n nodes = normalistNodeList(nodes);\n\n return dispatchEvent(eventTypes.filterContentUpdated, {nodes});\n};\n\n/**\n * Trigger an event to indicate that the filter has been processed.\n *\n * @method notifyFilterContentRenderingComplete\n * @param {NodeList|Node[]} nodes List of nodes that has been modified by filter\n * @returns {CustomEvent}\n * @fires filterContentRenderingComplete\n */\nexport const notifyFilterContentRenderingComplete = nodes => {\n return dispatchEvent(eventTypes.filterContentRenderingComplete, {nodes});\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', () => {\n // Provide a backwards-compatability layer for YUI Events.\n document.addEventListener(eventTypes.filterContentUpdated, e => {\n // Trigger the legacy jQuery event.\n jQuery(document).trigger(M.core.event.FILTER_CONTENT_UPDATED, [jQuery(e.detail.nodes)]);\n\n // Trigger the legacy YUI event.\n Y.fire(M.core.event.FILTER_CONTENT_UPDATED, {nodes: new Y.NodeList(e.detail.nodes)});\n });\n });\n\n legacyEventsRegistered = true;\n}\n"],"names":["eventTypes","filterContentUpdated","filterContentRenderingComplete","nodes","legacyEventsRegistered","Y","use","document","addEventListener","e","trigger","M","core","event","FILTER_CONTENT_UPDATED","detail","fire","NodeList"],"mappings":";;;;;;;;;;;;;;;wOAyCaA,WAAa,CAYtBC,qBAAsB,8BAStBC,+BAAgC,4GAWMC,QAGtCA,OAAQ,sBAAkBA,QAEnB,mCAAcH,WAAWC,qBAAsB,CAACE,MAAAA,uDAWPA,QACzC,mCAAcH,WAAWE,+BAAgC,CAACC,MAAAA,YAGjEC,wBAAyB,EACxBA,yBAKDC,EAAEC,IAAI,QAAS,qBAAqB,KAEhCC,SAASC,iBAAiBR,WAAWC,sBAAsBQ,wBAEhDF,UAAUG,QAAQC,EAAEC,KAAKC,MAAMC,uBAAwB,EAAC,mBAAOL,EAAEM,OAAOZ,SAG/EE,EAAEW,KAAKL,EAAEC,KAAKC,MAAMC,uBAAwB,CAACX,MAAO,IAAIE,EAAEY,SAASR,EAAEM,OAAOZ,eAIpFC,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_filters` subsystem.\n *\n * @module core_filters/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 filter event.\n * import {eventTypes as filterEventTypes} from 'core_filters/events';\n *\n * document.addEventListener(filterEventTypes.filterContentUpdated, e => {\n * window.console.log(e.detail.nodes); // A list of the HTMLElements whose content was updated\n * });\n */\n\nimport {dispatchEvent} from 'core/event_dispatcher';\nimport {getList as normalistNodeList} from 'core/normalise';\n\n/**\n * Events for the `core_filters` subsystem.\n *\n * @constant\n * @property {String} filterContentUpdated See {@link event:filterContentUpdated}\n * @property {String} filterContentRenderingComplete See {@link event:filterContentRenderingComplete}\n */\nexport const eventTypes = {\n /**\n * An event triggered when page content is updated and must be processed by the filter system.\n *\n * An example of this is loading user text that could have equations in it. MathJax can typeset the equations but\n * only if it is notified that there are new nodes in the page that need processing.\n *\n * @event filterContentUpdated\n * @type {CustomEvent}\n * @property {object} detail\n * @property {NodeElement[]} detail.nodes The list of parent nodes which were updated\n */\n filterContentUpdated: 'core_filters/contentUpdated',\n\n /**\n * An event triggered when filter system have done rendering the content using the filter system.\n *\n * @event filterContentRenderingComplete\n * @type {CustomEvent}\n * @property {object} detail\n */\n filterContentRenderingComplete: 'core_filters/contentRenderingComplete',\n};\n\n/**\n * Trigger an event to indicate that the specified nodes were updated and should be processed by the filter system.\n *\n * @method notifyFilterContentUpdated\n * @param {jQuery|Array} nodes\n * @returns {CustomEvent}\n * @fires filterContentUpdated\n */\nexport const notifyFilterContentUpdated = nodes => {\n // Historically this could be a jQuery Object.\n // Normalise the list of nodes to a NodeList.\n nodes = normalistNodeList(nodes);\n\n return dispatchEvent(eventTypes.filterContentUpdated, {nodes});\n};\n\n/**\n * Trigger an event to indicate that the filter has been processed.\n *\n * @method notifyFilterContentRenderingComplete\n * @param {NodeList|Node[]} nodes List of nodes that has been modified by filter\n * @returns {CustomEvent}\n * @fires filterContentRenderingComplete\n */\nexport const notifyFilterContentRenderingComplete = nodes => {\n return dispatchEvent(eventTypes.filterContentRenderingComplete, {nodes});\n};\n"],"names":["eventTypes","filterContentUpdated","filterContentRenderingComplete","nodes"],"mappings":";;;;;;;;;;;;;;;;MAwCaA,WAAa,CAYtBC,qBAAsB,8BAStBC,+BAAgC,4GAWMC,QAGtCA,OAAQ,sBAAkBA,QAEnB,mCAAcH,WAAWC,qBAAsB,CAACE,MAAAA,uDAWPA,QACzC,mCAAcH,WAAWE,+BAAgC,CAACC,MAAAA"} \ No newline at end of file diff --git a/filter/amd/src/events.js b/filter/amd/src/events.js index 614520ea53417..541277a2256f4 100644 --- a/filter/amd/src/events.js +++ b/filter/amd/src/events.js @@ -30,7 +30,6 @@ import {dispatchEvent} from 'core/event_dispatcher'; import {getList as normalistNodeList} from 'core/normalise'; -import jQuery from 'jquery'; /** * Events for the `core_filters` subsystem. @@ -90,23 +89,3 @@ export const notifyFilterContentUpdated = nodes => { export const notifyFilterContentRenderingComplete = nodes => { return dispatchEvent(eventTypes.filterContentRenderingComplete, {nodes}); }; - -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', () => { - // Provide a backwards-compatability layer for YUI Events. - document.addEventListener(eventTypes.filterContentUpdated, e => { - // Trigger the legacy jQuery event. - jQuery(document).trigger(M.core.event.FILTER_CONTENT_UPDATED, [jQuery(e.detail.nodes)]); - - // Trigger the legacy YUI event. - Y.fire(M.core.event.FILTER_CONTENT_UPDATED, {nodes: new Y.NodeList(e.detail.nodes)}); - }); - }); - - legacyEventsRegistered = true; -} diff --git a/lib/amd/build/event.min.js b/lib/amd/build/event.min.js index d2c8208fdcde9..27b8687a7f31e 100644 --- a/lib/amd/build/event.min.js +++ b/lib/amd/build/event.min.js @@ -1,4 +1,4 @@ -define("core/event",["exports","core_editor/events","core_filters/events","core_form/events","jquery","core/yui"],(function(_exports,_events,_events2,_events3,_jquery,_yui){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +define("core/event",["exports","core_editor/events","core_filters/events","core_form/events"],(function(_exports,_events,_events2,_events3){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0; /** * Global registry of core events that can be triggered/listened for. * @@ -6,6 +6,7 @@ define("core/event",["exports","core_editor/events","core_filters/events","core_ * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.0 - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),_yui=_interopRequireDefault(_yui);const getRenamedLegacyFunction=(oldFunctionName,newModule,newFunctionName,newFunctionRef)=>function(){return window.console.warn("The core/event::".concat(oldFunctionName,"() function has been moved to ").concat(newModule,"::").concat(newFunctionName,". ")+"Please update your code to use the new module."),newFunctionRef(...arguments)};var _default={Events:{FORM_FIELD_VALIDATION:"core_form-field-validation"},getLegacyEvents:()=>{const result=_jquery.default.Deferred();return window.console.warn("The getLegacyEvents function has been deprecated. Please update your code to use native events."),_yui.default.use("event","moodle-core-event",(function(){result.resolve(window.M.core.event)})),result.promise()},notifyEditorContentRestored:getRenamedLegacyFunction("notifyEditorContentRestored","core_editor/events","notifyEditorContentRestored",_events.notifyEditorContentRestored),notifyFilterContentUpdated:getRenamedLegacyFunction("notifyFilterContentUpdated","core_filters/events","notifyFilterContentUpdated",_events2.notifyFilterContentUpdated),notifyFormSubmitAjax:getRenamedLegacyFunction("notifyFormSubmitAjax","core_form/events","notifyFormSubmittedByJavascript",_events3.notifyFormSubmittedByJavascript)};return _exports.default=_default,_exports.default})); + */ +const getRenamedLegacyFunction=(oldFunctionName,newModule,newFunctionName,newFunctionRef)=>function(){return window.console.warn("The core/event::".concat(oldFunctionName,"() function has been moved to ").concat(newModule,"::").concat(newFunctionName,". ")+"Please update your code to use the new module."),newFunctionRef(...arguments)};var _default={Events:{FORM_FIELD_VALIDATION:"core_form-field-validation"},notifyEditorContentRestored:getRenamedLegacyFunction("notifyEditorContentRestored","core_editor/events","notifyEditorContentRestored",_events.notifyEditorContentRestored),notifyFilterContentUpdated:getRenamedLegacyFunction("notifyFilterContentUpdated","core_filters/events","notifyFilterContentUpdated",_events2.notifyFilterContentUpdated),notifyFormSubmitAjax:getRenamedLegacyFunction("notifyFormSubmitAjax","core_form/events","notifyFormSubmittedByJavascript",_events3.notifyFormSubmittedByJavascript)};return _exports.default=_default,_exports.default})); //# sourceMappingURL=event.min.js.map \ No newline at end of file diff --git a/lib/amd/build/event.min.js.map b/lib/amd/build/event.min.js.map index 61d4dea3d46f6..12485c1338341 100644 --- a/lib/amd/build/event.min.js.map +++ b/lib/amd/build/event.min.js.map @@ -1 +1 @@ -{"version":3,"file":"event.min.js","sources":["../src/event.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 * Global registry of core events that can be triggered/listened for.\n *\n * @module core/event\n * @copyright 2015 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.0\n */\n\nimport {notifyEditorContentRestored} from 'core_editor/events';\nimport {notifyFilterContentUpdated} from 'core_filters/events';\nimport {notifyFormSubmittedByJavascript} from 'core_form/events';\n\n// These are only imported for legacy.\nimport $ from 'jquery';\nimport Y from 'core/yui';\n\n// These are AMD only events - no backwards compatibility for new things.\n// Note: No new events should be created here.\nconst Events = {\n FORM_FIELD_VALIDATION: \"core_form-field-validation\"\n};\n\n/**\n * Load the legacy YUI module which defines events in M.core.event and return it.\n *\n * @method getLegacyEvents\n * @return {Promise}\n * @deprecated\n */\nconst getLegacyEvents = () => {\n const result = $.Deferred();\n window.console.warn(\"The getLegacyEvents function has been deprecated. Please update your code to use native events.\");\n\n Y.use('event', 'moodle-core-event', function() {\n result.resolve(window.M.core.event);\n });\n return result.promise();\n};\n\n/**\n * Get a curried function to warn that a function has been moved and renamed\n *\n * @param {String} oldFunctionName\n * @param {String} newModule\n * @param {String} newFunctionName\n * @param {Function} newFunctionRef\n * @returns {Function}\n */\nconst getRenamedLegacyFunction = (oldFunctionName, newModule, newFunctionName, newFunctionRef) => (...args) => {\n window.console.warn(\n `The core/event::${oldFunctionName}() function has been moved to ${newModule}::${newFunctionName}. ` +\n `Please update your code to use the new module.`\n );\n\n return newFunctionRef(...args);\n};\n\nexport default {\n Events,\n getLegacyEvents,\n\n notifyEditorContentRestored: getRenamedLegacyFunction(\n 'notifyEditorContentRestored',\n 'core_editor/events',\n 'notifyEditorContentRestored',\n notifyEditorContentRestored\n ),\n\n notifyFilterContentUpdated: getRenamedLegacyFunction(\n 'notifyFilterContentUpdated',\n 'core_filters/events',\n 'notifyFilterContentUpdated',\n notifyFilterContentUpdated\n ),\n\n notifyFormSubmitAjax: getRenamedLegacyFunction(\n 'notifyFormSubmitAjax',\n 'core_form/events',\n 'notifyFormSubmittedByJavascript',\n notifyFormSubmittedByJavascript\n ),\n};\n"],"names":["getRenamedLegacyFunction","oldFunctionName","newModule","newFunctionName","newFunctionRef","window","console","warn","Events","FORM_FIELD_VALIDATION","getLegacyEvents","result","$","Deferred","use","resolve","M","core","event","promise","notifyEditorContentRestored","notifyFilterContentUpdated","notifyFormSubmitAjax","notifyFormSubmittedByJavascript"],"mappings":";;;;;;;;qKAgEMA,yBAA2B,CAACC,gBAAiBC,UAAWC,gBAAiBC,iBAAmB,kBAC9FC,OAAOC,QAAQC,KACX,0BAAmBN,yDAAgDC,uBAAcC,wEAI9EC,2CAGI,CACXI,OAxCW,CACXC,sBAAuB,8BAwCvBC,gBA9BoB,WACdC,OAASC,gBAAEC,kBACjBR,OAAOC,QAAQC,KAAK,gHAElBO,IAAI,QAAS,qBAAqB,WAChCH,OAAOI,QAAQV,OAAOW,EAAEC,KAAKC,UAE1BP,OAAOQ,WAyBdC,4BAA6BpB,yBACzB,8BACA,qBACA,8BACAoB,qCAGJC,2BAA4BrB,yBACxB,6BACA,sBACA,6BACAqB,qCAGJC,qBAAsBtB,yBAClB,uBACA,mBACA,kCACAuB"} \ No newline at end of file +{"version":3,"file":"event.min.js","sources":["../src/event.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 * Global registry of core events that can be triggered/listened for.\n *\n * @module core/event\n * @copyright 2015 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.0\n */\n\nimport {notifyEditorContentRestored} from 'core_editor/events';\nimport {notifyFilterContentUpdated} from 'core_filters/events';\nimport {notifyFormSubmittedByJavascript} from 'core_form/events';\n\n// These are AMD only events - no backwards compatibility for new things.\n// Note: No new events should be created here.\nconst Events = {\n FORM_FIELD_VALIDATION: \"core_form-field-validation\"\n};\n\n/**\n * Get a curried function to warn that a function has been moved and renamed\n *\n * @param {String} oldFunctionName\n * @param {String} newModule\n * @param {String} newFunctionName\n * @param {Function} newFunctionRef\n * @returns {Function}\n */\nconst getRenamedLegacyFunction = (oldFunctionName, newModule, newFunctionName, newFunctionRef) => (...args) => {\n window.console.warn(\n `The core/event::${oldFunctionName}() function has been moved to ${newModule}::${newFunctionName}. ` +\n `Please update your code to use the new module.`\n );\n\n return newFunctionRef(...args);\n};\n\nexport default {\n Events,\n notifyEditorContentRestored: getRenamedLegacyFunction(\n 'notifyEditorContentRestored',\n 'core_editor/events',\n 'notifyEditorContentRestored',\n notifyEditorContentRestored\n ),\n\n notifyFilterContentUpdated: getRenamedLegacyFunction(\n 'notifyFilterContentUpdated',\n 'core_filters/events',\n 'notifyFilterContentUpdated',\n notifyFilterContentUpdated\n ),\n\n notifyFormSubmitAjax: getRenamedLegacyFunction(\n 'notifyFormSubmitAjax',\n 'core_form/events',\n 'notifyFormSubmittedByJavascript',\n notifyFormSubmittedByJavascript\n ),\n};\n"],"names":["getRenamedLegacyFunction","oldFunctionName","newModule","newFunctionName","newFunctionRef","window","console","warn","Events","FORM_FIELD_VALIDATION","notifyEditorContentRestored","notifyFilterContentUpdated","notifyFormSubmitAjax","notifyFormSubmittedByJavascript"],"mappings":";;;;;;;;;MA2CMA,yBAA2B,CAACC,gBAAiBC,UAAWC,gBAAiBC,iBAAmB,kBAC9FC,OAAOC,QAAQC,KACX,0BAAmBN,yDAAgDC,uBAAcC,wEAI9EC,2CAGI,CACXI,OAvBW,CACXC,sBAAuB,8BAuBvBC,4BAA6BV,yBACzB,8BACA,qBACA,8BACAU,qCAGJC,2BAA4BX,yBACxB,6BACA,sBACA,6BACAW,qCAGJC,qBAAsBZ,yBAClB,uBACA,mBACA,kCACAa"} \ No newline at end of file diff --git a/lib/amd/src/event.js b/lib/amd/src/event.js index efdc274588ef1..8987a0d18d2b0 100644 --- a/lib/amd/src/event.js +++ b/lib/amd/src/event.js @@ -26,33 +26,12 @@ import {notifyEditorContentRestored} from 'core_editor/events'; import {notifyFilterContentUpdated} from 'core_filters/events'; import {notifyFormSubmittedByJavascript} from 'core_form/events'; -// These are only imported for legacy. -import $ from 'jquery'; -import Y from 'core/yui'; - // These are AMD only events - no backwards compatibility for new things. // Note: No new events should be created here. const Events = { FORM_FIELD_VALIDATION: "core_form-field-validation" }; -/** - * Load the legacy YUI module which defines events in M.core.event and return it. - * - * @method getLegacyEvents - * @return {Promise} - * @deprecated - */ -const getLegacyEvents = () => { - const result = $.Deferred(); - window.console.warn("The getLegacyEvents function has been deprecated. Please update your code to use native events."); - - Y.use('event', 'moodle-core-event', function() { - result.resolve(window.M.core.event); - }); - return result.promise(); -}; - /** * Get a curried function to warn that a function has been moved and renamed * @@ -73,8 +52,6 @@ const getRenamedLegacyFunction = (oldFunctionName, newModule, newFunctionName, n export default { Events, - getLegacyEvents, - notifyEditorContentRestored: getRenamedLegacyFunction( 'notifyEditorContentRestored', 'core_editor/events', diff --git a/lib/editor/amd/build/events.min.js b/lib/editor/amd/build/events.min.js index e0f68d1b5c8db..b573733be8882 100644 --- a/lib/editor/amd/build/events.min.js +++ b/lib/editor/amd/build/events.min.js @@ -1,4 +1,4 @@ -define("core_editor/events",["exports","core/event_dispatcher","jquery","core/yui"],(function(_exports,_event_dispatcher,_jquery,_yui){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +define("core_editor/events",["exports","core/event_dispatcher"],(function(_exports,_event_dispatcher){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.notifyEditorContentRestored=_exports.eventTypes=void 0; /** * Javascript events for the `core_editor` subsystem. * @@ -6,6 +6,7 @@ define("core_editor/events",["exports","core/event_dispatcher","jquery","core/yu * @copyright 2021 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 4.0 - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.notifyEditorContentRestored=_exports.eventTypes=void 0,_jquery=_interopRequireDefault(_jquery),_yui=_interopRequireDefault(_yui);const eventTypes={editorContentRestored:"core_editor/contentRestored"};_exports.eventTypes=eventTypes;_exports.notifyEditorContentRestored=editor=>(editor||window.console.warn("The HTMLElement representing the editor that was modified should be provided to notifyEditorContentRestored."),(0,_event_dispatcher.dispatchEvent)(eventTypes.editorContentRestored,{},editor||document));let legacyEventsRegistered=!1;legacyEventsRegistered||(_yui.default.use("event","moodle-core-event",(()=>{document.addEventListener(eventTypes.editorContentRestored,(()=>{(0,_jquery.default)(document).trigger(M.core.event.EDITOR_CONTENT_RESTORED),_yui.default.fire(M.core.event.EDITOR_CONTENT_RESTORED)}))})),legacyEventsRegistered=!0)})); + */ +const eventTypes={editorContentRestored:"core_editor/contentRestored"};_exports.eventTypes=eventTypes;_exports.notifyEditorContentRestored=editor=>(editor||window.console.warn("The HTMLElement representing the editor that was modified should be provided to notifyEditorContentRestored."),(0,_event_dispatcher.dispatchEvent)(eventTypes.editorContentRestored,{},editor||document))})); //# sourceMappingURL=events.min.js.map \ No newline at end of file diff --git a/lib/editor/amd/build/events.min.js.map b/lib/editor/amd/build/events.min.js.map index 3f8fe007e3c48..51508240dcacd 100644 --- a/lib/editor/amd/build/events.min.js.map +++ b/lib/editor/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_editor` subsystem.\n *\n * @module core_editor/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\nimport {dispatchEvent} from 'core/event_dispatcher';\nimport jQuery from 'jquery';\nimport Y from 'core/yui';\n\n/**\n * Events for the `core_editor` subsystem.\n *\n * @constant\n * @property {String} editorContentRestored See {@link event:editorContentRestored}\n */\nexport const eventTypes = {\n /**\n * An event triggered when an editor restores auto-saved content.\n *\n * @event editorContentRestored\n */\n editorContentRestored: 'core_editor/contentRestored',\n};\n\n/**\n * Trigger an event to indicate that editor content was restored.\n *\n * @method notifyEditorContentRestored\n * @param {HTMLElement|null} editor The element that was modified\n * @returns {CustomEvent}\n * @fires editorContentRestored\n */\nexport const notifyEditorContentRestored = editor => {\n if (!editor) {\n window.console.warn(\n `The HTMLElement representing the editor that was modified should be provided to notifyEditorContentRestored.`\n );\n }\n return dispatchEvent(\n eventTypes.editorContentRestored,\n {},\n editor || document\n );\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', () => {\n // Provide a backwards-compatability layer for YUI Events.\n document.addEventListener(eventTypes.editorContentRestored, () => {\n // Trigger a legacy AMD event.\n jQuery(document).trigger(M.core.event.EDITOR_CONTENT_RESTORED);\n\n // Trigger a legacy YUI event.\n Y.fire(M.core.event.EDITOR_CONTENT_RESTORED);\n });\n });\n\n legacyEventsRegistered = true;\n}\n"],"names":["eventTypes","editorContentRestored","editor","window","console","warn","document","legacyEventsRegistered","use","addEventListener","trigger","M","core","event","EDITOR_CONTENT_RESTORED","fire"],"mappings":";;;;;;;;6MAiCaA,WAAa,CAMtBC,sBAAuB,mGAWgBC,SAClCA,QACDC,OAAOC,QAAQC,sHAIZ,mCACHL,WAAWC,sBACX,GACAC,QAAUI,eAIdC,wBAAyB,EACxBA,sCAKCC,IAAI,QAAS,qBAAqB,KAEhCF,SAASG,iBAAiBT,WAAWC,uBAAuB,yBAEjDK,UAAUI,QAAQC,EAAEC,KAAKC,MAAMC,sCAGpCC,KAAKJ,EAAEC,KAAKC,MAAMC,+BAI5BP,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_editor` subsystem.\n *\n * @module core_editor/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\nimport {dispatchEvent} from 'core/event_dispatcher';\n\n/**\n * Events for the `core_editor` subsystem.\n *\n * @constant\n * @property {String} editorContentRestored See {@link event:editorContentRestored}\n */\nexport const eventTypes = {\n /**\n * An event triggered when an editor restores auto-saved content.\n *\n * @event editorContentRestored\n */\n editorContentRestored: 'core_editor/contentRestored',\n};\n\n/**\n * Trigger an event to indicate that editor content was restored.\n *\n * @method notifyEditorContentRestored\n * @param {HTMLElement|null} editor The element that was modified\n * @returns {CustomEvent}\n * @fires editorContentRestored\n */\nexport const notifyEditorContentRestored = editor => {\n if (!editor) {\n window.console.warn(\n `The HTMLElement representing the editor that was modified should be provided to notifyEditorContentRestored.`\n );\n }\n return dispatchEvent(\n eventTypes.editorContentRestored,\n {},\n editor || document\n );\n};\n"],"names":["eventTypes","editorContentRestored","editor","window","console","warn","document"],"mappings":";;;;;;;;;MA+BaA,WAAa,CAMtBC,sBAAuB,mGAWgBC,SAClCA,QACDC,OAAOC,QAAQC,sHAIZ,mCACHL,WAAWC,sBACX,GACAC,QAAUI"} \ No newline at end of file diff --git a/lib/editor/amd/src/events.js b/lib/editor/amd/src/events.js index 6495b33171569..24309c2a4a985 100644 --- a/lib/editor/amd/src/events.js +++ b/lib/editor/amd/src/events.js @@ -22,8 +22,6 @@ */ import {dispatchEvent} from 'core/event_dispatcher'; -import jQuery from 'jquery'; -import Y from 'core/yui'; /** * Events for the `core_editor` subsystem. @@ -60,23 +58,3 @@ export const notifyEditorContentRestored = editor => { editor || document ); }; - -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', () => { - // Provide a backwards-compatability layer for YUI Events. - document.addEventListener(eventTypes.editorContentRestored, () => { - // Trigger a legacy AMD event. - jQuery(document).trigger(M.core.event.EDITOR_CONTENT_RESTORED); - - // Trigger a legacy YUI event. - Y.fire(M.core.event.EDITOR_CONTENT_RESTORED); - }); - }); - - legacyEventsRegistered = true; -} diff --git a/lib/form/amd/build/events.min.js b/lib/form/amd/build/events.min.js index 2dcca565d70bd..f8e2f11c95420 100644 --- a/lib/form/amd/build/events.min.js +++ b/lib/form/amd/build/events.min.js @@ -1,4 +1,4 @@ -define("core_form/events",["exports","core/str","core/event_dispatcher","jquery","core/yui"],(function(_exports,_str,_event_dispatcher,_jquery,_yui){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +define("core_form/events",["exports","core/str","core/event_dispatcher"],(function(_exports,_str,_event_dispatcher){ /** * Javascript events for the `core_form` subsystem. * @@ -14,6 +14,7 @@ define("core_form/events",["exports","core/str","core/event_dispatcher","jquery" * window.console.log(e.target); // The form that was submitted. * window.console.log(e.detail.skipValidation); // Whether form validation was skipped. * }); - */let changesMadeString;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.types=_exports.triggerUploadStarted=_exports.triggerUploadCompleted=_exports.notifyUploadStarted=_exports.notifyUploadCompleted=_exports.notifyUploadChanged=_exports.notifyFormSubmittedByJavascript=_exports.notifyFormError=_exports.notifyFieldValidationFailure=_exports.notifyFieldStructureChanged=_exports.eventTypes=void 0,_jquery=_interopRequireDefault(_jquery),_yui=_interopRequireDefault(_yui);const changesMadeCheck=e=>{e&&(e.returnValue=changesMadeString)},eventTypes={formError:"core_form/error",formSubmittedByJavascript:"core_form/submittedByJavascript",formFieldValidationFailed:"core_form/fieldValidationFailed",uploadStarted:"core_form/uploadStarted",uploadCompleted:"core_form/uploadCompleted",uploadChanged:"core_form/uploadChanged",fieldStructureChanged:"core_form/fieldStructureChanged"};_exports.eventTypes=eventTypes;_exports.notifyFormError=field=>(0,_event_dispatcher.dispatchEvent)(eventTypes.formError,{},field);_exports.notifyFormSubmittedByJavascript=function(form){let skipValidation=arguments.length>1&&void 0!==arguments[1]&&arguments[1],fallbackHandled=arguments.length>2&&void 0!==arguments[2]&&arguments[2];skipValidation&&(window.skipClientValidation=!0);const customEvent=(0,_event_dispatcher.dispatchEvent)(eventTypes.formSubmittedByJavascript,{skipValidation:skipValidation,fallbackHandled:fallbackHandled},form);return skipValidation&&(window.skipClientValidation=!1),customEvent};_exports.notifyFieldValidationFailure=(field,message)=>(0,_event_dispatcher.dispatchEvent)(eventTypes.formFieldValidationFailed,{message:message},field,{cancelable:!0});const notifyUploadStarted=async elementId=>(changesMadeString=await(0,_str.getString)("changesmadereallygoaway","moodle"),window.addEventListener("beforeunload",changesMadeCheck),(0,_event_dispatcher.dispatchEvent)(eventTypes.uploadStarted,{},document.getElementById(elementId),{bubbles:!0,cancellable:!1}));_exports.notifyUploadStarted=notifyUploadStarted;const notifyUploadCompleted=elementId=>(window.removeEventListener("beforeunload",changesMadeCheck),(0,_event_dispatcher.dispatchEvent)(eventTypes.uploadCompleted,{},document.getElementById(elementId),{bubbles:!0,cancellable:!1}));_exports.notifyUploadCompleted=notifyUploadCompleted;const triggerUploadStarted=notifyUploadStarted;_exports.triggerUploadStarted=triggerUploadStarted;const triggerUploadCompleted=notifyUploadCompleted;_exports.triggerUploadCompleted=triggerUploadCompleted;_exports.types={uploadStarted:"core_form/uploadStarted",uploadCompleted:"core_form/uploadCompleted"};let legacyEventsRegistered=!1;legacyEventsRegistered||(_yui.default.use("event","moodle-core-event",(()=>{document.addEventListener(eventTypes.formError,(e=>{const element=_yui.default.one(e.target),formElement=_yui.default.one(e.target.closest("form"));_yui.default.Global.fire(M.core.globalEvents.FORM_ERROR,{formid:formElement.generateID(),elementid:element.generateID()})})),document.addEventListener(eventTypes.formSubmittedByJavascript,(e=>{if(e.detail.fallbackHandled)return;e.skipValidation&&(window.skipClientValidation=!0);const form=_yui.default.one(e.target);form.fire(M.core.event.FORM_SUBMIT_AJAX,{currentTarget:form,fallbackHandled:!0}),e.skipValidation&&(window.skipClientValidation=!1)}))})),document.addEventListener(eventTypes.formFieldValidationFailed,(e=>{const legacyEvent=_jquery.default.Event("core_form-field-validation");(0,_jquery.default)(e.target).trigger(legacyEvent,e.detail.message)})),legacyEventsRegistered=!0);_exports.notifyUploadChanged=elementId=>(0,_event_dispatcher.dispatchEvent)(eventTypes.uploadChanged,{},document.getElementById(elementId),{bubbles:!0,cancellable:!1});_exports.notifyFieldStructureChanged=elementId=>(0,_event_dispatcher.dispatchEvent)(eventTypes.fieldStructureChanged,{},document.getElementById(elementId),{bubbles:!0,cancellable:!1})})); + */ +let changesMadeString;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.types=_exports.triggerUploadStarted=_exports.triggerUploadCompleted=_exports.notifyUploadStarted=_exports.notifyUploadCompleted=_exports.notifyUploadChanged=_exports.notifyFormSubmittedByJavascript=_exports.notifyFormError=_exports.notifyFieldValidationFailure=_exports.notifyFieldStructureChanged=_exports.eventTypes=void 0;const changesMadeCheck=e=>{e&&(e.returnValue=changesMadeString)},eventTypes={formError:"core_form/error",formSubmittedByJavascript:"core_form/submittedByJavascript",formFieldValidationFailed:"core_form/fieldValidationFailed",uploadStarted:"core_form/uploadStarted",uploadCompleted:"core_form/uploadCompleted",uploadChanged:"core_form/uploadChanged",fieldStructureChanged:"core_form/fieldStructureChanged"};_exports.eventTypes=eventTypes;_exports.notifyFormError=field=>(0,_event_dispatcher.dispatchEvent)(eventTypes.formError,{},field);_exports.notifyFormSubmittedByJavascript=function(form){let skipValidation=arguments.length>1&&void 0!==arguments[1]&&arguments[1],fallbackHandled=arguments.length>2&&void 0!==arguments[2]&&arguments[2];skipValidation&&(window.skipClientValidation=!0);const customEvent=(0,_event_dispatcher.dispatchEvent)(eventTypes.formSubmittedByJavascript,{skipValidation:skipValidation,fallbackHandled:fallbackHandled},form);return skipValidation&&(window.skipClientValidation=!1),customEvent};_exports.notifyFieldValidationFailure=(field,message)=>(0,_event_dispatcher.dispatchEvent)(eventTypes.formFieldValidationFailed,{message:message},field,{cancelable:!0});const notifyUploadStarted=async elementId=>(changesMadeString=await(0,_str.getString)("changesmadereallygoaway","moodle"),window.addEventListener("beforeunload",changesMadeCheck),(0,_event_dispatcher.dispatchEvent)(eventTypes.uploadStarted,{},document.getElementById(elementId),{bubbles:!0,cancellable:!1}));_exports.notifyUploadStarted=notifyUploadStarted;const notifyUploadCompleted=elementId=>(window.removeEventListener("beforeunload",changesMadeCheck),(0,_event_dispatcher.dispatchEvent)(eventTypes.uploadCompleted,{},document.getElementById(elementId),{bubbles:!0,cancellable:!1}));_exports.notifyUploadCompleted=notifyUploadCompleted;const triggerUploadStarted=notifyUploadStarted;_exports.triggerUploadStarted=triggerUploadStarted;const triggerUploadCompleted=notifyUploadCompleted;_exports.triggerUploadCompleted=triggerUploadCompleted;_exports.types={uploadStarted:"core_form/uploadStarted",uploadCompleted:"core_form/uploadCompleted"};_exports.notifyUploadChanged=elementId=>(0,_event_dispatcher.dispatchEvent)(eventTypes.uploadChanged,{},document.getElementById(elementId),{bubbles:!0,cancellable:!1});_exports.notifyFieldStructureChanged=elementId=>(0,_event_dispatcher.dispatchEvent)(eventTypes.fieldStructureChanged,{},document.getElementById(elementId),{bubbles:!0,cancellable:!1})})); //# sourceMappingURL=events.min.js.map \ No newline at end of file diff --git a/lib/form/amd/build/events.min.js.map b/lib/form/amd/build/events.min.js.map index 5757b431f7d2f..b0210175afaec 100644 --- a/lib/form/amd/build/events.min.js.map +++ b/lib/form/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//\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_form` subsystem.\n *\n * @module core_form/events\n * @copyright 2021 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.10\n *\n * @example Example of listening to a form event.\n * import {eventTypes as formEventTypes} from 'core_form/events';\n *\n * document.addEventListener(formEventTypes.formSubmittedByJavascript, e => {\n * window.console.log(e.target); // The form that was submitted.\n * window.console.log(e.detail.skipValidation); // Whether form validation was skipped.\n * });\n */\n\nimport {getString} from 'core/str';\nimport {dispatchEvent} from 'core/event_dispatcher';\n\nlet changesMadeString;\n\n/**\n * Prevent user navigate away when upload progress still running.\n * @param {Event} e The event\n */\nconst changesMadeCheck = e => {\n if (e) {\n e.returnValue = changesMadeString;\n }\n};\n\n/**\n * Events for `core_form`.\n *\n * @constant\n * @property {String} formError See {@link event:core_form/error}\n * @property {String} formFieldValidationFailed See {@link event:core_form/fieldValidationFailed}\n * @property {String} formSubmittedByJavascript See {@link event:core_form/submittedByJavascript}\n * @property {String} uploadChanged See {@link event:core_form/uploadChanged}\n * @property {String} fieldStructureChanged See {@link event:core_form/fieldStructureChanged}\n */\nexport const eventTypes = {\n /**\n * An event triggered when a form contains an error\n *\n * @event formError\n * @type {CustomEvent}\n * @property {HTMLElement} target The form field which errored\n */\n formError: 'core_form/error',\n\n /**\n * An event triggered when an mform is about to be submitted via javascript.\n *\n * @event core_form/submittedByJavascript\n * @type {CustomEvent}\n * @property {HTMLElement} target The form that was submitted\n * @property {object} detail\n * @property {boolean} detail.skipValidation Whether the form was submitted without validation (i.e. via a Cancel button)\n * @property {boolean} detail.fallbackHandled Whether the legacy YUI event has been handled\n */\n formSubmittedByJavascript: 'core_form/submittedByJavascript',\n\n /**\n * An event triggered upon form field validation failure.\n *\n * @event core_form/fieldValidationFailed\n * @type {CustomEvent}\n * @property {HTMLElement} target The field that failed validation\n * @property {object} detail\n * @property {String} detail.message The message displayed upon failure\n */\n formFieldValidationFailed: 'core_form/fieldValidationFailed',\n\n /**\n * An event triggered when an upload is started\n *\n * @event core_form/uploadStarted\n * @type {CustomEvent}\n * @property {HTMLElement} target The location where the upload began\n */\n uploadStarted: 'core_form/uploadStarted',\n\n /**\n * An event triggered when an upload completes\n *\n * @event core_form/uploadCompleted\n * @type {CustomEvent}\n * @property {HTMLElement} target The location where the upload completed\n */\n uploadCompleted: 'core_form/uploadCompleted',\n\n /**\n * An event triggered when a file upload field has been changed.\n *\n * @event core_form/uploadChanged\n * @type {CustomEvent}\n * @property {HTMLElement} target The form field which was changed\n */\n uploadChanged: 'core_form/uploadChanged',\n\n /**\n * An event triggered when a form field structure has changed.\n *\n * @event core_form/fieldStructureChanged\n * @type {CustomEvent}\n * @property {HTMLElement} target The form field that has changed\n */\n fieldStructureChanged: 'core_form/fieldStructureChanged',\n};\n\n// These are only imported for legacy.\nimport jQuery from 'jquery';\nimport Y from 'core/yui';\n\n/**\n * Trigger an event to indicate that a form field contained an error.\n *\n * @method notifyFormError\n * @param {HTMLElement} field The form field causing the error\n * @returns {CustomEvent}\n * @fires formError\n */\nexport const notifyFormError = field => dispatchEvent(eventTypes.formError, {}, field);\n\n/**\n * Trigger an event to indiciate that a form was submitted by Javascript.\n *\n * @method\n * @param {HTMLElement} form The form that was submitted\n * @param {Boolean} skipValidation Submit the form without validation. E.g. \"Cancel\".\n * @param {Boolean} fallbackHandled The legacy YUI event has been handled\n * @returns {CustomEvent}\n * @fires formSubmittedByJavascript\n */\nexport const notifyFormSubmittedByJavascript = (form, skipValidation = false, fallbackHandled = false) => {\n if (skipValidation) {\n window.skipClientValidation = true;\n }\n\n const customEvent = dispatchEvent(\n eventTypes.formSubmittedByJavascript,\n {\n skipValidation,\n fallbackHandled,\n },\n form\n );\n\n if (skipValidation) {\n window.skipClientValidation = false;\n }\n\n return customEvent;\n};\n\n/**\n * Trigger an event to indicate that a form field contained an error.\n *\n * @method notifyFieldValidationFailure\n * @param {HTMLElement} field The field which failed validation\n * @param {String} message The message displayed\n * @returns {CustomEvent}\n * @fires formFieldValidationFailed\n */\nexport const notifyFieldValidationFailure = (field, message) => dispatchEvent(\n eventTypes.formFieldValidationFailed,\n {\n message,\n },\n field,\n {\n cancelable: true\n }\n);\n\n/**\n * Trigger an event to indicate that an upload was started.\n *\n * @method\n * @param {String} elementId The element which was uploaded to\n * @returns {CustomEvent}\n * @fires uploadStarted\n */\nexport const notifyUploadStarted = async elementId => {\n // Add an additional check for changes made.\n changesMadeString = await getString('changesmadereallygoaway', 'moodle');\n window.addEventListener('beforeunload', changesMadeCheck);\n\n return dispatchEvent(\n eventTypes.uploadStarted,\n {},\n document.getElementById(elementId),\n {\n bubbles: true,\n cancellable: false,\n }\n );\n};\n\n/**\n * Trigger an event to indicate that an upload was completed.\n *\n * @method\n * @param {String} elementId The element which was uploaded to\n * @returns {CustomEvent}\n * @fires uploadCompleted\n */\nexport const notifyUploadCompleted = elementId => {\n // Remove the additional check for changes made.\n window.removeEventListener('beforeunload', changesMadeCheck);\n\n return dispatchEvent(\n eventTypes.uploadCompleted,\n {},\n document.getElementById(elementId),\n {\n bubbles: true,\n cancellable: false,\n }\n );\n};\n\n/**\n * Trigger upload start event.\n *\n * @method\n * @param {String} elementId\n * @returns {CustomEvent}\n * @fires uploadStarted\n * @deprecated Since Moodle 4.0 See {@link module:core_form/events.notifyUploadStarted notifyUploadStarted}\n */\nexport const triggerUploadStarted = notifyUploadStarted;\n\n/**\n * Trigger upload complete event.\n *\n * @method\n * @param {String} elementId\n * @returns {CustomEvent}\n * @fires uploadCompleted\n * @deprecated Since Moodle 4.0 See {@link module:core_form/events.notifyUploadCompleted notifyUploadCompleted}\n */\nexport const triggerUploadCompleted = notifyUploadCompleted;\n\n/**\n * List of the events.\n *\n * @deprecated since Moodle 4.0. See {@link module:core_form/events.eventTypes eventTypes} instead.\n **/\nexport const types = {\n uploadStarted: 'core_form/uploadStarted',\n uploadCompleted: 'core_form/uploadCompleted',\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 Y.use('event', 'moodle-core-event', () => {\n\n // Watch for the new native formError event, and trigger the legacy YUI event.\n document.addEventListener(eventTypes.formError, e => {\n const element = Y.one(e.target);\n const formElement = Y.one(e.target.closest('form'));\n\n Y.Global.fire(\n M.core.globalEvents.FORM_ERROR,\n {\n formid: formElement.generateID(),\n elementid: element.generateID(),\n }\n );\n });\n\n // Watch for the new native formSubmittedByJavascript event, and trigger the legacy YUI event.\n document.addEventListener(eventTypes.formSubmittedByJavascript, e => {\n if (e.detail.fallbackHandled) {\n // This event was originally generated by a YUI event.\n // Do not generate another as this will recurse.\n return;\n }\n\n if (e.skipValidation) {\n window.skipClientValidation = true;\n }\n\n // Trigger the legacy YUI event.\n const form = Y.one(e.target);\n form.fire(\n M.core.event.FORM_SUBMIT_AJAX,\n {\n currentTarget: form,\n fallbackHandled: true,\n }\n );\n\n if (e.skipValidation) {\n window.skipClientValidation = false;\n }\n });\n });\n\n // Watch for the new native formFieldValidationFailed event, and trigger the legacy jQuery event.\n document.addEventListener(eventTypes.formFieldValidationFailed, e => {\n // Note: The \"core_form-field-validation\" event is hard-coded in core/event.\n // This is not included to prevent cyclic module dependencies.\n const legacyEvent = jQuery.Event(\"core_form-field-validation\");\n\n jQuery(e.target).trigger(legacyEvent, e.detail.message);\n });\n\n legacyEventsRegistered = true;\n}\n\n/**\n * Trigger an event to notify the file upload field has been changed.\n *\n * @method\n * @param {string} elementId The element which was changed\n * @returns {CustomEvent}\n * @fires uploadChanged\n */\nexport const notifyUploadChanged = elementId => dispatchEvent(\n eventTypes.uploadChanged,\n {},\n document.getElementById(elementId),\n {\n bubbles: true,\n cancellable: false,\n }\n);\n\n/**\n * Trigger an event to notify the field structure has changed.\n *\n * @method\n * @param {string} elementId The element which was changed\n * @returns {CustomEvent}\n * @fires fieldStructureChanged\n */\nexport const notifyFieldStructureChanged = elementId => dispatchEvent(\n eventTypes.fieldStructureChanged,\n {},\n document.getElementById(elementId),\n {\n bubbles: true,\n cancellable: false,\n }\n);\n"],"names":["changesMadeString","changesMadeCheck","e","returnValue","eventTypes","formError","formSubmittedByJavascript","formFieldValidationFailed","uploadStarted","uploadCompleted","uploadChanged","fieldStructureChanged","field","form","skipValidation","fallbackHandled","window","skipClientValidation","customEvent","message","cancelable","notifyUploadStarted","async","addEventListener","document","getElementById","elementId","bubbles","cancellable","notifyUploadCompleted","removeEventListener","triggerUploadStarted","triggerUploadCompleted","legacyEventsRegistered","use","element","Y","one","target","formElement","closest","Global","fire","M","core","globalEvents","FORM_ERROR","formid","generateID","elementid","detail","event","FORM_SUBMIT_AJAX","currentTarget","legacyEvent","jQuery","Event","trigger"],"mappings":";;;;;;;;;;;;;;;;SAmCIA,weAMEC,iBAAmBC,IACjBA,IACAA,EAAEC,YAAcH,oBAcXI,WAAa,CAQtBC,UAAW,kBAYXC,0BAA2B,kCAW3BC,0BAA2B,kCAS3BC,cAAe,0BASfC,gBAAiB,4BASjBC,cAAe,0BASfC,sBAAuB,2FAeIC,QAAS,mCAAcR,WAAWC,UAAW,GAAIO,gDAYjC,SAACC,UAAMC,uEAAwBC,wEACtED,iBACAE,OAAOC,sBAAuB,SAG5BC,aAAc,mCAChBd,WAAWE,0BACX,CACIQ,eAAAA,eACAC,gBAAAA,iBAEJF,aAGAC,iBACAE,OAAOC,sBAAuB,GAG3BC,mDAYiC,CAACN,MAAOO,WAAY,mCAC5Df,WAAWG,0BACX,CACIY,QAAAA,SAEJP,MACA,CACIQ,YAAY,UAYPC,oBAAsBC,MAAAA,YAE/BtB,wBAA0B,kBAAU,0BAA2B,UAC/DgB,OAAOO,iBAAiB,eAAgBtB,mBAEjC,mCACHG,WAAWI,cACX,GACAgB,SAASC,eAAeC,WACxB,CACIC,SAAS,EACTC,aAAa,4DAaZC,sBAAwBH,YAEjCV,OAAOc,oBAAoB,eAAgB7B,mBAEpC,mCACHG,WAAWK,gBACX,GACAe,SAASC,eAAeC,WACxB,CACIC,SAAS,EACTC,aAAa,gEAcZG,qBAAuBV,6EAWvBW,uBAAyBH,4FAOjB,CACjBrB,cAAe,0BACfC,gBAAiB,iCAGjBwB,wBAAyB,EACxBA,sCAICC,IAAI,QAAS,qBAAqB,KAGhCV,SAASD,iBAAiBnB,WAAWC,WAAWH,UACtCiC,QAAUC,aAAEC,IAAInC,EAAEoC,QAClBC,YAAcH,aAAEC,IAAInC,EAAEoC,OAAOE,QAAQ,sBAEzCC,OAAOC,KACLC,EAAEC,KAAKC,aAAaC,WACpB,CACIC,OAAQR,YAAYS,aACpBC,UAAWd,QAAQa,kBAM/BxB,SAASD,iBAAiBnB,WAAWE,2BAA2BJ,OACxDA,EAAEgD,OAAOnC,uBAMTb,EAAEY,iBACFE,OAAOC,sBAAuB,SAI5BJ,KAAOuB,aAAEC,IAAInC,EAAEoC,QACrBzB,KAAK6B,KACDC,EAAEC,KAAKO,MAAMC,iBACb,CACIC,cAAexC,KACfE,iBAAiB,IAIrBb,EAAEY,iBACFE,OAAOC,sBAAuB,SAM1CO,SAASD,iBAAiBnB,WAAWG,2BAA2BL,UAGtDoD,YAAcC,gBAAOC,MAAM,kDAE1BtD,EAAEoC,QAAQmB,QAAQH,YAAapD,EAAEgD,OAAO/B,YAGnDc,wBAAyB,gCAWMP,YAAa,mCAC5CtB,WAAWM,cACX,GACAc,SAASC,eAAeC,WACxB,CACIC,SAAS,EACTC,aAAa,yCAYsBF,YAAa,mCACpDtB,WAAWO,sBACX,GACAa,SAASC,eAAeC,WACxB,CACIC,SAAS,EACTC,aAAa"} \ 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//\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_form` subsystem.\n *\n * @module core_form/events\n * @copyright 2021 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.10\n *\n * @example Example of listening to a form event.\n * import {eventTypes as formEventTypes} from 'core_form/events';\n *\n * document.addEventListener(formEventTypes.formSubmittedByJavascript, e => {\n * window.console.log(e.target); // The form that was submitted.\n * window.console.log(e.detail.skipValidation); // Whether form validation was skipped.\n * });\n */\n\nimport {getString} from 'core/str';\nimport {dispatchEvent} from 'core/event_dispatcher';\n\nlet changesMadeString;\n\n/**\n * Prevent user navigate away when upload progress still running.\n * @param {Event} e The event\n */\nconst changesMadeCheck = e => {\n if (e) {\n e.returnValue = changesMadeString;\n }\n};\n\n/**\n * Events for `core_form`.\n *\n * @constant\n * @property {String} formError See {@link event:core_form/error}\n * @property {String} formFieldValidationFailed See {@link event:core_form/fieldValidationFailed}\n * @property {String} formSubmittedByJavascript See {@link event:core_form/submittedByJavascript}\n * @property {String} uploadChanged See {@link event:core_form/uploadChanged}\n * @property {String} fieldStructureChanged See {@link event:core_form/fieldStructureChanged}\n */\nexport const eventTypes = {\n /**\n * An event triggered when a form contains an error\n *\n * @event formError\n * @type {CustomEvent}\n * @property {HTMLElement} target The form field which errored\n */\n formError: 'core_form/error',\n\n /**\n * An event triggered when an mform is about to be submitted via javascript.\n *\n * @event core_form/submittedByJavascript\n * @type {CustomEvent}\n * @property {HTMLElement} target The form that was submitted\n * @property {object} detail\n * @property {boolean} detail.skipValidation Whether the form was submitted without validation (i.e. via a Cancel button)\n * @property {boolean} detail.fallbackHandled Whether the legacy YUI event has been handled\n */\n formSubmittedByJavascript: 'core_form/submittedByJavascript',\n\n /**\n * An event triggered upon form field validation failure.\n *\n * @event core_form/fieldValidationFailed\n * @type {CustomEvent}\n * @property {HTMLElement} target The field that failed validation\n * @property {object} detail\n * @property {String} detail.message The message displayed upon failure\n */\n formFieldValidationFailed: 'core_form/fieldValidationFailed',\n\n /**\n * An event triggered when an upload is started\n *\n * @event core_form/uploadStarted\n * @type {CustomEvent}\n * @property {HTMLElement} target The location where the upload began\n */\n uploadStarted: 'core_form/uploadStarted',\n\n /**\n * An event triggered when an upload completes\n *\n * @event core_form/uploadCompleted\n * @type {CustomEvent}\n * @property {HTMLElement} target The location where the upload completed\n */\n uploadCompleted: 'core_form/uploadCompleted',\n\n /**\n * An event triggered when a file upload field has been changed.\n *\n * @event core_form/uploadChanged\n * @type {CustomEvent}\n * @property {HTMLElement} target The form field which was changed\n */\n uploadChanged: 'core_form/uploadChanged',\n\n /**\n * An event triggered when a form field structure has changed.\n *\n * @event core_form/fieldStructureChanged\n * @type {CustomEvent}\n * @property {HTMLElement} target The form field that has changed\n */\n fieldStructureChanged: 'core_form/fieldStructureChanged',\n};\n\n/**\n * Trigger an event to indicate that a form field contained an error.\n *\n * @method notifyFormError\n * @param {HTMLElement} field The form field causing the error\n * @returns {CustomEvent}\n * @fires formError\n */\nexport const notifyFormError = field => dispatchEvent(eventTypes.formError, {}, field);\n\n/**\n * Trigger an event to indiciate that a form was submitted by Javascript.\n *\n * @method\n * @param {HTMLElement} form The form that was submitted\n * @param {Boolean} skipValidation Submit the form without validation. E.g. \"Cancel\".\n * @param {Boolean} fallbackHandled The legacy YUI event has been handled\n * @returns {CustomEvent}\n * @fires formSubmittedByJavascript\n */\nexport const notifyFormSubmittedByJavascript = (form, skipValidation = false, fallbackHandled = false) => {\n if (skipValidation) {\n window.skipClientValidation = true;\n }\n\n const customEvent = dispatchEvent(\n eventTypes.formSubmittedByJavascript,\n {\n skipValidation,\n fallbackHandled,\n },\n form\n );\n\n if (skipValidation) {\n window.skipClientValidation = false;\n }\n\n return customEvent;\n};\n\n/**\n * Trigger an event to indicate that a form field contained an error.\n *\n * @method notifyFieldValidationFailure\n * @param {HTMLElement} field The field which failed validation\n * @param {String} message The message displayed\n * @returns {CustomEvent}\n * @fires formFieldValidationFailed\n */\nexport const notifyFieldValidationFailure = (field, message) => dispatchEvent(\n eventTypes.formFieldValidationFailed,\n {\n message,\n },\n field,\n {\n cancelable: true\n }\n);\n\n/**\n * Trigger an event to indicate that an upload was started.\n *\n * @method\n * @param {String} elementId The element which was uploaded to\n * @returns {CustomEvent}\n * @fires uploadStarted\n */\nexport const notifyUploadStarted = async elementId => {\n // Add an additional check for changes made.\n changesMadeString = await getString('changesmadereallygoaway', 'moodle');\n window.addEventListener('beforeunload', changesMadeCheck);\n\n return dispatchEvent(\n eventTypes.uploadStarted,\n {},\n document.getElementById(elementId),\n {\n bubbles: true,\n cancellable: false,\n }\n );\n};\n\n/**\n * Trigger an event to indicate that an upload was completed.\n *\n * @method\n * @param {String} elementId The element which was uploaded to\n * @returns {CustomEvent}\n * @fires uploadCompleted\n */\nexport const notifyUploadCompleted = elementId => {\n // Remove the additional check for changes made.\n window.removeEventListener('beforeunload', changesMadeCheck);\n\n return dispatchEvent(\n eventTypes.uploadCompleted,\n {},\n document.getElementById(elementId),\n {\n bubbles: true,\n cancellable: false,\n }\n );\n};\n\n/**\n * Trigger upload start event.\n *\n * @method\n * @param {String} elementId\n * @returns {CustomEvent}\n * @fires uploadStarted\n * @deprecated Since Moodle 4.0 See {@link module:core_form/events.notifyUploadStarted notifyUploadStarted}\n */\nexport const triggerUploadStarted = notifyUploadStarted;\n\n/**\n * Trigger upload complete event.\n *\n * @method\n * @param {String} elementId\n * @returns {CustomEvent}\n * @fires uploadCompleted\n * @deprecated Since Moodle 4.0 See {@link module:core_form/events.notifyUploadCompleted notifyUploadCompleted}\n */\nexport const triggerUploadCompleted = notifyUploadCompleted;\n\n/**\n * List of the events.\n *\n * @deprecated since Moodle 4.0. See {@link module:core_form/events.eventTypes eventTypes} instead.\n **/\nexport const types = {\n uploadStarted: 'core_form/uploadStarted',\n uploadCompleted: 'core_form/uploadCompleted',\n};\n\n/**\n * Trigger an event to notify the file upload field has been changed.\n *\n * @method\n * @param {string} elementId The element which was changed\n * @returns {CustomEvent}\n * @fires uploadChanged\n */\nexport const notifyUploadChanged = elementId => dispatchEvent(\n eventTypes.uploadChanged,\n {},\n document.getElementById(elementId),\n {\n bubbles: true,\n cancellable: false,\n }\n);\n\n/**\n * Trigger an event to notify the field structure has changed.\n *\n * @method\n * @param {string} elementId The element which was changed\n * @returns {CustomEvent}\n * @fires fieldStructureChanged\n */\nexport const notifyFieldStructureChanged = elementId => dispatchEvent(\n eventTypes.fieldStructureChanged,\n {},\n document.getElementById(elementId),\n {\n bubbles: true,\n cancellable: false,\n }\n);\n"],"names":["changesMadeString","changesMadeCheck","e","returnValue","eventTypes","formError","formSubmittedByJavascript","formFieldValidationFailed","uploadStarted","uploadCompleted","uploadChanged","fieldStructureChanged","field","form","skipValidation","fallbackHandled","window","skipClientValidation","customEvent","message","cancelable","notifyUploadStarted","async","addEventListener","document","getElementById","elementId","bubbles","cancellable","notifyUploadCompleted","removeEventListener","triggerUploadStarted","triggerUploadCompleted"],"mappings":";;;;;;;;;;;;;;;;;IAmCIA,8ZAMEC,iBAAmBC,IACjBA,IACAA,EAAEC,YAAcH,oBAcXI,WAAa,CAQtBC,UAAW,kBAYXC,0BAA2B,kCAW3BC,0BAA2B,kCAS3BC,cAAe,0BASfC,gBAAiB,4BASjBC,cAAe,0BASfC,sBAAuB,2FAWIC,QAAS,mCAAcR,WAAWC,UAAW,GAAIO,gDAYjC,SAACC,UAAMC,uEAAwBC,wEACtED,iBACAE,OAAOC,sBAAuB,SAG5BC,aAAc,mCAChBd,WAAWE,0BACX,CACIQ,eAAAA,eACAC,gBAAAA,iBAEJF,aAGAC,iBACAE,OAAOC,sBAAuB,GAG3BC,mDAYiC,CAACN,MAAOO,WAAY,mCAC5Df,WAAWG,0BACX,CACIY,QAAAA,SAEJP,MACA,CACIQ,YAAY,UAYPC,oBAAsBC,MAAAA,YAE/BtB,wBAA0B,kBAAU,0BAA2B,UAC/DgB,OAAOO,iBAAiB,eAAgBtB,mBAEjC,mCACHG,WAAWI,cACX,GACAgB,SAASC,eAAeC,WACxB,CACIC,SAAS,EACTC,aAAa,4DAaZC,sBAAwBH,YAEjCV,OAAOc,oBAAoB,eAAgB7B,mBAEpC,mCACHG,WAAWK,gBACX,GACAe,SAASC,eAAeC,WACxB,CACIC,SAAS,EACTC,aAAa,gEAcZG,qBAAuBV,6EAWvBW,uBAAyBH,4FAOjB,CACjBrB,cAAe,0BACfC,gBAAiB,0DAWciB,YAAa,mCAC5CtB,WAAWM,cACX,GACAc,SAASC,eAAeC,WACxB,CACIC,SAAS,EACTC,aAAa,yCAYsBF,YAAa,mCACpDtB,WAAWO,sBACX,GACAa,SAASC,eAAeC,WACxB,CACIC,SAAS,EACTC,aAAa"} \ No newline at end of file diff --git a/lib/form/amd/src/events.js b/lib/form/amd/src/events.js index b5c3b4df82dac..599a976fc659c 100644 --- a/lib/form/amd/src/events.js +++ b/lib/form/amd/src/events.js @@ -125,10 +125,6 @@ export const eventTypes = { fieldStructureChanged: 'core_form/fieldStructureChanged', }; -// These are only imported for legacy. -import jQuery from 'jquery'; -import Y from 'core/yui'; - /** * Trigger an event to indicate that a form field contained an error. * @@ -269,67 +265,6 @@ export const types = { uploadCompleted: 'core_form/uploadCompleted', }; -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', () => { - - // Watch for the new native formError event, and trigger the legacy YUI event. - document.addEventListener(eventTypes.formError, e => { - const element = Y.one(e.target); - const formElement = Y.one(e.target.closest('form')); - - Y.Global.fire( - M.core.globalEvents.FORM_ERROR, - { - formid: formElement.generateID(), - elementid: element.generateID(), - } - ); - }); - - // Watch for the new native formSubmittedByJavascript event, and trigger the legacy YUI event. - document.addEventListener(eventTypes.formSubmittedByJavascript, e => { - if (e.detail.fallbackHandled) { - // This event was originally generated by a YUI event. - // Do not generate another as this will recurse. - return; - } - - if (e.skipValidation) { - window.skipClientValidation = true; - } - - // Trigger the legacy YUI event. - const form = Y.one(e.target); - form.fire( - M.core.event.FORM_SUBMIT_AJAX, - { - currentTarget: form, - fallbackHandled: true, - } - ); - - if (e.skipValidation) { - window.skipClientValidation = false; - } - }); - }); - - // Watch for the new native formFieldValidationFailed event, and trigger the legacy jQuery event. - document.addEventListener(eventTypes.formFieldValidationFailed, e => { - // Note: The "core_form-field-validation" event is hard-coded in core/event. - // This is not included to prevent cyclic module dependencies. - const legacyEvent = jQuery.Event("core_form-field-validation"); - - jQuery(e.target).trigger(legacyEvent, e.detail.message); - }); - - legacyEventsRegistered = true; -} - /** * Trigger an event to notify the file upload field has been changed. * From 828e9bd87e5ce04ebe656f78c4237b24d909b7d4 Mon Sep 17 00:00:00 2001 From: AMOS bot Date: Sun, 21 Jul 2024 00:07:45 +0000 Subject: [PATCH 152/178] Automatically generated installer lang files --- install/lang/ia/langconfig.php | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 install/lang/ia/langconfig.php diff --git a/install/lang/ia/langconfig.php b/install/lang/ia/langconfig.php new file mode 100644 index 0000000000000..7128e27ab1e0c --- /dev/null +++ b/install/lang/ia/langconfig.php @@ -0,0 +1,32 @@ +. + +/** + * Automatically generated strings for Moodle installer + * + * Do not edit this file manually! It contains just a subset of strings + * needed during the very first steps of installation. This file was + * generated automatically by export-installer.php (which is part of AMOS + * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the + * list of strings defined in /install/stringnames.txt. + * + * @package installer + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$string['thislanguage'] = 'Interlingua'; From 4fe100f16111a22778503c864cd6f0723c95878c Mon Sep 17 00:00:00 2001 From: Laurent David Date: Tue, 18 Jun 2024 12:40:42 +0200 Subject: [PATCH 153/178] MDL-73232 core_courseformat: Inform user when reaching section max * Disable the Add new Section button when max has been reached * It also grays out the (+) button between sections and display a tooltip * When adding a new section via the (+) button between section should add a section with the (+) (add) button disabled. --- .../amd/build/local/content/actions.min.js | 4 +-- .../build/local/content/actions.min.js.map | 2 +- .../format/amd/src/local/content/actions.js | 27 +++++++++++++++-- .../output/local/content/addsection.php | 2 +- .../local/content/addsection.mustache | 30 +++++++++++-------- .../section/addsectiondivider.mustache | 8 ++--- .../tests/behat/course_courseindex.feature | 14 +++++++++ lang/en/courseformat.php | 2 ++ theme/boost/scss/moodle/course.scss | 27 +++++++++++++++++ theme/boost/style/moodle.css | 26 ++++++++++++++++ theme/classic/style/moodle.css | 26 ++++++++++++++++ 11 files changed, 145 insertions(+), 23 deletions(-) diff --git a/course/format/amd/build/local/content/actions.min.js b/course/format/amd/build/local/content/actions.min.js index 6554b05d56ac7..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:"disabled",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=>{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.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 1a282c3fbfc88..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: `disabled`,\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 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.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","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","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/src/local/content/actions.js b/course/format/amd/src/local/content/actions.js index 39f082247bc7d..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: `disabled`, ITALIC: `font-italic`, + DISPLAYNONE: `d-none`, }; } @@ -719,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/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/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/section/addsectiondivider.mustache b/course/format/templates/local/content/section/addsectiondivider.mustache index 4aae94e060468..de2c886c69236 100644 --- a/course/format/templates/local/content/section/addsectiondivider.mustache +++ b/course/format/templates/local/content/section/addsectiondivider.mustache @@ -33,21 +33,21 @@ } }} {{#showaddsection}} -
+
{{#addsections}} {{< core_courseformat/local/content/divider}} {{$extraclasses}}always-hidden mt-2{{/extraclasses}} {{$content}} {{#pix}} t/add, core {{/pix}} - {{title}} {{/content}} {{/ core_courseformat/local/content/divider}} diff --git a/course/format/tests/behat/course_courseindex.feature b/course/format/tests/behat/course_courseindex.feature index 2106f571fc4a0..93333bfb262b1 100644 --- a/course/format/tests/behat/course_courseindex.feature +++ b/course/format/tests/behat/course_courseindex.feature @@ -381,3 +381,17 @@ Feature: Course index depending on role And I turn editing mode on When I set the field "Edit section name" in the "page-header" "region" to "Custom section name" Then I should see "Custom section name" in the "courseindex-content" "region" + + @javascript + Scenario: We cannot add a section when the number of section reaches maxsections but as soon as we reach under the limit we can add a section again. + Given the following config values are set as admin: + | maxsections | 4 | moodlecourse| + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + Then I should see "Section 1" in the "courseindex-content" "region" + And ".disabled" "css_element" should exist in the "[data-action='addSection']" "css_element" + And I should see "You have reached the maximum number of sections allowed for a course." + And I delete section "4" + And I click on "Delete" "button" in the ".modal" "css_element" + And ".disabled" "css_element" should not exist in the "[data-action='addSection']" "css_element" + And I should not see "You have reached the maximum number of sections allowed for a course." diff --git a/lang/en/courseformat.php b/lang/en/courseformat.php index b6135aaeb0652..d8222cdc2d5fa 100644 --- a/lang/en/courseformat.php +++ b/lang/en/courseformat.php @@ -63,6 +63,7 @@ $string['cmsmove_info'] = 'Move {$a} activities after'; $string['courseindex'] = 'Course index'; $string['courseindexoptions'] = 'Course index options'; +$string['maxsectionaddmessage'] = 'You have reached the maximum number of sections allowed for a course.'; $string['nobulkaction'] = 'No bulk actions available'; $string['preference:coursesectionspreferences'] = 'Section user preferences for course {$a}'; $string['privacy:metadata:preference:coursesectionspreferences'] = 'Section user preferences like collapsed and expanded.'; @@ -72,6 +73,7 @@ $string['section_show_feedback_batch'] = 'Selected {$a->count} course sections shown.'; $string['section_delete_feedback'] = 'Course section {$a->name} deleted.'; $string['section_delete_feedback_batch'] = 'Selected {$a->count} course sections deleted.'; +$string['sectionaddmax'] = 'You have reached the maximum number of sections allowed for a course...'; $string['sectionavailability_title'] = 'Section availability'; $string['sectiondelete_info'] = 'This will delete {$a->name} and all the activities it contains.'; $string['sectiondelete_title'] = 'Delete section?'; diff --git a/theme/boost/scss/moodle/course.scss b/theme/boost/scss/moodle/course.scss index e2d24dfe099e4..f36eb2b0a5f34 100644 --- a/theme/boost/scss/moodle/course.scss +++ b/theme/boost/scss/moodle/course.scss @@ -1256,6 +1256,13 @@ $divider-hover-color: $primary !default; } } +.max-section-alert { + border-top: $divider-width dashed $border-color; + font-size: $font-size-sm; + font-weight: normal; + color: $gray-500; +} + /* Single section page specific styles */ .single-section { @@ -1549,6 +1556,9 @@ $divider-hover-color: $primary !default; width: 100%; margin: map-get($spacers, 2) map-get($spacers, 1); border-top: $divider-width dashed $divider-color; + .changenumsections.disabled & { + border-top: $divider-width dashed $divider-color; + } } .divider-content { opacity: 0; @@ -1588,6 +1598,9 @@ $divider-hover-color: $primary !default; &:has(.btn.add-content:hover) { hr { border-color: $divider-hover-color; + .changenumsections.disabled & { + border-color: $gray-200; + } } } } @@ -1625,6 +1638,20 @@ $divider-hover-color: $primary !default; height: 14px; font-size: 14px; } + .changenumsections.disabled & { + color: $gray-500; + background-color: $gray-200; + outline: none; + box-shadow: none; + &:hover, + &:focus { + color: $gray-500; + background-color: $gray-200; + outline: none; + box-shadow: none; + } + pointer-events: auto; // Restore pointer events for the disabled button so we can see the tooltip. + } } /* Bulk editing */ diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index bba582f17a008..da285b4494772 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -29098,6 +29098,13 @@ span.editinstructions .alert-link { color: #0f6cbf; } +.max-section-alert { + border-top: 2px dashed #dee2e6; + font-size: 0.8203125rem; + font-weight: normal; + color: #8f959e; +} + /* Single section page specific styles */ .single-section > ul > .course-section.hidden .section-item { background-color: inherit; @@ -29757,6 +29764,9 @@ span.editinstructions .alert-link { margin: 0.5rem 0.25rem; border-top: 2px dashed #dee2e6; } +.changenumsections.disabled .divider hr { + border-top: 2px dashed #dee2e6; +} .divider .divider-content { opacity: 0; visibility: hidden; @@ -29786,6 +29796,9 @@ span.editinstructions .alert-link { .divider:has(.btn.add-content:hover) hr { border-color: #0f6cbf; } +.changenumsections.disabled .divider:has(.btn.add-content:hover) hr { + border-color: #e9ecef; +} .activity:focus-within + .activity .divider .divider-content, .course-section-header:focus-within + .content .section .activity:first-child .divider .divider-content, @@ -29816,6 +29829,19 @@ span.editinstructions .alert-link { height: 14px; font-size: 14px; } +.changenumsections.disabled .btn.add-content { + color: #8f959e; + background-color: #e9ecef; + outline: none; + box-shadow: none; + pointer-events: auto; +} +.changenumsections.disabled .btn.add-content:hover, .changenumsections.disabled .btn.add-content:focus { + color: #8f959e; + background-color: #e9ecef; + outline: none; + box-shadow: none; +} /* Bulk editing */ .bulkenabled .bulk-hidden { diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index abf1af20e4afd..f75dae86cdc0d 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -29098,6 +29098,13 @@ span.editinstructions .alert-link { color: #0f6cbf; } +.max-section-alert { + border-top: 2px dashed #dee2e6; + font-size: 0.8203125rem; + font-weight: normal; + color: #8f959e; +} + /* Single section page specific styles */ .single-section > ul > .course-section.hidden .section-item { background-color: inherit; @@ -29757,6 +29764,9 @@ span.editinstructions .alert-link { margin: 0.5rem 0.25rem; border-top: 2px dashed #dee2e6; } +.changenumsections.disabled .divider hr { + border-top: 2px dashed #dee2e6; +} .divider .divider-content { opacity: 0; visibility: hidden; @@ -29786,6 +29796,9 @@ span.editinstructions .alert-link { .divider:has(.btn.add-content:hover) hr { border-color: #0f6cbf; } +.changenumsections.disabled .divider:has(.btn.add-content:hover) hr { + border-color: #e9ecef; +} .activity:focus-within + .activity .divider .divider-content, .course-section-header:focus-within + .content .section .activity:first-child .divider .divider-content, @@ -29816,6 +29829,19 @@ span.editinstructions .alert-link { height: 14px; font-size: 14px; } +.changenumsections.disabled .btn.add-content { + color: #8f959e; + background-color: #e9ecef; + outline: none; + box-shadow: none; + pointer-events: auto; +} +.changenumsections.disabled .btn.add-content:hover, .changenumsections.disabled .btn.add-content:focus { + color: #8f959e; + background-color: #e9ecef; + outline: none; + box-shadow: none; +} /* Bulk editing */ .bulkenabled .bulk-hidden { From dc12e44bd4ffc0c923ffe1540baf9d8b2bc00213 Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Mon, 22 Jul 2024 14:17:47 +0800 Subject: [PATCH 154/178] MDL-82551 core: Set welcome message heading to level 1 --- lib/templates/welcome.mustache | 4 ++-- my/tests/behat/welcome.feature | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/templates/welcome.mustache b/lib/templates/welcome.mustache index 60b02db478689..7f9bfa8553dd6 100644 --- a/lib/templates/welcome.mustache +++ b/lib/templates/welcome.mustache @@ -25,7 +25,7 @@ } }} {{#welcomemessage}} -

+

{{.}} -

+ {{/welcomemessage}} diff --git a/my/tests/behat/welcome.feature b/my/tests/behat/welcome.feature index 2c4a1bf8dd658..7fead25598d77 100644 --- a/my/tests/behat/welcome.feature +++ b/my/tests/behat/welcome.feature @@ -30,3 +30,20 @@ Feature: Welcome message And I should see "You are not logged in" in the "page-footer" "region" And I log in as "admin" Then I should see "Hi, Admin!" in the "page-header" "region" + + @accessibility @javascript + Scenario Outline: The start page must meet accessibility standards when the welcome message is displayed + Given the following config values are set as admin: + | defaulthomepage | | + When I log in as "admin" + Then I should see "Welcome, Admin!" in the "page-header" "region" + And the page should meet accessibility standards + + Examples: + | defaulthomepage | + # Home. + | 0 | + # Dashboard. + | 1 | + # My courses. + | 2 | From 0b9660a43f24c29827815fad98d2af04ea11ef9a Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Mon, 22 Jul 2024 14:36:10 +0800 Subject: [PATCH 155/178] MDL-82553 behat: Run accessibility tests using WCAG 2.2 success criteria --- lib/tests/behat/behat_accessibility.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/tests/behat/behat_accessibility.php b/lib/tests/behat/behat_accessibility.php index dd7f7100149fa..0574e4667ead0 100644 --- a/lib/tests/behat/behat_accessibility.php +++ b/lib/tests/behat/behat_accessibility.php @@ -190,11 +190,11 @@ protected function run_axe_for_tags(array $standardtags = [], array $extratags = protected function get_axe_config_for_tags(?array $standardtags = null, ?array $extratags = null): string { if (empty($standardtags)) { $standardtags = [ - // Meet WCAG 2.1 A requirements. - 'wcag21a', + // Meet WCAG 2.2 Level A success criteria. + 'wcag22a', - // Meet WCAG 2.1 AA requirements. - 'wcag21aa', + // Meet WCAG 2.2 Level AA success criteria. + 'wcag22aa', // Meet Section 508 requirements. // See https://www.epa.gov/accessibility/what-section-508 for detail. From 572d45c885459cc20b2f85e0c0b5b9322255864f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikel=20Mart=C3=ADn?= Date: Mon, 15 Jul 2024 14:23:35 +0200 Subject: [PATCH 156/178] MDL-81766 course: Fix activity name inplace editor selector Fix name inplace editor for course modules by updating the selector to a more specific one. --- course/amd/build/actions.min.js | 2 +- course/amd/build/actions.min.js.map | 2 +- course/amd/src/actions.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/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]); } From d77b20b65f5e1fb76e7f1a4c8995577b1b9493a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikel=20Mart=C3=ADn?= Date: Mon, 15 Jul 2024 14:24:27 +0200 Subject: [PATCH 157/178] MDL-81766 courseformat: Fix last activity dropzone When using drag and drop with activities and subsection combination, the dropzone of the last element was incorrectly calculated. Fixed the 'getLastCm()' function. --- course/format/amd/build/local/content/section.min.js | 2 +- course/format/amd/build/local/content/section.min.js.map | 2 +- course/format/amd/src/local/content/section.js | 9 ++++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/course/format/amd/build/local/content/section.min.js b/course/format/amd/build/local/content/section.min.js index ab69426c5ac14..b8da029f23cb7 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}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(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={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(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})); //# 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 593f3bbd3d204..d46626c187a3c 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 * 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 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","getLastCmFallback","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 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 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","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/src/local/content/section.js b/course/format/amd/src/local/content/section.js index 0a621301c0e9b..b94693d490883 100644 --- a/course/format/amd/src/local/content/section.js +++ b/course/format/amd/src/local/content/section.js @@ -136,7 +136,14 @@ 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; } /** From b18db245525313119570c4ae508815bee5a21018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikel=20Mart=C3=ADn?= Date: Thu, 11 Jul 2024 13:46:13 +0200 Subject: [PATCH 158/178] MDL-81766 courseformat: Fix delegated section headers display Section specific page was not prepared to display another sections in it. Fixed some of its logic to handle other section's headers inside. --- course/format/classes/output/local/content/section.php | 2 +- .../format/classes/output/local/content/section/header.php | 5 ++++- .../templates/local/content/section/content.mustache | 3 ++- .../format/templates/local/content/section/header.mustache | 7 ++++--- 4 files changed, 11 insertions(+), 6 deletions(-) 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/header.php b/course/format/classes/output/local/content/section/header.php index 7c938342936f2..dcb608d6ef088 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); @@ -118,6 +118,9 @@ public function export_for_template(\renderer_base $output): stdClass { $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/templates/local/content/section/content.mustache b/course/format/templates/local/content/section/content.mustache index d4db117e78e2c..7a61ca87e3637 100644 --- a/course/format/templates/local/content/section/content.mustache +++ b/course/format/templates/local/content/section/content.mustache @@ -28,7 +28,8 @@ "name": "Section title", "title": "
Section title", "url": "#", - "ishidden": true + "ishidden": true, + "headinglevel": 3 }, "cmlist": { "cms": [ diff --git a/course/format/templates/local/content/section/header.mustache b/course/format/templates/local/content/section/header.mustache index 8cac7191cf45e..bb13b7f66f8f2 100644 --- a/course/format/templates/local/content/section/header.mustache +++ b/course/format/templates/local/content/section/header.mustache @@ -27,7 +27,8 @@ "url": "#", "headerdisplaymultipage": true, "sectionbulk": true, - "editing": 0 + "editing": 0, + "headinglevel": 3 } }} {{#sectionbulk}} @@ -69,10 +70,10 @@ {{#pix}} t/collapsedchevron_rtl, core {{/pix}} -

{{{title}}} -

+
{{/displayonesection}} {{/sitehome}} From c51989d649649af04c7911cb903472dbf439a7a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikel=20Mart=C3=ADn?= Date: Fri, 12 Jul 2024 08:10:51 +0200 Subject: [PATCH 159/178] MDL-81766 courseformat: Fix bulk actions for delegated sections Disable bulk editing for delegated sections and fix some styles related to it. --- .../format/amd/build/local/content/section/cmitem.min.js | 2 +- .../amd/build/local/content/section/cmitem.min.js.map | 2 +- course/format/amd/src/local/content/section/cmitem.js | 2 +- course/format/classes/output/local/content/cm.php | 1 + .../classes/output/local/content/section/header.php | 2 +- course/format/templates/local/content/cm.mustache | 8 +++++--- theme/boost/scss/moodle/course.scss | 5 +++++ theme/boost/style/moodle.css | 4 ++++ theme/classic/style/moodle.css | 4 ++++ 9 files changed, 23 insertions(+), 7 deletions(-) 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/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/classes/output/local/content/cm.php b/course/format/classes/output/local/content/cm.php index 76173d173b891..fb290348b2474 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. diff --git a/course/format/classes/output/local/content/section/header.php b/course/format/classes/output/local/content/section/header.php index dcb608d6ef088..d325efe9a278f 100644 --- a/course/format/classes/output/local/content/section/header.php +++ b/course/format/classes/output/local/content/section/header.php @@ -114,7 +114,7 @@ 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; } 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/theme/boost/scss/moodle/course.scss b/theme/boost/scss/moodle/course.scss index e2d24dfe099e4..49d15508ac9cb 100644 --- a/theme/boost/scss/moodle/course.scss +++ b/theme/boost/scss/moodle/course.scss @@ -1650,6 +1650,11 @@ $divider-hover-color: $primary !default; position: absolute; left: -2rem; } + // Delegated sections are not available for bulk editing. + &:has(.delegated-section):hover { + outline: none !important; // stylelint-disable-line declaration-no-important + box-shadow: none !important; // stylelint-disable-line declaration-no-important + } } .course-section-header .bulkselect { left: -2.75rem; diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index bba582f17a008..1c2d059417625 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -29837,6 +29837,10 @@ span.editinstructions .alert-link { position: absolute; left: -2rem; } +.bulkenabled .activity-item:has(.delegated-section):hover { + outline: none !important; + box-shadow: none !important; +} .bulkenabled .course-section-header .bulkselect { left: -2.75rem; position: relative; diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index abf1af20e4afd..42d4acacb90b9 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -29837,6 +29837,10 @@ span.editinstructions .alert-link { position: absolute; left: -2rem; } +.bulkenabled .activity-item:has(.delegated-section):hover { + outline: none !important; + box-shadow: none !important; +} .bulkenabled .course-section-header .bulkselect { left: -2.75rem; position: relative; From 2a01a188a1f66f7de3e4c6ec7eda353be85e056f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikel=20Mart=C3=ADn?= Date: Mon, 15 Jul 2024 09:58:47 +0200 Subject: [PATCH 160/178] MDL-81766 courseformat: Fix hide addsection in delegated sections Delegated sections should not render the add new section button at the bottom of them. --- .../topics/classes/output/courseformat/content/section.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/course/format/topics/classes/output/courseformat/content/section.php b/course/format/topics/classes/output/courseformat/content/section.php index ca41d21552794..a7c50d734d744 100644 --- a/course/format/topics/classes/output/courseformat/content/section.php +++ b/course/format/topics/classes/output/courseformat/content/section.php @@ -45,7 +45,7 @@ public function export_for_template(\renderer_base $output): stdClass { $data = parent::export_for_template($output); - if (!$this->format->get_sectionnum()) { + if (!$this->format->get_sectionnum() && !$this->section->is_delegated()) { $addsectionclass = $format->get_output_classname('content\\addsection'); $addsection = new $addsectionclass($format); $data->numsections = $addsection->export_for_template($output); From 35cfdf49273070dc44e80d94ef8614e6461c5ebd Mon Sep 17 00:00:00 2001 From: Michael Aherne Date: Tue, 23 Jul 2024 09:33:36 +0100 Subject: [PATCH 161/178] MDL-82554 question: Allow null createdby in question_has_capability_on. --- lib/questionlib.php | 2 +- lib/tests/questionlib_test.php | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/questionlib.php b/lib/questionlib.php index bffcd5389c6d7..378632e0d98e0 100644 --- a/lib/questionlib.php +++ b/lib/questionlib.php @@ -1434,7 +1434,7 @@ function question_has_capability_on($questionorid, $cap, $notused = -1): bool { } else if (is_object($questionorid)) { // All we really need in this function is the contextid and author of the question. // We won't bother fetching other details of the question if these 2 fields are provided. - if (isset($questionorid->contextid) && isset($questionorid->createdby)) { + if (isset($questionorid->contextid) && property_exists($questionorid, 'createdby')) { $question = $questionorid; } else if (!empty($questionorid->id)) { $questionid = $questionorid->id; diff --git a/lib/tests/questionlib_test.php b/lib/tests/questionlib_test.php index 2c8183455dc18..7b2aa9462e51f 100644 --- a/lib/tests/questionlib_test.php +++ b/lib/tests/questionlib_test.php @@ -2009,6 +2009,35 @@ public function test_question_has_capability_on_wrong_param_type(): void { question_has_capability_on('one', 'tag'); } + /** + * Test that question_has_capability_on does not fail when passed an object with a null + * createdby property. + */ + public function test_question_has_capability_on_object_with_null_createdby(): void { + $this->resetAfterTest(); + $generator = $this->getDataGenerator(); + $user = $generator->create_user(); + $category = $generator->create_category(); + $context = \context_coursecat::instance($category->id); + + $role = $generator->create_role(); + role_assign($role, $user->id, $context->id); + assign_capability('moodle/question:editmine', CAP_ALLOW, $role, $context->id); + + $this->setUser($user); + + $fakequestion = (object) [ + 'contextid' => $context->id, + 'createdby' => null, + ]; + + $this->assertFalse(question_has_capability_on($fakequestion, 'edit')); + + $fakequestion->createdby = $user->id; + + $this->assertTrue(question_has_capability_on($fakequestion, 'edit')); + } + /** * Test of question_categorylist function. * From b92886ad59b3836bfb04e8b8d5d82ad6b65b59a7 Mon Sep 17 00:00:00 2001 From: Conn Warwicker Date: Mon, 13 May 2024 11:22:29 +0100 Subject: [PATCH 162/178] MDL-70854 core: Add stored progress bars. - Alters existing progress_bar class to support extension - Adds stored_progress_bar class as child of progress_bar - Adds webservice to poll stored progress - Updates database tables - Bumps version - Adds unit/behat tests --- .upgradenotes/MDL-70854-2024071306574741.yml | 5 + .../tool/task/classes/running_tasks_table.php | 24 ++ .../task/tests/behat/running_tasks.feature | 21 + config-dist.php | 9 + lang/en/admin.php | 1 + lib/amd/build/stored_progress.min.js | 11 + lib/amd/build/stored_progress.min.js.map | 1 + lib/amd/src/stored_progress.js | 110 ++++++ lib/behat/classes/behat_core_generator.php | 5 + .../external/output/poll_stored_progress.php | 118 ++++++ lib/classes/output/core_renderer.php | 11 +- lib/classes/output/core_renderer_cli.php | 4 +- lib/classes/output/progress_bar.php | 162 ++++++-- lib/classes/output/stored_progress_bar.php | 365 ++++++++++++++++++ .../task/delete_unconfirmed_users_task.php | 20 +- .../task/stored_progress_bar_cleanup_task.php | 53 +++ .../task/stored_progress_task_trait.php | 59 +++ lib/db/install.xml | 17 + lib/db/services.php | 8 + lib/db/tasks.php | 10 + lib/db/upgrade.php | 28 ++ lib/javascript-static.js | 4 +- lib/templates/progress_bar.mustache | 36 +- lib/testing/generator/data_generator.php | 54 +++ lib/tests/behat/behat_general.php | 17 + .../output/poll_stored_progress_test.php | 65 ++++ lib/tests/stored_progress_bar_test.php | 195 ++++++++++ .../stored_progress_bar_cleanup_task_test.php | 51 +++ version.php | 2 +- 29 files changed, 1408 insertions(+), 58 deletions(-) create mode 100644 .upgradenotes/MDL-70854-2024071306574741.yml create mode 100644 lib/amd/build/stored_progress.min.js create mode 100644 lib/amd/build/stored_progress.min.js.map create mode 100644 lib/amd/src/stored_progress.js create mode 100644 lib/classes/external/output/poll_stored_progress.php create mode 100644 lib/classes/output/stored_progress_bar.php create mode 100644 lib/classes/task/stored_progress_bar_cleanup_task.php create mode 100644 lib/classes/task/stored_progress_task_trait.php create mode 100644 lib/tests/external/output/poll_stored_progress_test.php create mode 100644 lib/tests/stored_progress_bar_test.php create mode 100644 lib/tests/task/stored_progress_bar_cleanup_task_test.php 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/admin/tool/task/classes/running_tasks_table.php b/admin/tool/task/classes/running_tasks_table.php index f26265d99ab61..1193e93ead765 100644 --- a/admin/tool/task/classes/running_tasks_table.php +++ b/admin/tool/task/classes/running_tasks_table.php @@ -49,6 +49,7 @@ public function __construct() { 'classname' => get_string('classname', 'tool_task'), 'type' => get_string('tasktype', 'admin'), 'time' => get_string('taskage', 'tool_task'), + 'progress' => get_string('progress', 'core'), 'timestarted' => get_string('started', 'tool_task'), 'hostname' => get_string('hostname', 'tool_task'), 'pid' => get_string('pid', 'tool_task'), @@ -153,4 +154,27 @@ public function col_time($row): string { public function col_timestarted($row): string { return userdate($row->timestarted); } + + /** + * Format the progress column. + * + * @param \stdClass $row + * @return string + */ + public function col_progress($row): string { + // Check to see if there is a stored progress record for this task. + if ($row->type === 'adhoc') { + $idnumber = \core\output\stored_progress_bar::convert_to_idnumber($row->classname, $row->id); + } else { + $idnumber = \core\output\stored_progress_bar::convert_to_idnumber($row->classname); + } + + $bar = \core\output\stored_progress_bar::get_by_idnumber($idnumber); + if ($bar) { + return $bar->get_content(); + } else { + return '-'; + } + } + } diff --git a/admin/tool/task/tests/behat/running_tasks.feature b/admin/tool/task/tests/behat/running_tasks.feature index 5725d5c51c435..b26d6bd993260 100644 --- a/admin/tool/task/tests/behat/running_tasks.feature +++ b/admin/tool/task/tests/behat/running_tasks.feature @@ -38,3 +38,24 @@ Feature: See running scheduled tasks And I should see "2 days" in the "core\task\asynchronous_restore_task" "table_row" And I should see "c69335460f7f" in the "core\task\asynchronous_restore_task" "table_row" And I should see "1916" in the "core\task\asynchronous_restore_task" "table_row" + + @javascript + Scenario: If a task with a stored progress bar is running, I should be able to observe the progress. + Given the following config values are set as admin: + | progresspollinterval | 1 | + And the following "tool_task > scheduled tasks" exist: + | classname | seconds | hostname | pid | + | \core\task\delete_unconfirmed_users_task | 120 | c69335460f7f | 1917 | + And the following "stored progress bars" exist: + | idnumber | percent | + | core_task_delete_unconfirmed_users_task | 50.00 | + And I navigate to "Server > Tasks > Tasks running now" in site administration + And I should see "2 mins" in the "Delete unconfirmed users" "table_row" + And I should see "c69335460f7f" in the "Delete unconfirmed users" "table_row" + And I should see "1917" in the "Delete unconfirmed users" "table_row" + And I should see "50.0%" in the "Delete unconfirmed users" "table_row" + When I set the stored progress bar "core_task_delete_unconfirmed_users_task" to "75.00" + # Wait for the progress polling. + And I wait "1" seconds + Then I should not see "50.0%" in the "Delete unconfirmed users" "table_row" + And I should see "75.0%" in the "Delete unconfirmed users" "table_row" 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/lang/en/admin.php b/lang/en/admin.php index be392c748380c..db918e07cbd86 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -1342,6 +1342,7 @@ $string['stickyblocksduplicatenotice'] = 'If any block you add here is already present in a particular page, it will result in a duplicate.
Only the pinned block will be non-editable, the duplicate will still be editable.'; $string['stickyblocksmymoodle'] = 'My Moodle'; $string['stickyblockspagetype'] = 'Page type to configure'; +$string['storedprogressbarcleanuptask'] = 'Stored progress bar cleanup task'; $string['strictformsrequired'] = 'Strict validation of required fields'; $string['stripalltitletags'] = 'Remove HTML tags from all activity names'; $string['supportandservices'] = 'Support and services'; diff --git a/lib/amd/build/stored_progress.min.js b/lib/amd/build/stored_progress.min.js new file mode 100644 index 0000000000000..2e068914025ff --- /dev/null +++ b/lib/amd/build/stored_progress.min.js @@ -0,0 +1,11 @@ +define("core/stored_progress",["exports","core/ajax","core/notification"],(function(_exports,Ajax,_notification){var obj;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)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,Ajax=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} +/** + * Script to update stored_progress progress bars on the screen. + * + * @module core/stored_progress + * @copyright 2023 onwards Catalyst IT {@link http://www.catalyst-eu.net/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Conn Warwicker + */(Ajax),_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj};var STORED_PROGRESS_LOADED=!1;function poll(ids,timeout){let promise=Ajax.call([{methodname:"core_output_poll_stored_progress",args:{ids:ids}}]),repollids=[];promise[0].then((function(results){return results.forEach((function(data){updateProgressBar(data.uniqueid,data.progress,data.message,data.estimated,data.error),data.progress<100&&!data.error&&repollids.push(data.id),data.timeout&&data.timeout>0&&(timeout=data.timeout)})),repollids.length>0&&setTimeout((()=>poll(repollids,timeout)),1e3*timeout)})).catch(_notification.default.exception)}_exports.init=timeout=>{if(!1===STORED_PROGRESS_LOADED){let ids=[];document.querySelectorAll(".stored-progress-bar").forEach((el=>{let id=el.dataset.recordid;ids.push(id)})),poll(ids,timeout),STORED_PROGRESS_LOADED=!0}}})); + +//# sourceMappingURL=stored_progress.min.js.map \ No newline at end of file diff --git a/lib/amd/build/stored_progress.min.js.map b/lib/amd/build/stored_progress.min.js.map new file mode 100644 index 0000000000000..46068db0a4394 --- /dev/null +++ b/lib/amd/build/stored_progress.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"stored_progress.min.js","sources":["../src/stored_progress.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 * Script to update stored_progress progress bars on the screen.\n *\n * @module core/stored_progress\n * @copyright 2023 onwards Catalyst IT {@link http://www.catalyst-eu.net/}\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @author Conn Warwicker \n */\n\n/* global updateProgressBar */\n\nimport * as Ajax from 'core/ajax';\nimport Notification from 'core/notification';\n\n/**\n * @var bool This AMD script is loaded multiple times, for each progress bar on a page.\n * So this stops it running multiple times.\n * */\nvar STORED_PROGRESS_LOADED = false;\n\n/**\n * Poll a given stored progress record.\n *\n * @param {array} ids\n * @param {integer} timeout\n */\nfunction poll(ids, timeout) {\n\n // Call AJAX request.\n let promise = Ajax.call([{\n methodname: 'core_output_poll_stored_progress', args: {'ids': ids}\n }]);\n\n let repollids = [];\n\n // When AJAX request returns, handle the results.\n promise[0].then(function(results) {\n\n results.forEach(function(data) {\n\n // Update the progress bar percentage and message using the core method from the javascript-static.js.\n updateProgressBar(data.uniqueid, data.progress, data.message, data.estimated, data.error);\n\n // Add the bar for re-polling if it's not completed.\n if (data.progress < 100 && !data.error) {\n repollids.push(data.id);\n }\n\n // If a different timeout came back from the script, use that instead.\n if (data.timeout && data.timeout > 0) {\n timeout = data.timeout;\n }\n\n });\n\n // If we still want to poll any of them, do it again.\n if (repollids.length > 0) {\n return setTimeout(() => poll(repollids, timeout), timeout * 1000);\n }\n\n return false;\n\n }).catch(Notification.exception);\n\n}\n\n/**\n * Initialise the polling process.\n *\n * @param {integer} timeout Timeout to use (seconds).\n */\nexport const init = (timeout) => {\n\n if (STORED_PROGRESS_LOADED === false) {\n\n let ids = [];\n\n // Find any stored progress bars we want to poll.\n document.querySelectorAll('.stored-progress-bar').forEach(el => {\n\n // Get its id and add to array.\n let id = el.dataset.recordid;\n ids.push(id);\n\n });\n\n // Poll for updates from these IDs.\n poll(ids, timeout);\n\n // Script has run, we don't want it to run again.\n STORED_PROGRESS_LOADED = true;\n\n }\n\n};"],"names":["STORED_PROGRESS_LOADED","poll","ids","timeout","promise","Ajax","call","methodname","args","repollids","then","results","forEach","data","updateProgressBar","uniqueid","progress","message","estimated","error","push","id","length","setTimeout","catch","Notification","exception","document","querySelectorAll","el","dataset","recordid"],"mappings":";;;;;;;;oFAiCIA,wBAAyB,WAQpBC,KAAKC,IAAKC,aAGXC,QAAUC,KAAKC,KAAK,CAAC,CACrBC,WAAY,mCAAoCC,KAAM,KAAQN,QAG9DO,UAAY,GAGhBL,QAAQ,GAAGM,MAAK,SAASC,gBAErBA,QAAQC,SAAQ,SAASC,MAGrBC,kBAAkBD,KAAKE,SAAUF,KAAKG,SAAUH,KAAKI,QAASJ,KAAKK,UAAWL,KAAKM,OAG/EN,KAAKG,SAAW,MAAQH,KAAKM,OAC7BV,UAAUW,KAAKP,KAAKQ,IAIpBR,KAAKV,SAAWU,KAAKV,QAAU,IAC/BA,QAAUU,KAAKV,YAMnBM,UAAUa,OAAS,GACZC,YAAW,IAAMtB,KAAKQ,UAAWN,UAAoB,IAAVA,YAKvDqB,MAAMC,sBAAaC,yBASLvB,cAEc,IAA3BH,uBAAkC,KAE9BE,IAAM,GAGVyB,SAASC,iBAAiB,wBAAwBhB,SAAQiB,SAGlDR,GAAKQ,GAAGC,QAAQC,SACpB7B,IAAIkB,KAAKC,OAKbpB,KAAKC,IAAKC,SAGVH,wBAAyB"} \ No newline at end of file diff --git a/lib/amd/src/stored_progress.js b/lib/amd/src/stored_progress.js new file mode 100644 index 0000000000000..762dc38443d36 --- /dev/null +++ b/lib/amd/src/stored_progress.js @@ -0,0 +1,110 @@ +// 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 . + +/** + * Script to update stored_progress progress bars on the screen. + * + * @module core/stored_progress + * @copyright 2023 onwards Catalyst IT {@link http://www.catalyst-eu.net/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Conn Warwicker + */ + +/* global updateProgressBar */ + +import * as Ajax from 'core/ajax'; +import Notification from 'core/notification'; + +/** + * @var bool This AMD script is loaded multiple times, for each progress bar on a page. + * So this stops it running multiple times. + * */ +var STORED_PROGRESS_LOADED = false; + +/** + * Poll a given stored progress record. + * + * @param {array} ids + * @param {integer} timeout + */ +function poll(ids, timeout) { + + // Call AJAX request. + let promise = Ajax.call([{ + methodname: 'core_output_poll_stored_progress', args: {'ids': ids} + }]); + + let repollids = []; + + // When AJAX request returns, handle the results. + promise[0].then(function(results) { + + results.forEach(function(data) { + + // Update the progress bar percentage and message using the core method from the javascript-static.js. + updateProgressBar(data.uniqueid, data.progress, data.message, data.estimated, data.error); + + // Add the bar for re-polling if it's not completed. + if (data.progress < 100 && !data.error) { + repollids.push(data.id); + } + + // If a different timeout came back from the script, use that instead. + if (data.timeout && data.timeout > 0) { + timeout = data.timeout; + } + + }); + + // If we still want to poll any of them, do it again. + if (repollids.length > 0) { + return setTimeout(() => poll(repollids, timeout), timeout * 1000); + } + + return false; + + }).catch(Notification.exception); + +} + +/** + * Initialise the polling process. + * + * @param {integer} timeout Timeout to use (seconds). + */ +export const init = (timeout) => { + + if (STORED_PROGRESS_LOADED === false) { + + let ids = []; + + // Find any stored progress bars we want to poll. + document.querySelectorAll('.stored-progress-bar').forEach(el => { + + // Get its id and add to array. + let id = el.dataset.recordid; + ids.push(id); + + }); + + // Poll for updates from these IDs. + poll(ids, timeout); + + // Script has run, we don't want it to run again. + STORED_PROGRESS_LOADED = true; + + } + +}; \ No newline at end of file diff --git a/lib/behat/classes/behat_core_generator.php b/lib/behat/classes/behat_core_generator.php index bc1e080791274..7bedba6d03a1b 100644 --- a/lib/behat/classes/behat_core_generator.php +++ b/lib/behat/classes/behat_core_generator.php @@ -316,6 +316,11 @@ protected function get_creatable_entities(): array { 'required' => ['subject', 'userfrom', 'userto'], 'switchids' => ['userfrom' => 'userfromid', 'userto' => 'usertoid'], ], + 'stored progress bars' => [ + 'singular' => 'stored progress bar', + 'datagenerator' => 'stored_progress_bar', + 'required' => ['idnumber'], + ], ]; return $entities; diff --git a/lib/classes/external/output/poll_stored_progress.php b/lib/classes/external/output/poll_stored_progress.php new file mode 100644 index 0000000000000..d17a6f3b6d7b4 --- /dev/null +++ b/lib/classes/external/output/poll_stored_progress.php @@ -0,0 +1,118 @@ +. + +namespace core\external\output; + +use core_external\external_function_parameters; +use core_external\external_multiple_structure; +use core_external\external_single_structure; +use core_external\external_value; + +/** + * Poll Stored Progress webservice. + * + * @package core + * @copyright 2023 onwards Catalyst IT {@link http://www.catalyst-eu.net/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Conn Warwicker + */ +class poll_stored_progress extends \core_external\external_api { + + /** + * Returns description of method parameters + * + * @return external_function_parameters + */ + public static function execute_parameters() { + return new external_function_parameters([ + 'ids' => new external_multiple_structure( + new external_value(PARAM_INT, 'The stored_progress ID', VALUE_REQUIRED) + ), + ]); + } + + /** + * Returns description of method return data + * + * @return external_multiple_structure + */ + public static function execute_returns() { + return new external_multiple_structure( + new external_single_structure([ + 'id' => new external_value(PARAM_INT, 'stored_progress record id'), + 'uniqueid' => new external_value(PARAM_TEXT, 'unique element id'), + 'progress' => new external_value(PARAM_FLOAT, 'percentage progress'), + 'estimated' => new external_value(PARAM_RAW, 'estimated time left string'), + 'message' => new external_value(PARAM_TEXT, 'message to be displayed with the bar'), + 'error' => new external_value(PARAM_TEXT, 'error', VALUE_OPTIONAL), + 'timeout' => new external_value(PARAM_TEXT, 'timeout to use in the polling', VALUE_OPTIONAL), + ]) + ); + } + + /** + * Poll the database for the progress of stored progress objects + * + * @param array $ids + * @return array + */ + public static function execute(array $ids) { + global $CFG, $DB; + + $params = self::validate_parameters(self::execute_parameters(), [ + 'ids' => $ids, + ]); + + $return = []; + + foreach ($ids as $id) { + + // Load the stored progress bar object. + $bar = \core\output\stored_progress_bar::get_by_id($id); + if ($bar) { + + // Return the updated bar data. + $return[$id] = [ + 'id' => $id, + 'uniqueid' => $bar->get_id(), + 'progress' => $bar->get_percent(), + 'estimated' => $bar->get_estimate_message($bar->get_percent()), + 'message' => $bar->get_message(), + 'timeout' => \core\output\stored_progress_bar::get_timeout(), + 'error' => $bar->get_haserrored(), + ]; + + } else { + + // If we could not find the record, we still need to return the right arguments in the array for the webservice. + $return[$id] = [ + 'id' => $id, + 'uniqueid' => '', + 'progress' => 0, + 'estimated' => '', + 'message' => get_string('invalidrecordunknown', 'error'), + 'timeout' => \core\output\stored_progress_bar::get_timeout(), + 'error' => true, + ]; + + } + + } + + return $return; + } + +} diff --git a/lib/classes/output/core_renderer.php b/lib/classes/output/core_renderer.php index 27d63344b7bb7..d52a988411014 100644 --- a/lib/classes/output/core_renderer.php +++ b/lib/classes/output/core_renderer.php @@ -4697,15 +4697,12 @@ public function render_progress_bar(progress_bar $bar) { * @param float $percent * @param string $msg Message * @param string $estimate time remaining message + * @param bool $error Was there an error? * @return string ascii fragment */ - public function render_progress_bar_update(string $id, float $percent, string $msg, string $estimate): string { - return html_writer::script(js_writer::function_call('updateProgressBar', [ - $id, - round($percent, 1), - $msg, - $estimate, - ])); + public function render_progress_bar_update(string $id, float $percent, string $msg, string $estimate, + bool $error = false): string { + return html_writer::script(js_writer::function_call('updateProgressBar', [$id, $percent, $msg, $estimate, $error])); } /** diff --git a/lib/classes/output/core_renderer_cli.php b/lib/classes/output/core_renderer_cli.php index d277966bd2a44..0e8f20722748c 100644 --- a/lib/classes/output/core_renderer_cli.php +++ b/lib/classes/output/core_renderer_cli.php @@ -116,9 +116,11 @@ public function render_progress_bar(progress_bar $bar) { * @param float $percent * @param string $msg Message * @param string $estimate time remaining message + * @param bool $error (Unused in cli) * @return string ascii fragment */ - public function render_progress_bar_update(string $id, float $percent, string $msg, string $estimate): string { + public function render_progress_bar_update(string $id, float $percent, string $msg, string $estimate, + bool $error = false): string { $size = 55; // The width of the progress bar in chars. $ascii = ''; diff --git a/lib/classes/output/progress_bar.php b/lib/classes/output/progress_bar.php index 93cb60545e094..6c26a14676e7b 100644 --- a/lib/classes/output/progress_bar.php +++ b/lib/classes/output/progress_bar.php @@ -34,35 +34,49 @@ * @category output */ class progress_bar implements renderable, templatable { - /** @var string html id */ - private $htmlid; + + /** @var bool Can use output buffering. */ + protected static $supportsoutputbuffering = false; + + /** @var string unique id */ + protected $idnumber; + /** @var int total width */ - private $width; + protected $width; + /** @var int last percentage printed */ - private $percent = 0; + protected $percent = 0; + /** @var int time when last printed */ - private $lastupdate = 0; + protected $lastupdate = 0; + /** @var int when did we start printing this */ - private $timestart = 0; + protected $timestart = 0; + + /** @var bool Whether or not to auto render updates to the screen */ + protected $autoupdate = true; + + /** @var bool Whether or not an error has occured */ + protected $haserrored = false; /** * Constructor * * Prints JS code if $autostart true. * - * @param string $htmlid The container ID. + * @param string $htmlid The unique ID for the progress bar or HTML container id. * @param int $width The suggested width. * @param bool $autostart Whether to start the progress bar right away. */ public function __construct($htmlid = '', $width = 500, $autostart = false) { - if (!CLI_SCRIPT && !NO_OUTPUT_BUFFERING) { + if (!static::$supportsoutputbuffering && !CLI_SCRIPT && !NO_OUTPUT_BUFFERING) { debugging('progress_bar used in a non-CLI script without setting NO_OUTPUT_BUFFERING.', DEBUG_DEVELOPER); } if (!empty($htmlid)) { - $this->htmlid = $htmlid; + $this->idnumber = $htmlid; } else { - $this->htmlid = 'pbar_' . uniqid(); + $this->idnumber = 'pbar_'.uniqid(); } $this->width = $width; @@ -77,7 +91,15 @@ public function __construct($htmlid = '', $width = 500, $autostart = false) { * @return string id */ public function get_id(): string { - return $this->htmlid; + return $this->idnumber; + } + + /** + * Get the percent + * @return float + */ + public function get_percent(): float { + return $this->percent; } /** @@ -86,15 +108,43 @@ public function get_id(): string { * @return void Echo's output */ public function create() { - global $OUTPUT; $this->timestart = microtime(true); + $this->render(); + } + + /** + * Render the progress bar. + * + * @return void + */ + public function render(): void { flush(); - echo $OUTPUT->render($this); + echo $this->get_content(); flush(); } + /** + * Get the content to be rendered + * + * @return string + */ + public function get_content(): string { + global $OUTPUT; + return $OUTPUT->render($this); + } + + /** + * Set whether or not to auto render updates to the screen + * + * @param bool $value + * @return void + */ + public function auto_update(bool $value): void { + $this->autoupdate = $value; + } + /** * Update the progress bar. * @@ -103,7 +153,7 @@ public function create() { * @return void Echo's output * @throws coding_exception */ - private function update_raw($percent, $msg) { + protected function update_raw($percent, $msg) { global $OUTPUT; if (empty($this->timestart)) { @@ -113,28 +163,20 @@ private function update_raw($percent, $msg) { $estimate = $this->estimate($percent); - if ($estimate === null) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf - // Always do the first and last updates. - } else if ($estimate == 0) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf - // Always do the last updates. - } else if ($this->lastupdate + 20 < time()) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf - // We must update otherwise browser would time out. - } else if (round($this->percent, 2) === round($percent, 2)) { - // No significant change, no need to update anything. + // No significant change, no need to update anything. + if (round($this->percent, 2) === round($percent, 2)) { return; } - $estimatemsg = ''; - if ($estimate != 0 && is_numeric($estimate)) { - // Err on the conservative side and also avoid showing 'now' as the estimate. - $estimatemsg = format_time(ceil($estimate)); - } + $estimatemsg = $this->get_estimate_message($percent); $this->percent = $percent; $this->lastupdate = microtime(true); - echo $OUTPUT->render_progress_bar_update($this->htmlid, $this->percent, $msg, $estimatemsg); - flush(); + if ($this->autoupdate) { + echo $OUTPUT->render_progress_bar_update($this->idnumber, sprintf("%.1f", $this->percent), $msg, $estimatemsg); + flush(); + } } /** @@ -143,7 +185,7 @@ private function update_raw($percent, $msg) { * @param int $pt From 1-100. * @return mixed Null (unknown), or int. */ - private function estimate($pt) { + protected function estimate($pt) { if ($this->lastupdate == 0) { return null; } @@ -201,10 +243,68 @@ public function restart() { */ public function export_for_template(renderer_base $output) { return [ - 'id' => $this->htmlid, + 'id' => '', + 'idnumber' => $this->idnumber, 'width' => $this->width, + 'class' => '', + 'value' => 0, + 'error' => 0, ]; } + + /** + * This gets the estimate message to be displayed with the progress bar. + * + * @param float $percent + * @return string + */ + public function get_estimate_message(float $percent): string { + $estimate = $this->estimate($percent); + $estimatemsg = ''; + if ($estimate != 0 && is_numeric($estimate)) { + $estimatemsg = format_time(ceil($estimate)); + } + + return $estimatemsg; + } + + /** + * Set the error flag on the object + * + * @param bool $value + * @return void + */ + protected function set_haserrored(bool $value): void { + $this->haserrored = $value; + } + + /** + * Check if the process has errored + * + * @return bool + */ + public function get_haserrored(): bool { + return $this->haserrored; + } + + /** + * Set that the process running has errored + * + * @param string $errormsg + * @return void + */ + public function error(string $errormsg): void { + global $OUTPUT; + + $this->haserrored = true; + $this->message = $errormsg; + + if ($this->autoupdate) { + echo $OUTPUT->render_progress_bar_update($this->idnumber, sprintf("%.1f", $this->percent), $errormsg, '', true); + flush(); + } + } + } // Alias this class to the old name. diff --git a/lib/classes/output/stored_progress_bar.php b/lib/classes/output/stored_progress_bar.php new file mode 100644 index 0000000000000..37d5be6a007fd --- /dev/null +++ b/lib/classes/output/stored_progress_bar.php @@ -0,0 +1,365 @@ +. + +namespace core\output; + +/** + * Stored progress bar class. + * + * @package core + * @copyright 2023 onwards Catalyst IT {@link http://www.catalyst-eu.net/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Conn Warwicker + */ +class stored_progress_bar extends progress_bar { + + /** @var bool Can use output buffering. */ + protected static $supportsoutputbuffering = true; + + /** @var int DB record ID */ + protected $recordid; + + /** @var string|null Message to associate with bar */ + protected $message = null; + + /** @var \core\clock Clock object */ + protected $clock; + + /** + * This overwrites the progress_bar::__construct method. + * + * @param string $idnumber + */ + public function __construct($idnumber) { + + $this->clock = \core\di::get(\core\clock::class); + + // Construct from the parent. + parent::__construct($idnumber, 0, true); + + } + + /** + * Just set the timestart, do not render the bar immediately. + * + * @return void + */ + public function create(): void { + $this->timestart = $this->clock->time(); + } + + /** + * Load the stored progress bar from the database based on its uniqued idnumber + * + * @param string $idnumber Unique ID of the bar + * @return stored_progress_bar|null + */ + public static function get_by_idnumber(string $idnumber): ?stored_progress_bar { + global $DB; + + $record = $DB->get_record('stored_progress', ['idnumber' => $idnumber]); + if ($record) { + return self::load($record); + } else { + return null; + } + } + + /** + * Load the stored progress bar from the database, based on it's record ID + * + * @param int $id Database record ID + * @return stored_progress_bar|null + */ + public static function get_by_id(int $id): ?stored_progress_bar { + global $DB; + + $record = $DB->get_record('stored_progress', ['id' => $id]); + if ($record) { + return self::load($record); + } else { + return null; + } + } + + /** + * Load the stored progress bar object from its record in the database. + * + * @param stdClass $record + * @return stored_progress_bar + */ + public static function load(\stdClass $record): stored_progress_bar { + $progress = new stored_progress_bar($record->idnumber); + $progress->set_record_id($record->id); + $progress->set_time_started($record->timestart); + $progress->set_last_updated($record->lastupdate); + $progress->set_percent($record->percentcompleted); + $progress->set_message($record->message); + $progress->set_haserrored($record->haserrored); + return $progress; + } + + /** + * Set the DB record ID + * + * @param int $id + * @return void + */ + protected function set_record_id(int $id): void { + $this->recordid = $id; + } + + /** + * Set the time we started the process. + * + * @param int $value + * @return void + */ + protected function set_time_started(int $value): void { + $this->timestart = $value; + } + + /** + * Set the time we started last updated the progress. + * + * @param int|null $value + * @return void + */ + protected function set_last_updated(?int $value = null): void { + $this->lastupdate = $value; + } + + /** + * Set the percent completed. + * + * @param float|null $value + * @return void + */ + protected function set_percent($value = null): void { + $this->percent = $value; + } + + /** + * Set the message. + * + * @param string|null $value + * @return void + */ + protected function set_message(?string $value = null): void { + $this->message = $value; + } + + /** + * Set that the process running has errored and store that against the bar + * + * @param string $errormsg + * @return void + */ + public function error(string $errormsg): void { + // Update the error variables. + parent::error($errormsg); + + // Update the record. + $this->update_record(); + } + + /** + * Get the progress bar message. + * + * @return string|null + */ + public function get_message(): ?string { + return $this->message; + } + + /** + * Get the content to display the progress bar and start polling via AJAX + * + * @return string + */ + public function get_content(): string { + global $CFG, $PAGE, $OUTPUT; + + $PAGE->requires->js_call_amd('core/stored_progress', 'init', [ + self::get_timeout(), + ]); + + $context = $this->export_for_template($OUTPUT); + return $OUTPUT->render_from_template('core/progress_bar', $context); + } + + /** + * Export for template. + * + * @param renderer_base $output The renderer. + * @return array + */ + public function export_for_template(\renderer_base $output): array { + return [ + 'id' => $this->recordid, + 'idnumber' => $this->idnumber, + 'width' => $this->width, + 'class' => 'stored-progress-bar', + 'value' => $this->percent, + 'message' => $this->message, + 'error' => $this->haserrored, + ]; + } + + /** + * Start the recording of the progress and store in the database + * + * @return int ID of the DB record + */ + public function start(): int { + global $OUTPUT, $DB; + + // If we are running in an non-interactive CLI environment, call the progress bar renderer to avoid warnings + // when we do an update. + if (defined('STDOUT') && !stream_isatty(STDOUT)) { + $OUTPUT->render_progress_bar($this); + } + + // Delete any existing records for this. + $this->clear_records(); + + // Create new progress record. + $this->recordid = $DB->insert_record('stored_progress', [ + 'idnumber' => $this->idnumber, + 'timestart' => (int)$this->timestart, + ]); + + return $this->recordid; + } + + /** + * End the polling progress and delete the DB record. + * + * @return void + */ + protected function clear_records(): void { + global $DB; + + $DB->delete_records('stored_progress', [ + 'idnumber' => $this->idnumber, + ]); + } + + /** + * Update the database record with the percentage and message + * + * @param float $percent + * @param string $msg + * @return void + */ + protected function update_raw($percent, $msg): void { + $this->percent = $percent; + $this->message = $msg; + + // Update the database record with the new data. + $this->update_record(); + + // Update any CLI script's progress with an ASCII progress bar. + $this->render_update(); + } + + /** + * Render an update to the CLI + * + * This will only work in CLI scripts, and not in scheduled/adhoc tasks even though they run via CLI, + * as they seem to use a different renderer (core_renderer instead of core_renderer_cli). + * + * We also can't check this based on "CLI_SCRIPT" const as that is true for tasks. + * + * So this will just check a flag to see if we want auto rendering of updates. + * + * @return void + */ + protected function render_update(): void { + global $OUTPUT; + + // If no output buffering, don't render it at all. + if (defined('NO_OUTPUT_BUFFERING') && NO_OUTPUT_BUFFERING) { + $this->auto_update(false); + } + + // If we want the screen to auto update, render it. + if ($this->autoupdate) { + echo $OUTPUT->render_progress_bar_update( + $this->idnumber, sprintf("%.1f", $this->percent), $this->message, $this->get_estimate_message($this->percent) + ); + } + } + + /** + * Update the database record + * + * @throws \moodle_exception + * @return void + */ + protected function update_record(): void { + global $DB; + + if (is_null($this->recordid)) { + throw new \moodle_exception('Polling has not been started. Cannot set iteration.'); + } + + // Update time. + $this->lastupdate = $this->clock->time(); + + // Update the database record. + $record = new \stdClass(); + $record->id = $this->recordid; + $record->lastupdate = (int)$this->lastupdate; + $record->percentcompleted = $this->percent; + $record->message = $this->message; + $record->haserrored = $this->haserrored; + $DB->update_record('stored_progress', $record); + } + + /** + * We need a way to specify a unique idnumber for processes being monitored, so that + * firstly we don't accidentally overwrite a running process, and secondly so we can + * automatically load them in some cases, without having to manually code in its name. + * + * So this uses the classname of the object being monitored, along with its id. + * + * This method should be used when creating the stored_progress record to set it's idnumber. + * + * @param string $class Class name of the object being monitored, e.g. \local_something\task\my_task + * @param int|null $id ID of an object from database, e.g. 123 + * @return string Converted string, e.g. local_something_task_my_task_123 + */ + public static function convert_to_idnumber(string $class, ?int $id = null): string { + $idnumber = preg_replace("/[^a-z0-9_]/", "_", ltrim($class, '\\')); + if (!is_null($id)) { + $idnumber .= '_' . $id; + } + + return $idnumber; + } + + /** + * Get the polling timeout in seconds. Default: 5. + * + * @return int + */ + public static function get_timeout(): int { + global $CFG; + return $CFG->progresspollinterval ?? 5; + } + +} diff --git a/lib/classes/task/delete_unconfirmed_users_task.php b/lib/classes/task/delete_unconfirmed_users_task.php index 40dfe8ef1ef8a..9cc4c416b0f8a 100644 --- a/lib/classes/task/delete_unconfirmed_users_task.php +++ b/lib/classes/task/delete_unconfirmed_users_task.php @@ -27,6 +27,7 @@ * Simple task to delete user accounts for users who have not confirmed in time. */ class delete_unconfirmed_users_task extends scheduled_task { + use stored_progress_task_trait; /** * Get a descriptive name for this task (shown to admins). @@ -48,14 +49,23 @@ public function execute() { // Delete users who haven't confirmed within required period. if (!empty($CFG->deleteunconfirmed)) { + $this->start_stored_progress(); $cuttime = $timenow - ($CFG->deleteunconfirmed * 3600); - $rs = $DB->get_recordset_sql ("SELECT * - FROM {user} - WHERE confirmed = 0 AND timecreated > 0 - AND timecreated < ? AND deleted = 0", array($cuttime)); + $selectcount = "SELECT COUNT(*)"; + $select = "SELECT *"; + $sql = " + FROM {user} + WHERE confirmed = 0 AND timecreated > 0 + AND timecreated < ? AND deleted = 0"; + $params = [$cuttime]; + $count = $DB->count_records_sql($selectcount . $sql, $params); + $rs = $DB->get_recordset_sql($select . $sql, $params); + $processed = 0; foreach ($rs as $user) { delete_user($user); - mtrace(" Deleted unconfirmed user ".fullname($user, true)." ($user->id)"); + $message = " Deleted unconfirmed user ".fullname($user, true)." ($user->id)"; + $processed++; + $this->progress->update($processed, $count, $message); } $rs->close(); } diff --git a/lib/classes/task/stored_progress_bar_cleanup_task.php b/lib/classes/task/stored_progress_bar_cleanup_task.php new file mode 100644 index 0000000000000..14f438cfc3577 --- /dev/null +++ b/lib/classes/task/stored_progress_bar_cleanup_task.php @@ -0,0 +1,53 @@ +. + +namespace core\task; + +/** + * Scheduled task to clean up old stored_progress bar records. + * + * @package core + * @copyright 2023 onwards Catalyst IT {@link http://www.catalyst-eu.net/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Conn Warwicker + */ +class stored_progress_bar_cleanup_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('storedprogressbarcleanuptask', 'admin'); + } + + /** + * Delete all the old stored progress bar records. + * By default this runs once per day at 1AM. + * + * @return void + */ + public function execute(): void { + global $DB; + + $twentyfourhoursago = time() - DAYSECS; + + $DB->delete_records_select('stored_progress', 'lastupdate < :ago', ['ago' => $twentyfourhoursago]); + + mtrace('Deleted old stored_progress records'); + } +} diff --git a/lib/classes/task/stored_progress_task_trait.php b/lib/classes/task/stored_progress_task_trait.php new file mode 100644 index 0000000000000..ae79ccf913e4d --- /dev/null +++ b/lib/classes/task/stored_progress_task_trait.php @@ -0,0 +1,59 @@ +. + +namespace core\task; + +/** + * Trait to use in tasks to automatically add stored progress functionality. + * + * @package core + * @copyright 2024 onwards Catalyst IT {@link http://www.catalyst-eu.net/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Conn Warwicker + */ +trait stored_progress_task_trait { + + /** @var \core\output\stored_progress_bar|null $progress */ + protected $progress = null; + + /** + * Start a stored progress bar implementation for the task this trait is used in. + * + * @return void + */ + protected function start_stored_progress(): void { + global $OUTPUT, $PAGE; + + // To get around the issue in MDL-80770, we are manually setting the renderer to cli. + $OUTPUT = $PAGE->get_renderer('core', null, 'cli'); + + // Construct a unique name for the progress bar. + // For adhoc tasks, this will need the ID in it. For scheduled tasks just the class name. + if (method_exists($this, 'get_id')) { + $name = get_class($this) . '_' . $this->get_id(); + } else { + $name = get_class($this); + } + + $this->progress = new \core\output\stored_progress_bar( + \core\output\stored_progress_bar::convert_to_idnumber($name) + ); + + // Start the progress. + $this->progress->start(); + } + +} diff --git a/lib/db/install.xml b/lib/db/install.xml index a2f52c5a99649..a88720e8a9706 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -4842,5 +4842,22 @@ + + + + + + + + + + + + + + + + +
diff --git a/lib/db/services.php b/lib/db/services.php index 39766f2707fff..a0ec47bf50520 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -3223,6 +3223,14 @@ 'type' => 'read', 'ajax' => true, ], + 'core_output_poll_stored_progress' => [ + 'classname' => 'core\external\output\poll_stored_progress', + 'methodname' => 'execute', + 'description' => 'Polls for the current percentage progress of a stored progress object', + 'type' => 'read', + 'ajax' => true, + 'readonlysession' => true, + ], ); $services = array( diff --git a/lib/db/tasks.php b/lib/db/tasks.php index 0a9548e4b45dc..2ef837ca2ad11 100644 --- a/lib/db/tasks.php +++ b/lib/db/tasks.php @@ -476,4 +476,14 @@ 'dayofweek' => 'R', 'disabled' => true, ], + [ + 'classname' => 'core\task\stored_progress_bar_cleanup_task', + 'blocking' => 0, + 'minute' => '00', + 'hour' => '01', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*', + 'disabled' => false, + ], ); diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index c5e13751ca346..56bfed471a501 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -1178,5 +1178,33 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2024070500.01); } + if ($oldversion < 2024071900.01) { + // Define table stored_progress to be created. + $table = new xmldb_table('stored_progress'); + + // Adding fields to table stored_progress. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('idnumber', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('timestart', XMLDB_TYPE_INTEGER, '20', null, null, null, null); + $table->add_field('lastupdate', XMLDB_TYPE_INTEGER, '20', null, null, null, null); + $table->add_field('percentcompleted', XMLDB_TYPE_NUMBER, '5, 2', null, null, null, '0'); + $table->add_field('message', XMLDB_TYPE_CHAR, '255', null, null, null, null); + $table->add_field('haserrored', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0'); + + // Adding keys to table stored_progress. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + + // Adding indexes to table stored_progress. + $table->add_index('uid_index', XMLDB_INDEX_NOTUNIQUE, ['idnumber']); + + // Conditionally launch create table for stored_progress. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2024071900.01); + } + return true; } diff --git a/lib/javascript-static.js b/lib/javascript-static.js index 9782cfa499666..a5bf70c16d190 100644 --- a/lib/javascript-static.js +++ b/lib/javascript-static.js @@ -1164,7 +1164,8 @@ function stripHTML(str) { throw new Error('stripHTML can not be used any more. Please use jQuery instead.'); } -function updateProgressBar(id, percent, msg, estimate) { +// eslint-disable-next-line no-unused-vars +function updateProgressBar(id, percent, msg, estimate, error) { var event, el = document.getElementById(id), eventData = {}; @@ -1176,6 +1177,7 @@ function updateProgressBar(id, percent, msg, estimate) { eventData.message = msg; eventData.percent = percent; eventData.estimate = estimate; + eventData.error = error; try { event = new CustomEvent('update', { diff --git a/lib/templates/progress_bar.mustache b/lib/templates/progress_bar.mustache index 82f9cfcb38dd1..acdd7d13d7df6 100644 --- a/lib/templates/progress_bar.mustache +++ b/lib/templates/progress_bar.mustache @@ -25,17 +25,17 @@ "width": "500" } }} -
+
-
+
-
 
+
 
-   - 0% +   + {{value}}%
@@ -43,21 +43,33 @@ {{! We must not use the JS helper otherwise this gets executed too late. }}