Merge branch 'MDL-72310-master' of git://github.com/aanabit/moodle
authorIlya Tregubov <ilya@moodle.com>
Wed, 15 Sep 2021 12:06:17 +0000 (14:06 +0200)
committerIlya Tregubov <ilya@moodle.com>
Wed, 15 Sep 2021 12:06:17 +0000 (14:06 +0200)
375 files changed:
admin/classes/local/entities/task_log.php
admin/qbankplugins.php
admin/settings/competency.php
admin/settings/messaging.php
admin/settings/subsystems.php
admin/tool/behat/cli/util.php
admin/tool/behat/cli/util_single_run.php
admin/tool/behat/lang/en/tool_behat.php
admin/tool/behat/tests/behat/data_generators.feature
admin/tool/brickfield/classes/local/htmlchecker/common/checks/content_too_long.php
admin/tool/brickfield/tests/local/htmlchecker/common/checks/content_too_long_test.php
admin/tool/capability/tests/behat/show_contexts.feature
admin/tool/componentlibrary/content/moodle/components/coursecards.md
admin/tool/componentlibrary/lang/en/tool_componentlibrary.php
admin/tool/lpimportcsv/settings.php
admin/tool/mobile/settings.php
admin/tool/moodlenet/settings.php
admin/tool/moodlenet/templates/view-cards.mustache
admin/tool/task/classes/check/cronrunning.php
admin/tool/task/lang/en/tool_task.php
analytics/tests/manager_test.php
analytics/tests/prediction_test.php
auth/db/auth.php
auth/db/tests/db_test.php
auth/ldap/auth.php
auth/ldap/lang/en/auth_ldap.php
auth/shibboleth/classes/helper.php
backup/cc/sheets/course_header.xml
backup/cc/sheets/moodle_blti_export.xml
backup/converter/moodle1/tests/fixtures/moodle.xml
backup/util/ui/backup_ui_setting.class.php
backup/util/ui/tests/base_setting_ui_test.php [new file with mode: 0644]
blocks/course_list/tests/behat/block_course_list_category.feature
blocks/course_list/tests/behat/block_course_list_course.feature
blocks/course_list/tests/behat/block_course_list_dashboard.feature
blocks/course_list/tests/behat/block_course_list_frontpage.feature
blocks/myoverview/templates/view-cards.mustache
blocks/myoverview/templates/view-list.mustache
blocks/myoverview/templates/view-summary.mustache
blocks/recentlyaccessedcourses/tests/behat/block_recentlyaccessedcourses_dashboard.feature
calendar/tests/behat/calendar_import.feature
calendar/tests/rrule_manager_test.php
cohort/tests/behat/upload_cohorts.feature
cohort/tests/fixtures/uploadcohorts1.csv
cohort/tests/fixtures/uploadcohorts2.csv
cohort/tests/fixtures/uploadcohorts4.csv
cohort/tests/fixtures/uploadcohorts_test.csv
completion/classes/cm_completion_details.php
config-dist.php
contentbank/contenttype/h5p/classes/content.php
course/amd/build/actions.min.js
course/amd/build/actions.min.js.map
course/amd/src/actions.js
course/classes/cache/course_image.php
course/classes/category.php
course/format/amd/build/courseeditor.min.js.map
course/format/amd/build/local/content.min.js [new file with mode: 0644]
course/format/amd/build/local/content.min.js.map [new file with mode: 0644]
course/format/amd/build/local/courseeditor/courseeditor.min.js
course/format/amd/build/local/courseeditor/courseeditor.min.js.map
course/format/amd/build/local/courseeditor/dndcmitem.min.js [new file with mode: 0644]
course/format/amd/build/local/courseeditor/dndcmitem.min.js.map [new file with mode: 0644]
course/format/amd/build/local/courseeditor/dndsection.min.js [new file with mode: 0644]
course/format/amd/build/local/courseeditor/dndsection.min.js.map [new file with mode: 0644]
course/format/amd/build/local/courseeditor/dndsectionitem.min.js [new file with mode: 0644]
course/format/amd/build/local/courseeditor/dndsectionitem.min.js.map [new file with mode: 0644]
course/format/amd/build/local/courseeditor/exporter.min.js
course/format/amd/build/local/courseeditor/exporter.min.js.map
course/format/amd/build/local/courseeditor/mutations.min.js
course/format/amd/build/local/courseeditor/mutations.min.js.map
course/format/amd/build/local/courseindex/cm.min.js
course/format/amd/build/local/courseindex/cm.min.js.map
course/format/amd/build/local/courseindex/section.min.js [new file with mode: 0644]
course/format/amd/build/local/courseindex/section.min.js.map [new file with mode: 0644]
course/format/amd/build/local/courseindex/sectiontitle.min.js [new file with mode: 0644]
course/format/amd/build/local/courseindex/sectiontitle.min.js.map [new file with mode: 0644]
course/format/amd/src/courseeditor.js
course/format/amd/src/local/content.js [new file with mode: 0644]
course/format/amd/src/local/courseeditor/courseeditor.js
course/format/amd/src/local/courseeditor/dndcmitem.js [new file with mode: 0644]
course/format/amd/src/local/courseeditor/dndsection.js [new file with mode: 0644]
course/format/amd/src/local/courseeditor/dndsectionitem.js [new file with mode: 0644]
course/format/amd/src/local/courseeditor/exporter.js
course/format/amd/src/local/courseeditor/mutations.js
course/format/amd/src/local/courseindex/cm.js
course/format/amd/src/local/courseindex/section.js [new file with mode: 0644]
course/format/amd/src/local/courseindex/sectiontitle.js [new file with mode: 0644]
course/format/classes/base.php
course/format/classes/output/local/state/section.php
course/format/classes/stateactions.php
course/format/templates/local/content.mustache
course/format/templates/local/content/section.mustache
course/format/templates/local/content/section/cmitem.mustache
course/format/templates/local/content/section/cmlist.mustache
course/format/templates/local/content/section/header.mustache
course/format/templates/local/courseindex/cm.mustache
course/format/templates/local/courseindex/section.mustache
course/format/topics/lib.php
course/format/upgrade.txt
course/format/weeks/lib.php
course/lib.php
course/templates/coursecards.mustache
course/templates/view-cards.mustache
course/tests/behat/behat_course.php
course/tests/behat/category_management.feature
course/tests/behat/course_category_management_listing.feature
course/tests/behat/course_creation.feature
course/tests/behat/course_request.feature
course/tests/behat/frontpage_display_modes.feature
course/tests/behat/navigate_course_list.feature
course/tests/category_test.php
customfield/tests/behat/edit_fields_settings.feature
enrol/manual/manage.php
enrol/manual/tests/behat/manage.feature [new file with mode: 0644]
files/tests/externallib_test.php
filter/tex/latex.php
filter/tex/lib.php
filter/tex/tests/lib_test.php [new file with mode: 0644]
filter/tex/texdebug.php
grade/import/direct/classes/import_form.php
group/clientlib.js
group/index.php
group/templates/index.mustache
install/lang/pt/install.php
install/lang/sv/install.php
lang/en/admin.php
lang/en/backup.php
lang/en/badges.php
lang/en/calendar.php
lang/en/completion.php
lang/en/grades.php
lang/en/h5p.php
lang/en/hub.php
lang/en/moodle.php
lib/accesslib.php
lib/amd/build/inplace_editable.min.js
lib/amd/build/inplace_editable.min.js.map
lib/amd/build/local/reactive/dragdrop.min.js [new file with mode: 0644]
lib/amd/build/local/reactive/dragdrop.min.js.map [new file with mode: 0644]
lib/amd/build/reactive.min.js
lib/amd/build/reactive.min.js.map
lib/amd/src/inplace_editable.js
lib/amd/src/local/reactive/dragdrop.js [new file with mode: 0644]
lib/amd/src/reactive.js
lib/behat/classes/behat_command.php
lib/behat/lib.php
lib/classes/component.php
lib/classes/event/message_user_blocked.php
lib/classes/event/message_user_unblocked.php
lib/classes/event/question_base.php
lib/classes/event/question_category_base.php
lib/classes/event/question_moved.php
lib/classes/event/questions_exported.php
lib/classes/event/questions_imported.php
lib/classes/plugin_manager.php
lib/completionlib.php
lib/db/install.php
lib/db/upgrade.php
lib/dml/pgsql_native_moodle_database.php
lib/dml/pgsql_native_moodle_recordset.php
lib/dml/tests/dml_test.php
lib/dml/tests/pgsql_native_moodle_database_test.php
lib/dml/tests/pgsql_native_recordset_test.php
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-debug.js
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-min.js
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button.js
lib/editor/atto/plugins/table/yui/src/button/js/button.js
lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-min.js
lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin.js
lib/editor/atto/yui/src/editor/js/editor-plugin-buttons.js
lib/form/dndupload.js
lib/form/questioncategory.php
lib/jabber/XMPP/BOSH.php [deleted file]
lib/jabber/XMPP/Exception.php [deleted file]
lib/jabber/XMPP/Log.php [deleted file]
lib/jabber/XMPP/Roster.php [deleted file]
lib/jabber/XMPP/XMLObj.php [deleted file]
lib/jabber/XMPP/XMLStream.php [deleted file]
lib/jabber/XMPP/XMPP.php [deleted file]
lib/jabber/readme_moodle.txt [deleted file]
lib/moodlelib.php
lib/phpunit/bootstrap.php
lib/questionlib.php
lib/setup.php
lib/setuplib.php
lib/tests/behat_lib_test.php
lib/tests/questionlib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js
lib/yui/src/notification/js/dialogue.js
message/amd/build/preferences_notifications_list_controller.min.js
message/amd/build/preferences_notifications_list_controller.min.js.map
message/amd/src/preferences_notifications_list_controller.js
message/output/jabber/classes/privacy/provider.php [deleted file]
message/output/jabber/db/upgrade.php [deleted file]
message/output/jabber/lang/en/message_jabber.php [deleted file]
message/output/jabber/message_output_jabber.php [deleted file]
message/output/jabber/settings.php [deleted file]
message/output/jabber/tests/privacy_test.php [deleted file]
message/tests/behat/message_manage_notification_preferences.feature
message/upgrade.txt
mnet/service/enrol/tests/privacy_test.php
mod/forum/lang/en/deprecated.txt
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/templates/forum_post_email_htmlemail_body.mustache
mod/forum/templates/forum_post_email_textemail.mustache
mod/forum/tests/behat/forum_subscriptions_management.feature
mod/forum/tests/behat/inpage_reply.feature
mod/forum/tests/lib_test.php
mod/lesson/lang/en/lesson.php
mod/lti/locallib.php
mod/lti/tests/locallib_test.php
mod/quiz/addrandom.php
mod/quiz/addrandomform.php
mod/quiz/amd/build/add_question_modal_launcher.min.js
mod/quiz/amd/build/add_question_modal_launcher.min.js.map
mod/quiz/amd/build/add_random_question.min.js
mod/quiz/amd/build/add_random_question.min.js.map
mod/quiz/amd/src/add_question_modal_launcher.js
mod/quiz/amd/src/add_random_question.js
mod/quiz/classes/external.php
mod/quiz/classes/output/edit_renderer.php
mod/quiz/edit.php
mod/quiz/module.js
mod/quiz/styles.css
mod/quiz/templates/modal_add_random_question.mustache
mod/quiz/tests/external_test.php
privacy/tests/moodle_content_writer_test.php
question/amd/build/edit_tags.min.js
question/amd/build/edit_tags.min.js.map
question/amd/build/repository.min.js
question/amd/build/repository.min.js.map
question/amd/build/selectors.min.js
question/amd/build/selectors.min.js.map
question/amd/src/edit_tags.js
question/amd/src/repository.js
question/amd/src/selectors.js
question/bank/deletequestion/lang/en/qbank_deletequestion.php
question/bank/editquestion/lang/en/qbank_editquestion.php
question/bank/exportquestions/lang/en/qbank_exportquestions.php
question/bank/exporttoxml/classes/export_xml_action_column.php [new file with mode: 0644]
question/bank/exporttoxml/classes/helper.php [new file with mode: 0644]
question/bank/exporttoxml/classes/plugin_feature.php [new file with mode: 0644]
question/bank/exporttoxml/classes/privacy/provider.php [new file with mode: 0644]
question/bank/exporttoxml/exportone.php [new file with mode: 0644]
question/bank/exporttoxml/lang/en/qbank_exporttoxml.php [new file with mode: 0644]
question/bank/exporttoxml/tests/behat/export_to_xml_action.feature [new file with mode: 0644]
question/bank/exporttoxml/tests/helper_test.php [new file with mode: 0644]
question/bank/exporttoxml/version.php [moved from question/format/webct/version.php with 77% similarity]
question/bank/importquestions/lang/en/qbank_importquestions.php
question/bank/managecategories/category.php [moved from question/category.php with 81% similarity]
question/bank/managecategories/classes/form/question_category_edit_form.php [new file with mode: 0644]
question/bank/managecategories/classes/form/question_move_form.php [new file with mode: 0644]
question/bank/managecategories/classes/helper.php [new file with mode: 0644]
question/bank/managecategories/classes/navigation.php [new file with mode: 0644]
question/bank/managecategories/classes/plugin_feature.php [new file with mode: 0644]
question/bank/managecategories/classes/privacy/provider.php [moved from question/format/webct/classes/privacy/provider.php with 64% similarity]
question/bank/managecategories/classes/question_category_list.php [new file with mode: 0644]
question/bank/managecategories/classes/question_category_list_item.php [new file with mode: 0644]
question/bank/managecategories/classes/question_category_object.php [new file with mode: 0644]
question/bank/managecategories/lang/en/qbank_managecategories.php [new file with mode: 0644]
question/bank/managecategories/templates/listitem.mustache [new file with mode: 0644]
question/bank/managecategories/tests/behat/move_question_categories.feature [moved from question/tests/behat/move_question_categories.feature with 100% similarity]
question/bank/managecategories/tests/behat/question_categories.feature [new file with mode: 0644]
question/bank/managecategories/tests/behat/question_categories_idnumber.feature [new file with mode: 0644]
question/bank/managecategories/tests/behat/view_manage_categories_plugin.feature [new file with mode: 0644]
question/bank/managecategories/tests/helper_test.php [new file with mode: 0644]
question/bank/managecategories/tests/question_category_object_test.php [new file with mode: 0644]
question/bank/managecategories/version.php [new file with mode: 0644]
question/bank/previewquestion/amd/build/preview.min.js [new file with mode: 0644]
question/bank/previewquestion/amd/build/preview.min.js.map [new file with mode: 0644]
question/bank/previewquestion/amd/src/preview.js [new file with mode: 0644]
question/bank/previewquestion/classes/form/preview_options_form.php [new file with mode: 0644]
question/bank/previewquestion/classes/helper.php [new file with mode: 0644]
question/bank/previewquestion/classes/output/renderer.php [new file with mode: 0644]
question/bank/previewquestion/classes/plugin_feature.php [new file with mode: 0644]
question/bank/previewquestion/classes/preview_action_column.php [new file with mode: 0644]
question/bank/previewquestion/classes/privacy/provider.php [new file with mode: 0644]
question/bank/previewquestion/classes/question_preview_options.php [new file with mode: 0644]
question/bank/previewquestion/lang/en/qbank_previewquestion.php [new file with mode: 0644]
question/bank/previewquestion/preview.php [new file with mode: 0644]
question/bank/previewquestion/templates/preview_question.mustache [new file with mode: 0644]
question/bank/previewquestion/tests/behat/preview_question.feature [moved from question/tests/behat/preview_question.feature with 93% similarity]
question/bank/previewquestion/tests/behat/preview_question_action.feature [new file with mode: 0644]
question/bank/previewquestion/tests/helper_test.php [new file with mode: 0644]
question/bank/previewquestion/version.php [moved from message/output/jabber/version.php with 68% similarity]
question/bank/tagquestion/amd/build/edit_tags.min.js [new file with mode: 0644]
question/bank/tagquestion/amd/build/edit_tags.min.js.map [new file with mode: 0644]
question/bank/tagquestion/amd/build/repository.min.js [new file with mode: 0644]
question/bank/tagquestion/amd/build/repository.min.js.map [new file with mode: 0644]
question/bank/tagquestion/amd/build/selectors.min.js [new file with mode: 0644]
question/bank/tagquestion/amd/build/selectors.min.js.map [new file with mode: 0644]
question/bank/tagquestion/amd/src/edit_tags.js [new file with mode: 0644]
question/bank/tagquestion/amd/src/repository.js [new file with mode: 0644]
question/bank/tagquestion/amd/src/selectors.js [new file with mode: 0644]
question/bank/tagquestion/classes/external/submit_tags.php [new file with mode: 0644]
question/bank/tagquestion/classes/form/tags_form.php [new file with mode: 0644]
question/bank/tagquestion/classes/plugin_feature.php [new file with mode: 0644]
question/bank/tagquestion/classes/privacy/provider.php [new file with mode: 0644]
question/bank/tagquestion/classes/tags_action_column.php [new file with mode: 0644]
question/bank/tagquestion/db/services.php [new file with mode: 0644]
question/bank/tagquestion/lang/en/qbank_tagquestion.php [new file with mode: 0644]
question/bank/tagquestion/lib.php [new file with mode: 0644]
question/bank/tagquestion/tests/behat/tag_question_action.feature [new file with mode: 0644]
question/bank/tagquestion/tests/external/submit_tags_test.php [new file with mode: 0644]
question/bank/tagquestion/version.php [moved from message/output/jabber/db/install.php with 65% similarity]
question/category_class.php
question/category_form.php
question/classes/bank/export_xml_action_column.php
question/classes/bank/search/category_condition.php
question/classes/bank/tags_action_column.php
question/classes/external.php
question/classes/local/bank/view.php
question/editlib.php
question/engine/renderer.php
question/format.php
question/format/upgrade.txt
question/format/webct/format.php [deleted file]
question/format/webct/lang/en/qformat_webct.php [deleted file]
question/format/webct/tests/behat/import.feature [deleted file]
question/format/webct/tests/behat/importcalculated.feature [deleted file]
question/format/webct/tests/fixtures/sample_calculated_webct.txt [deleted file]
question/format/webct/tests/fixtures/sample_webct.txt [deleted file]
question/format/webct/tests/webctformat_test.php [deleted file]
question/lib.php
question/move_form.php
question/preview.php
question/previewlib.php
question/qengine.js
question/tests/behat/question_categories.feature
question/tests/behat/question_categories_idnumber.feature
question/tests/category_class_test.php [deleted file]
question/tests/events_test.php
question/tests/externallib_test.php
question/type/ddimageortext/tests/behat/preview.feature
question/type/ddmarker/tests/behat/preview.feature
question/type/ddwtos/tests/behat/preview.feature
question/type/description/tests/behat/preview.feature
question/type/edit_question_form.php
question/type/essay/tests/behat/max_file_size.feature
question/type/essay/tests/behat/preview.feature
question/type/gapselect/tests/behat/basic_test.feature
question/type/match/tests/behat/edit.feature
question/type/match/tests/behat/preview.feature
question/type/multichoice/tests/behat/preview.feature
question/type/numerical/tests/behat/preview.feature
question/type/shortanswer/tests/behat/edit.feature
question/type/shortanswer/tests/behat/preview.feature
question/type/tags_form.php
question/type/truefalse/tests/behat/preview.feature
question/upgrade.txt
question/yui/build/moodle-question-preview/moodle-question-preview-debug.js
question/yui/build/moodle-question-preview/moodle-question-preview-min.js
question/yui/build/moodle-question-preview/moodle-question-preview.js
question/yui/src/preview/js/preview.js
repository/contentbank/tests/behat/select_content.feature
repository/contentbank/tests/browser_test.php
repository/contentbank/tests/search_test.php
repository/nextcloud/tests/lib_test.php
theme/boost/scss/moodle/atto.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/courseindex.scss
theme/boost/scss/preset/default.scss
theme/boost/style/moodle.css
theme/classic/scss/preset/default.scss
theme/classic/style/moodle.css
user/action_redir.php
user/profile/field/social/upgradelib.php
user/selector/lib.php
version.php

index b55cc06..4f8136a 100644 (file)
@@ -147,7 +147,7 @@ class task_log extends base {
             ->set_type(column::TYPE_TIMESTAMP)
             ->add_field("{$tablealias}.timestart")
             ->set_is_sortable(true)
-            ->add_callback([format::class, 'userdate']);
+            ->add_callback([format::class, 'userdate'], get_string('strftimedatetimeshortaccurate', 'core_langconfig'));
 
         // Duration column.
         $columns[] = (new column(
index e3b71a1..e4a9613 100644 (file)
@@ -35,6 +35,7 @@ $PAGE->set_url('/admin/qbankplugins.php');
 $PAGE->set_context($syscontext);
 
 require_admin();
+require_sesskey();
 
 $return = new moodle_url('/admin/settings.php', ['section' => 'manageqbanks']);
 
index 1fffa73..3798953 100644 (file)
@@ -30,17 +30,15 @@ if (has_capability('moodle/site:config', $systemcontext)) {
     $parentname = 'competencies';
 
     // Settings page.
+    $iscompetencyenabled = get_config('core_competency', 'enabled');
     $settings = new admin_settingpage('competencysettings', new lang_string('competenciessettings', 'core_competency'),
-        'moodle/site:config', false);
-    $ADMIN->add($parentname, $settings);
+    'moodle/site:config', !$iscompetencyenabled);
+    if ($iscompetencyenabled) {
+        $ADMIN->add($parentname, $settings);
+    }
 
     // Load the full tree of settings.
     if ($ADMIN->fulltree) {
-        $setting = new admin_setting_configcheckbox('core_competency/enabled',
-            new lang_string('enablecompetencies', 'core_competency'),
-            new lang_string('enablecompetencies_desc', 'core_competency'), 1);
-        $settings->add($setting);
-
         $setting = new admin_setting_configcheckbox('core_competency/pushcourseratingstouserplans',
             new lang_string('pushcourseratingstouserplans', 'core_competency'),
             new lang_string('pushcourseratingstouserplans_desc', 'core_competency'), 1);
index 2221e72..50686e5 100644 (file)
 defined('MOODLE_INTERNAL') || die();
 
 if ($hassiteconfig) {
-    $temp = new admin_settingpage('messages', new lang_string('messagingssettings', 'admin'));
-    $temp->add(new admin_setting_configcheckbox('messaging',
-        new lang_string('messaging', 'admin'),
-        new lang_string('configmessaging', 'admin'),
-        1));
+    $temp = new admin_settingpage('messages',
+        new lang_string('messagingssettings', 'admin'),
+        'moodle/site:config',
+        empty($CFG->messaging)
+    );
+
     $temp->add(new admin_setting_configcheckbox('messagingallusers',
             new lang_string('messagingallusers', 'admin'),
             new lang_string('configmessagingallusers', 'admin'),
index 01de61c..10e06c6 100644 (file)
@@ -52,6 +52,18 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $optionalsubsystems->add(new admin_setting_configcheckbox('enableanalytics', new lang_string('enableanalytics', 'admin'),
         new lang_string('configenableanalytics', 'admin'), 1, 1, 0));
 
+    $optionalsubsystems->add(new admin_setting_configcheckbox('core_competency/enabled',
+        new lang_string('enablecompetencies', 'core_competency'),
+        new lang_string('enablecompetencies_desc', 'core_competency'),
+        1)
+    );
+
+    $optionalsubsystems->add(new admin_setting_configcheckbox('messaging',
+        new lang_string('messaging', 'admin'),
+        new lang_string('configmessaging', 'admin'),
+        1)
+    );
+
     $fullunicodesupport = true;
     if ($DB->get_dbfamily() == 'mysql') {
         $collation = $DB->get_dbcollation();
index e5f5ade..8b39c7c 100644 (file)
@@ -19,6 +19,9 @@
  *
  * All CLI utilities uses $CFG->behat_dataroot and $CFG->prefix_dataroot as
  * $CFG->dataroot and $CFG->prefix
+ * Same applies for $CFG->behat_dbname, $CFG->behat_dbuser, $CFG->behat_dbpass
+ * and $CFG->behat_dbhost. But if any of those is not defined $CFG->dbname,
+ * $CFG->dbuser, $CFG->dbpass and/or $CFG->dbhost will be used.
  *
  * @package    tool_behat
  * @copyright  2012 David Monllaó
index 868dd84..5727e5b 100644 (file)
@@ -19,6 +19,9 @@
  *
  * All CLI utilities uses $CFG->behat_dataroot and $CFG->prefix_dataroot as
  * $CFG->dataroot and $CFG->prefix
+ * Same applies for $CFG->behat_dbname, $CFG->behat_dbuser, $CFG->behat_dbpass
+ * and $CFG->behat_dbhost. But if any of those is not defined $CFG->dbname,
+ * $CFG->dbuser, $CFG->dbpass and/or $CFG->dbhost will be used.
  *
  * @package    tool_behat
  * @copyright  2012 David Monllaó
index 01c607d..f78896a 100644 (file)
@@ -29,7 +29,7 @@ $string['errorbehatcommand'] = 'Error running behat CLI command. Try running "{$
 $string['errorcomposer'] = 'Composer dependencies are not installed.';
 $string['errordataroot'] = '$CFG->behat_dataroot is not set or is invalid.';
 $string['errorsetconfig'] = '$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot need to be set in config.php.';
-$string['erroruniqueconfig'] = '$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot values need to be different than $CFG->dataroot, $CFG->prefix, $CFG->wwwroot, $CFG->phpunit_dataroot and $CFG->phpunit_prefix values.';
+$string['erroruniqueconfig'] = '$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot values need to be different than $CFG->dataroot, $CFG->prefix, $CFG->wwwroot, $CFG->phpunit_dataroot and $CFG->phpunit_prefix values.<br/>Or, if $CFG->behat_prefix is the same, $CFG->behat_dbname or $CFG->behat_dbhost need to be different from $CFG->phpunit_dbname and $CFG->phpunit_dbhost and from $CFG->dbname and $CFG->dbhost.';
 $string['fieldvalueargument'] = 'Field value arguments';
 $string['fieldvalueargument_help'] = 'This argument should be completed by a field value. There are many field types, including simple ones like checkboxes, selects or textareas, or complex ones like date selectors. See the dev documentation <a href="https://docs.moodle.org/dev/Acceptance_testing" target="_blank">Acceptance_testing</a> for details of expected field values.';
 $string['giveninfo'] = 'Given. Processes to set up the environment';
index 9423a39..17b94f5 100644 (file)
@@ -40,7 +40,7 @@ Feature: Set up contextual data for tests
     And I should see "Course 2"
     And I follow "Cat 2"
     And I should see "No courses in this category"
-    And I follow "Miscellaneous"
+    And I follow "Category 1"
     And I should see "Course 3"
 
   @javascript
index f05ac7b..0c56a46 100644 (file)
@@ -40,14 +40,15 @@ class content_too_long extends brickfield_accessibility_test {
 
         $contentlengthlimit = 500;
         $pagetext = '';
-        foreach ($this->get_all_elements(null, 'text') as $element) {
-            $text = $element->nodeValue;
+        // There will be only one, but array is returned anyway.
+        foreach ($this->get_all_elements('body') as $element) {
+            $text = $element->textContent;
             if ($text != null) {
                 $pagetext = $pagetext . $text;
             }
         }
 
-        $wordcount = str_word_count($pagetext);
+        $wordcount = count_words($pagetext);
         if ($wordcount > $contentlengthlimit) {
             $this->add_report(null, "<p id='wc'>Word Count: " . $wordcount . "</p>", false);
         }
index bd1fed8..a5a6702 100644 (file)
@@ -37,20 +37,15 @@ class content_too_long_test extends all_checks {
 
     /** @var string Html fail */
     private $htmlfail = <<<EOD
-    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN""http://www.w3.org/TR/REC-html40/loose.dtd">
-    <html lang="en">
-    <head>
-    <title>Content must not exceed a certain length</title>
-    </head>
-    <body>
-    <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent accumsan, ante varius viverra aliquam, dolor risus
+    <p><span>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent accumsan, ante varius viverra aliquam, dolor risus
     scelerisque massa, ut lacinia ipsum felis id est. Nullam convallis odio ante, in commodo elit fermentum sed. Vivamus ullamcorper
     tincidunt sagittis. Sed et semper sapien. Quisque malesuada lacus nec libero cursus, aliquam malesuada neque ultricies. Cras sit
     amet enim vel orci tristique porttitor a vitae urna. Suspendisse mi leo, hendrerit et eleifend a, mollis at ex. Maecenas eget
     magna nec sem dignissim pharetra vel nec ex. Donec in porta lectus. Aenean porttitor euismod lectus, sodales eleifend ex egestas
-    in. Donec sed metus sodales, lobortis velit quis, dictum arcu.
-    Praesent mollis urna eget odio cursus, sit amet sollicitudin ante aliquam. Integer nec massa nec ipsum tincidunt laoreet in
-    vitae metus. Integer massa lacus, elementum quis dui sed, eleifend fringilla turpis. In hac habitasse platea dictumst. Phasellus
+    in. Donec sed metus sodales, lobortis velit quis, dictum arcu.</span></p>
+    <p><span>Praesent mollis urna eget odio cursus, sit amet sollicitudin ante aliquam. Integer nec massa nec ipsum tincidunt
+    laoreet in vitae metus.
+    Integer massa lacus, elementum quis dui sed, eleifend fringilla turpis. In hac habitasse platea dictumst. Phasellus
     efficitur quis felis non eleifend. Sed et mauris vel lorem ultrices porta. Mauris commodo condimentum felis, vel dictum ex
     laoreet sit amet. Duis venenatis ut lacus non ultrices. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per
     inceptos himenaeos. Nam nunc magna, semper feugiat feugiat a, pellentesque vel nulla.
@@ -59,9 +54,10 @@ class content_too_long_test extends all_checks {
     blandit eget elit sit amet, suscipit mollis ligula. Suspendisse rutrum sem ex, eu commodo nisi aliquam sit amet. Fusce ut felis
     justo. Sed a quam at lectus consectetur vulputate. Proin elementum dui nisi, in condimentum diam porttitor eget. Donec vehicula
     condimentum velit vel semper. Mauris vehicula tortor lectus, quis convallis erat aliquet vel. In dictum nunc ac posuere porta.
-    Sed vel leo aliquam, volutpat ligula ac, blandit diam. Donec nec ligula lacus.
-    Mauris ac libero vel ex fringilla fringilla. Ut vehicula justo eu nunc imperdiet ultricies. Sed interdum ligula at nisi rhoncus
-    auctor. Sed tempus tellus eget risus placerat, et viverra dolor gravida. Sed ultricies neque id ex tempor viverra. Ut imperdiet
+    Sed vel leo aliquam, volutpat ligula ac, blandit diam. Donec nec ligula lacus.</span></p>
+    <p><span>Mauris ac libero vel ex fringilla fringilla. Ut vehicula justo eu nunc imperdiet ultricies. Sed interdum ligula at nisi
+    rhoncus auctor.
+    Sed tempus tellus eget risus placerat, et viverra dolor gravida. Sed ultricies neque id ex tempor viverra. Ut imperdiet
     pharetra magna sed tristique. Pellentesque blandit elit ac neque lacinia finibus. Lorem ipsum dolor sit amet, consectetur
     adipiscing elit. Donec vel auctor dolor. Morbi id elit mollis ante mattis semper eu ac lectus. Integer elit turpis, facilisis
     vel metus eget, blandit tempus arcu. Pellentesque eget magna eu ex eleifend tincidunt. Curabitur sit amet congue nisi.
@@ -69,29 +65,60 @@ class content_too_long_test extends all_checks {
     turpis. Aenean tincidunt tristique dui, pretium lacinia felis posuere vel. Donec massa ligula, luctus vitae enim nec, sagittis
     hendrerit lorem. In consequat sodales metus vel porttitor. Aenean fringilla fringilla risus, vitae interdum turpis egestas quis.
     Aenean volutpat arcu leo, ut dictum purus consectetur id. Cras enim ipsum, tincidunt vitae mi vel, varius convallis ex. Fusce
-    pretium porttitor tempus.
-    Morbi laoreet dapibus lectus ut efficitur. Donec at hendrerit nunc. Vivamus venenatis augue non nulla finibus vestibulum. Nam
+    pretium porttitor tempus.</span></p>
+    <p>Morbi laoreet dapibus lectus ut efficitur. Donec at hendrerit nunc. Vivamus venenatis augue non nulla finibus vestibulum. Nam
     nunc magna, hendrerit a ipsum nec, pulvinar imperdiet augue. Fusce vel metus maximus, mattis magna at, egestas enim. Suspendisse
     et nisl at enim mollis scelerisque. Duis ut ipsum vel turpis eleifend aliquet a a ante. Nam lacinia purus vulputate purus
     tincidunt, aliquet sagittis nisi sagittis. Pellentesque efficitur massa non ex sodales pretium. Cras convallis vitae ex et
     dignissim. Nunc suscipit bibendum aliquam. Maecenas interdum tellus varius, laoreet velit sed, ornare arcu. Nunc pulvinar
     elementum sem eget scelerisque. Duis volutpat tellus ut risus finibus, nec molestie erat fermentum
     </p>
-    </body>
-    </html>
+EOD;
+
+    /** @var string Multibyte html falure */
+    private $htmlfail2 = <<<EOD
+    <p><span>ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル ブルース カンベッル
+    </span></p>
 EOD;
 
     /** @var string Html pass */
     private $htmlpass = <<<EOD
-    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN""http://www.w3.org/TR/REC-html40/loose.dtd">
-    <html lang="en">
-    <head>
-    <title>Content must not exceed a certain length/title>
-    </head>
-    <body>
     <p>Nice and short text</p>
-    </body>
-    </html>
 EOD;
 
     /**
@@ -101,6 +128,9 @@ EOD;
         $results = $this->get_checker_results($this->htmlfail);
         $this->assertTrue($results[0]->message == '<p id=\'wc\'>Word Count: 578</p>');
 
+        $results = $this->get_checker_results($this->htmlfail2);
+        $this->assertTrue($results[0]->message == '<p id=\'wc\'>Word Count: 504</p>');
+
         $results = $this->get_checker_results($this->htmlpass);
         $this->assertEmpty($results);
     }
index f61ca9e..053f593 100644 (file)
@@ -22,7 +22,7 @@ Feature: Show capabilities for multiple contexts
       | Roles:      | Student               |
     And I click on "Get the overview" "button"
     Then I should see "Permissions in System"
-    And I should see "Permissions in Category: Miscellaneous"
+    And I should see "Permissions in Category: Category 1"
     And I should see "Permissions in Course: Course 1"
     And I should not see "Permissions in Course: Course 2"
 
@@ -32,7 +32,7 @@ Feature: Show capabilities for multiple contexts
       | Roles:      | Student               |
     And I click on "Get the overview" "button"
     Then I should see "Permissions in System"
-    And I should not see "Permissions in Category: Miscellaneous"
+    And I should not see "Permissions in Category: Category 1"
     And I should not see "Permissions in Course: Course 1"
     And I should not see "Permissions in Course: Course 2"
 
@@ -42,7 +42,7 @@ Feature: Show capabilities for multiple contexts
       | Roles:      | Student                                            |
     And I click on "Get the overview" "button"
     Then I should see "Permissions in System"
-    And I should see "Permissions in Category: Miscellaneous"
+    And I should see "Permissions in Category: Category 1"
     And I should see "Permissions in Course: Course 1"
     And I should not see "Permissions in Course: Course 2"
 
@@ -52,7 +52,7 @@ Feature: Show capabilities for multiple contexts
       | Roles:      | Student                                    |
     And I click on "Get the overview" "button"
     Then I should see "Permissions in System"
-    And I should see "Permissions in Category: Miscellaneous"
+    And I should see "Permissions in Category: Category 1"
     And I should see "Permissions in Course: Course 1"
     And I should not see "Permissions in Course: Course 2"
 
@@ -62,7 +62,7 @@ Feature: Show capabilities for multiple contexts
       | Roles:      | Student                                          |
     And I click on "Get the overview" "button"
     Then I should see "Permissions in System"
-    And I should not see "Permissions in Category: Miscellaneous"
+    And I should not see "Permissions in Category: Category 1"
     And I should not see "Permissions in Course: Course 1"
     And I should not see "Permissions in Course: Course 2"
 
@@ -72,7 +72,7 @@ Feature: Show capabilities for multiple contexts
       | Roles:      |                       |
     And I click on "Get the overview" "button"
     Then I should see "Permissions in System"
-    And I should see "Permissions in Category: Miscellaneous"
+    And I should see "Permissions in Category: Category 1"
     And I should see "Permissions in Course: Course 1"
     And I should not see "Permissions in Course: Course 2"
 
@@ -82,7 +82,7 @@ Feature: Show capabilities for multiple contexts
       | Roles:      |                     |
     And I click on "Get the overview" "button"
     Then I should see "Permissions in System"
-    And I should not see "Permissions in Category: Miscellaneous"
+    And I should not see "Permissions in Category: Category 1"
     And I should not see "Permissions in Course: Course 1"
     And I should not see "Permissions in Course: Course 2"
 
@@ -92,7 +92,7 @@ Feature: Show capabilities for multiple contexts
       | Roles:      |                                           |
     And I click on "Get the overview" "button"
     Then I should see "Permissions in System"
-    And I should see "Permissions in Category: Miscellaneous"
+    And I should see "Permissions in Category: Category 1"
     And I should see "Permissions in Course: Course 1"
     And I should see "Permissions in Course: Course 2"
 
@@ -103,7 +103,7 @@ Feature: Show capabilities for multiple contexts
     And I set the field "Show differences only" to "1"
     And I click on "Get the overview" "button"
     Then I should see "Permissions in System"
-    And I should see "Permissions in Category: Miscellaneous"
+    And I should see "Permissions in Category: Category 1"
     And I should see "There are no differences to show between selected roles in this context"
     And I should see "Permissions in Course: Course 1"
     And I should not see "Permissions in Course: Course 2"
@@ -116,7 +116,7 @@ Feature: Show capabilities for multiple contexts
     And I click on "Get the overview" "button"
     Then I should see "Permissions in System"
     And I should see "There are no differences to show between selected roles in this context"
-    And I should not see "Permissions in Category: Miscellaneous"
+    And I should not see "Permissions in Category: Category 1"
     And I should not see "Permissions in Course: Course 1"
     And I should not see "Permissions in Course: Course 2"
 
@@ -127,7 +127,7 @@ Feature: Show capabilities for multiple contexts
     And I set the field "Show differences only" to "1"
     And I click on "Get the overview" "button"
     Then I should see "Permissions in System"
-    And I should see "Permissions in Category: Miscellaneous"
+    And I should see "Permissions in Category: Category 1"
     And I should see "There are no differences to show between selected roles in this context"
     And I should see "Permissions in Course: Course 1"
     And I should not see "Permissions in Course: Course 2"
index c4d0e15..ca44930 100644 (file)
@@ -33,7 +33,7 @@ Course cards should always show
             "courseimage": "https://placekitten.com/300/500",
             "fullname": "Mathematics Year One",
             "isfavourite": true,
-            "coursecategory": "Miscellaneous",
+            "coursecategory": "Category 1",
             "showcoursecategory": true,
             "visible": true
         }
@@ -71,7 +71,7 @@ The example below show a deck of cards as used on the starred courses block
             "courseimage": "https://placekitten.com/300/500",
             "fullname": "Mathematics Year One",
             "isfavourite": true,
-            "coursecategory": "Miscellaneous",
+            "coursecategory": "Category 1",
             "showcoursecategory": true,
             "visible": true
         },
index 0863d22..111b744 100644 (file)
@@ -25,7 +25,7 @@
 $string['copied'] = 'Copied!';
 $string['copy'] = 'Copy';
 $string['copytoclipboard'] = 'Copy to clipboard';
-$string['installer'] = '<h3>Please setup this component library</h3>
+$string['installer'] = '<h3>Component library setup</h3>
     <p>Before you can see the content of the component library you will need to have shell access to your Moodle installation and be able to write to folder /admin/tool/componentlibrary and have npm installed on your Moodle server.</p>
     <p>If you meet these requirements you can navigate to your Moodle root folder and run:</p>
     <pre>$ npm install</pre>
@@ -33,7 +33,7 @@ $string['installer'] = '<h3>Please setup this component library</h3>
     <p>This will fetch all the required packages for building the component library docs.</p>
     <p>Once they are installed you can run:</p>
     <pre>$ grunt componentlibrary</pre>
-    <p>For more info see the README.md file in this plugin</p>';
+    <p>For more info see the README.md file in this plugin.</p>';
 $string['pluginname'] = 'UI Component library';
 $string['privacy:metadata'] = 'The Component library plugin does not store any personal data.';
 $string['showboth'] = 'Show with both';
index 8cc12da..c9c32c8 100644 (file)
  */
 defined('MOODLE_INTERNAL') || die;
 
-// Manage competency frameworks page.
-$temp = new admin_externalpage(
-    'toollpimportcsv',
-    get_string('pluginname', 'tool_lpimportcsv'),
-    new moodle_url('/admin/tool/lpimportcsv/index.php'),
-    'moodle/competency:competencymanage'
-);
-$ADMIN->add('competencies', $temp);
-// Export competency framework page.
-$temp = new admin_externalpage(
-    'toollpexportcsv',
-    get_string('exportnavlink', 'tool_lpimportcsv'),
-    new moodle_url('/admin/tool/lpimportcsv/export.php'),
-    'moodle/competency:competencymanage'
-);
-$ADMIN->add('competencies', $temp);
+if (get_config('core_competency', 'enabled')) {
+    // Manage competency frameworks page.
+    $temp = new admin_externalpage(
+        'toollpimportcsv',
+        get_string('pluginname', 'tool_lpimportcsv'),
+        new moodle_url('/admin/tool/lpimportcsv/index.php'),
+        'moodle/competency:competencymanage'
+    );
+    $ADMIN->add('competencies', $temp);
+    // Export competency framework page.
+    $temp = new admin_externalpage(
+        'toollpexportcsv',
+        get_string('exportnavlink', 'tool_lpimportcsv'),
+        new moodle_url('/admin/tool/lpimportcsv/export.php'),
+        'moodle/competency:competencymanage'
+    );
+    $ADMIN->add('competencies', $temp);
+}
 
 // No report settings.
 $settings = null;
index 029390c..4f0f17a 100644 (file)
@@ -30,21 +30,30 @@ use core_admin\local\settings\autocomplete;
 
 if ($hassiteconfig) {
 
-    $ADMIN->add('root', new admin_category('mobileapp', new lang_string('mobileapp', 'tool_mobile')), 'development');
-
-    $temp = new admin_settingpage('mobilesettings', new lang_string('mobilesettings', 'tool_mobile'), 'moodle/site:config', false);
-
     // We should wait to the installation to finish since we depend on some configuration values that are set once
     // the admin user profile is configured.
     if (!during_initial_install()) {
         $enablemobiledocurl = new moodle_url(get_docs_url('Enable_mobile_web_services'));
         $enablemobiledoclink = html_writer::link($enablemobiledocurl, new lang_string('documentation'));
         $default = is_https() ? 1 : 0;
-        $temp->add(new admin_setting_enablemobileservice('enablemobilewebservice',
+        $optionalsubsystems = $ADMIN->locate('optionalsubsystems');
+        $optionalsubsystems->add(new admin_setting_enablemobileservice('enablemobilewebservice',
                 new lang_string('enablemobilewebservice', 'admin'),
                 new lang_string('configenablemobilewebservice', 'admin', $enablemobiledoclink), $default));
     }
 
+    $ismobilewsdisabled = empty($CFG->enablemobilewebservice);
+    $ADMIN->add('root',
+        new admin_category('mobileapp', new lang_string('mobileapp', 'tool_mobile'), $ismobilewsdisabled),
+        'development'
+    );
+
+    $temp = new admin_settingpage('mobilesettings',
+        new lang_string('mobilesettings', 'tool_mobile'),
+        'moodle/site:config',
+        $ismobilewsdisabled
+    );
+
     $temp->add(new admin_setting_configtext('tool_mobile/apppolicy', new lang_string('apppolicy', 'tool_mobile'),
         new lang_string('apppolicy_help', 'tool_mobile'), '', PARAM_URL));
 
@@ -60,8 +69,8 @@ if ($hassiteconfig) {
         $featuresnotice = $OUTPUT->render($notify);
     }
 
-    $hideappsubscription = empty($CFG->enablemobilewebservice);
-    $hideappsubscription = $hideappsubscription || (isset($CFG->disablemobileappsubscription) && !empty($CFG->disablemobileappsubscription));
+    $hideappsubscription = (isset($CFG->disablemobileappsubscription) && !empty($CFG->disablemobileappsubscription));
+    $hideappsubscription = $ismobilewsdisabled || $hideappsubscription;
 
     $ADMIN->add(
         'mobileapp',
@@ -79,7 +88,7 @@ if ($hassiteconfig) {
         'mobileauthentication',
         new lang_string('mobileauthentication', 'tool_mobile'),
         'moodle/site:config',
-        empty($CFG->enablemobilewebservice)
+        $ismobilewsdisabled
     );
 
     $temp->add(new admin_setting_heading('tool_mobile/moodleappsportalfeaturesauth', '', $featuresnotice));
@@ -123,7 +132,7 @@ if ($hassiteconfig) {
         'mobileappearance',
         new lang_string('mobileappearance', 'tool_mobile'),
         'moodle/site:config',
-        empty($CFG->enablemobilewebservice)
+        $ismobilewsdisabled
     );
 
     if (!empty($featuresnotice)) {
@@ -164,7 +173,7 @@ if ($hassiteconfig) {
         'mobilefeatures',
         new lang_string('mobilefeatures', 'tool_mobile'),
         'moodle/site:config',
-        empty($CFG->enablemobilewebservice)
+        $ismobilewsdisabled
     );
 
     if (!empty($featuresnotice)) {
index 1767b24..1eaad78 100644 (file)
 defined('MOODLE_INTERNAL') || die();
 
 if ($hassiteconfig) {
+    // Add an enable subsystem setting to the "Advanced features" settings page.
+    $optionalsubsystems = $ADMIN->locate('optionalsubsystems');
+    $optionalsubsystems->add(new admin_setting_configcheckbox('tool_moodlenet/enablemoodlenet',
+        new lang_string('enablemoodlenet', 'tool_moodlenet'),
+        new lang_string('enablemoodlenet_desc', 'tool_moodlenet'),
+        0, 1, 0)
+    );
+
     // Create a MoodleNet category.
-    $ADMIN->add('root', new admin_category('moodlenet', get_string('pluginname', 'tool_moodlenet')));
-    // Our settings page.
-    $settings = new admin_settingpage('tool_moodlenet', get_string('moodlenetsettings', 'tool_moodlenet'));
-    $ADMIN->add('moodlenet', $settings);
+    if (get_config('tool_moodlenet', 'enablemoodlenet')) {
+        $ADMIN->add('root', new admin_category('moodlenet', get_string('pluginname', 'tool_moodlenet')));
+        // Our settings page.
+        $settings = new admin_settingpage('tool_moodlenet', get_string('moodlenetsettings', 'tool_moodlenet'));
+        $ADMIN->add('moodlenet', $settings);
 
-    $temp = new admin_setting_configcheckbox('tool_moodlenet/enablemoodlenet', get_string('enablemoodlenet', 'tool_moodlenet'),
-        new lang_string('enablemoodlenet_desc', 'tool_moodlenet'), 0, 1, 0);
-    $settings->add($temp);
+        $temp = new admin_setting_configtext('tool_moodlenet/defaultmoodlenetname',
+            get_string('defaultmoodlenetname', 'tool_moodlenet'), new lang_string('defaultmoodlenetname_desc', 'tool_moodlenet'),
+            new lang_string('defaultmoodlenetnamevalue', 'tool_moodlenet'));
+        $settings->add($temp);
 
-    $temp = new admin_setting_configtext('tool_moodlenet/defaultmoodlenetname',
-        get_string('defaultmoodlenetname', 'tool_moodlenet'), new lang_string('defaultmoodlenetname_desc', 'tool_moodlenet'),
-        new lang_string('defaultmoodlenetnamevalue', 'tool_moodlenet'));
-    $settings->add($temp);
+        $temp = new admin_setting_configtext('tool_moodlenet/defaultmoodlenet', get_string('defaultmoodlenet', 'tool_moodlenet'),
+            new lang_string('defaultmoodlenet_desc', 'tool_moodlenet'), 'https://moodle.net');
+        $settings->add($temp);
 
-    $temp = new admin_setting_configtext('tool_moodlenet/defaultmoodlenet', get_string('defaultmoodlenet', 'tool_moodlenet'),
-        new lang_string('defaultmoodlenet_desc', 'tool_moodlenet'), 'https://moodle.net');
-    $settings->add($temp);
+    }
 }
index da742ff..31820a6 100644 (file)
@@ -27,7 +27,7 @@
                 "viewurl": "https://moodlesite/course/view.php?id=2",
                 "courseimage": "https://moodlesite/pluginfile/123/course/overviewfiles/123.jpg",
                 "fullname": "course 3",
-                "coursecategory": "Miscellaneous",
+                "coursecategory": "Category 1",
                 "visible": true
             }
         ]
index 2efcee1..f9005fa 100644 (file)
@@ -71,7 +71,8 @@ class cronrunning extends check {
         $formatexpected = format_time($expectedfrequency);
         $formatinterval = format_time($lastcroninterval);
 
-        $details = format_time($delta);
+        // Inform user the time since last cron start.
+        $details = get_string('lastcronstart', 'tool_task', $formatdelta);
 
         if ($delta > $expectedfrequency + MINSECS) {
             $status = result::WARNING;
index 8532fff..884a106 100644 (file)
@@ -51,6 +51,7 @@ $string['enablerunnow_desc'] = 'Allows administrators to run a single scheduled
 $string['faildelay'] = 'Fail delay';
 $string['fromcomponent'] = 'From component: {$a}';
 $string['hostname'] = 'Host name';
+$string['lastcronstart'] = 'Time since last cron run: {$a}';
 $string['lastruntime'] = 'Last run';
 $string['lastupdated'] = 'Last updated {$a}.';
 $string['nextruntime'] = 'Next run';
index 582c457..846485b 100644 (file)
@@ -510,7 +510,11 @@ class analytics_manager_testcase extends advanced_testcase {
 
         $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'Course category'));
         $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'Course category 1'));
-        $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'Miscellaneous'));
+        $this->assertCount(
+            2,
+            \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT],
+            get_string('defaultcategoryname')
+        ));
         $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE], 'Test course 1'));
         $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE], 'Test course'));
     }
index 43af3a6..30b57e4 100644 (file)
@@ -135,7 +135,7 @@ class core_analytics_prediction_testcase extends advanced_testcase {
         $this->resetAfterTest(true);
         $this->setAdminuser();
 
-        $misc = $DB->get_record('course_categories', ['name' => 'Miscellaneous']);
+        $misc = $DB->get_record('course_categories', ['name' => get_string('defaultcategoryname')]);
         $miscctx = \context_coursecat::instance($misc->id);
 
         $category = $this->getDataGenerator()->create_category();
index 9d3cc18..96aad43 100644 (file)
@@ -131,11 +131,11 @@ class auth_plugin_db extends auth_plugin_base {
             $authdb->Close();
 
             if ($this->config->passtype === 'plaintext') {
-                return ($fromdb == $extpassword);
+                return ($fromdb === $extpassword);
             } else if ($this->config->passtype === 'md5') {
-                return (strtolower($fromdb) == md5($extpassword));
+                return (strtolower($fromdb) === md5($extpassword));
             } else if ($this->config->passtype === 'sha1') {
-                return (strtolower($fromdb) == sha1($extpassword));
+                return (strtolower($fromdb) === sha1($extpassword));
             } else if ($this->config->passtype === 'saltedcrypt') {
                 return password_verify($extpassword, $fromdb);
             } else {
index ce16358..3b9d726 100644 (file)
@@ -334,12 +334,28 @@ class auth_db_testcase extends advanced_testcase {
         $DB->update_record('auth_db_users', $user3);
         $this->assertTrue($auth->user_login('u3', 'heslo'));
 
+        // Test user created to see if the checking happens strictly.
+        $usermd5 = (object)['name' => 'usermd5', 'pass' => '0e462097431906509019562988736854'];
+        $usermd5->id = $DB->insert_record('auth_db_users', $usermd5);
+
+        // md5('240610708') === '0e462097431906509019562988736854'.
+        $this->assertTrue($auth->user_login('usermd5', '240610708'));
+        $this->assertFalse($auth->user_login('usermd5', 'QNKCDZO'));
+
         set_config('passtype', 'sh1', 'auth_db');
         $auth->config->passtype = 'sha1';
         $user3->pass = sha1('heslo');
         $DB->update_record('auth_db_users', $user3);
         $this->assertTrue($auth->user_login('u3', 'heslo'));
 
+        // Test user created to see if the checking happens strictly.
+        $usersha1 = (object)['name' => 'usersha1', 'pass' => '0e66507019969427134894567494305185566735'];
+        $usersha1->id = $DB->insert_record('auth_db_users', $usersha1);
+
+        // sha1('aaroZmOk') === '0e66507019969427134894567494305185566735'.
+        $this->assertTrue($auth->user_login('usersha1', 'aaroZmOk'));
+        $this->assertFalse($auth->user_login('usersha1', 'aaK1STfY'));
+
         set_config('passtype', 'saltedcrypt', 'auth_db');
         $auth->config->passtype = 'saltedcrypt';
         $user3->pass = password_hash('heslo', PASSWORD_BCRYPT);
index 62f4b6d..04ba8c9 100644 (file)
@@ -1242,18 +1242,18 @@ class auth_plugin_ldap extends auth_plugin_base {
                     empty($nuvalue) ? $nuvalue = array() : $nuvalue;
                     $ouvalue = core_text::convert($oldvalue, 'utf-8', $this->config->ldapencoding);
                     foreach ($ldapkeys as $ldapkey) {
-                        // Skip update if $ldapkey does not exist in LDAP.
-                        if (!isset($user_entry[$ldapkey][0])) {
-                            $success = false;
-                            error_log($this->errorlogtag.get_string('updateremfailfield', 'auth_ldap',
-                                                                     array('ldapkey' => $ldapkey,
-                                                                            'key' => $key,
-                                                                            'ouvalue' => $ouvalue,
-                                                                            'nuvalue' => $nuvalue)));
-                            continue;
+                        // If the field is empty in LDAP there are two options:
+                        // 1. We get the LDAP field using ldap_first_attribute.
+                        // 2. LDAP don't send the field using  ldap_first_attribute.
+                        // So, for option 1 we check the if the field is retrieve it.
+                        // And get the original value of field in LDAP if the field.
+                        // Otherwise, let value in blank and delegate the check in ldap_modify.
+                        if (isset($user_entry[$ldapkey][0])) {
+                            $ldapvalue = $user_entry[$ldapkey][0];
+                        } else {
+                            $ldapvalue = '';
                         }
 
-                        $ldapvalue = $user_entry[$ldapkey][0];
                         if (!$ambiguous) {
                             // Skip update if the values already match
                             if ($nuvalue !== $ldapvalue) {
index 4844518..3a233b8 100644 (file)
@@ -147,7 +147,6 @@ $string['start_tls'] = 'Use regular LDAP service (port 389) with TLS encryption'
 $string['start_tls_key'] = 'Use TLS';
 $string['updateremfail'] = 'Error updating LDAP record. Error code: {$a->errno}; Error string: {$a->errstring}<br/>Key ({$a->key}) - old moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\'';
 $string['updateremfailamb'] = 'Failed to update LDAP with ambiguous field {$a->key}; old moodle value: \'{$a->ouvalue}\', new value: \'{$a->nuvalue}\'';
-$string['updateremfailfield'] = 'Failed to update LDAP with non-existent field (\'{$a->ldapkey}\'). Key ({$a->key}) - old Moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\'';
 $string['updatepasserror'] = 'Error in user_update_password(). Error code: {$a->errno}; Error string: {$a->errstring}';
 $string['updatepasserrorexpire'] = 'Error in user_update_password() when reading password expiry time. Error code: {$a->errno}; Error string: {$a->errstring}';
 $string['updatepasserrorexpiregrace'] = 'Error in user_update_password() when modifying expiry time and/or grace logins. Error code: {$a->errno}; Error string: {$a->errstring}';
index 0e99d19..297146f 100644 (file)
@@ -93,13 +93,12 @@ class helper {
 
         foreach ($sessions as $session) {
             // Get user session from DB.
-            if (session_decode(base64_decode($session->sessdata))) {
-                if (isset($_SESSION['SESSION']) && isset($_SESSION['SESSION']->shibboleth_session_id)) {
-                    // If there is a match, kill the session.
-                    if ($_SESSION['SESSION']->shibboleth_session_id == trim($spsessionid)) {
-                        // Delete this user's sessions.
-                        \core\session\manager::kill_user_sessions($session->userid);
-                    }
+            $usersession = self::unserializesession(base64_decode($session->sessdata));
+            if (isset($usersession['SESSION']) && isset($usersession['SESSION']->shibboleth_session_id)) {
+                // If there is a match, kill the session.
+                if ($usersession['SESSION']->shibboleth_session_id == trim($spsessionid)) {
+                    // Delete this user's sessions.
+                    \core\session\manager::kill_user_sessions($session->userid);
                 }
             }
         }
index ed35f0a..69fa46c 100644 (file)
@@ -1,7 +1,7 @@
       <ID>1</ID>
       <CATEGORY>
         <ID>1</ID>
-        <NAME>Miscellaneous</NAME>
+        <NAME>Category 1</NAME>
       </CATEGORY>
       <PASSWORD></PASSWORD>
       <FULLNAME>[#course_name#]</FULLNAME>
index e87ff0c..ae4ba82 100644 (file)
       <ID>2</ID>
       <CATEGORY>
         <ID>1</ID>
-        <NAME>Miscellaneous</NAME>
+        <NAME>Category 1</NAME>
       </CATEGORY>
       <PASSWORD></PASSWORD>
       <FULLNAME>Course Fullname 101</FULLNAME>
index ff4679c..1d26478 100644 (file)
@@ -42,7 +42,7 @@
       <ID>33</ID>
       <CATEGORY>
         <ID>1</ID>
-        <NAME>Miscellaneous</NAME>
+        <NAME>Category 1</NAME>
       </CATEGORY>
       <PASSWORD></PASSWORD>
       <FULLNAME>Moodle 2.0 Test Restore</FULLNAME>
index 973b155..da74cfe 100644 (file)
@@ -146,11 +146,13 @@ class base_setting_ui {
      * @throws base_setting_ui_exception when the label is not valid.
      * @param string $label
      */
-    public function set_label($label) {
-        $label = (string)$label;
-        if ($label === '' || $label !== clean_param($label, PARAM_TEXT)) {
+    public function set_label(string $label) :void {
+        $label = clean_param($label, PARAM_CLEANHTML);
+
+        if ($label === '') {
             throw new base_setting_ui_exception('setting_invalid_ui_label');
         }
+
         $this->label = $label;
     }
 
diff --git a/backup/util/ui/tests/base_setting_ui_test.php b/backup/util/ui/tests/base_setting_ui_test.php
new file mode 100644 (file)
index 0000000..6aa3da6
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Tests for base_setting_ui class.
+ *
+ * @package   core_backup
+ * @copyright 2021 Université Rennes 2 {@link https://www.univ-rennes2.fr}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+require_once($CFG->dirroot.'/backup/util/settings/tests/settings_test.php');
+
+/**
+ * Tests for base_setting_ui class.
+ *
+ * @copyright 2021 Université Rennes 2 {@link https://www.univ-rennes2.fr}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class base_setting_ui_test extends advanced_testcase {
+    /**
+     * Tests set_label().
+     *
+     * @return void
+     */
+    public function test_set_label() {
+        $this->resetAfterTest();
+
+        $bs = new mock_base_setting('test', base_setting::IS_BOOLEAN);
+        $bsui = new base_setting_ui($bs);
+
+        // Should keep original text string.
+        $bsui->set_label('Section name');
+        $this->assertEquals('Section name', $bsui->get_label());
+
+        // Should keep original HTML string.
+        $bsui->set_label('<b>Section name</b>');
+        $this->assertEquals('<b>Section name</b>', $bsui->get_label());
+
+        // Should be converted to text string.
+        $bsui->set_label(123);
+        $this->assertSame('123', $bsui->get_label());
+
+        // Should raise an exception when label is empty.
+        try {
+            $bsui->set_label('');
+            $this->assertTrue(false, 'base_setting_ui_exception');
+        } catch (Exception $exception) {
+            $this->assertTrue($exception instanceof base_setting_ui_exception);
+            $this->assertEquals($exception->errorcode, 'setting_invalid_ui_label');
+        }
+    }
+}
index 3c03721..53ec6cc 100644 (file)
@@ -7,15 +7,15 @@ Feature: Enable the course_list block on a category page and view it's contents
   Background:
     Given the following "categories" exist:
       | name        | category | idnumber |
-      | Category 1  | 0        | CAT1     |
-      | Category 2  | 0        | CAT2     |
-      | Category 3  | CAT2     | CAT3     |
+      | Category A  | 0        | CATA     |
+      | Category B  | 0        | CATB     |
+      | Category C  | CATB     | CATC     |
     And the following "courses" exist:
       | fullname | shortname | category |
       | Course 1 | C1        | 0        |
-      | Course 2 | C2        | CAT1     |
-      | Course 3 | C3        | CAT2     |
-      | Course 4 | C4        | CAT3     |
+      | Course 2 | C2        | CATA     |
+      | Course 3 | C3        | CATB     |
+      | Course 4 | C4        | CATC     |
     And the following "users" exist:
       | username | firstname | lastname | email                |
       | teacher1 | Teacher   | First    | teacher1@example.com |
@@ -30,30 +30,30 @@ Feature: Enable the course_list block on a category page and view it's contents
     And I am on site homepage
     And I turn editing mode on
     And I am on course index
-    And I follow "Miscellaneous"
+    And I follow "Category 1"
     And I add the "Courses" block
     And I log out
     When I log in as "teacher1"
     And I am on course index
-    And I follow "Miscellaneous"
+    And I follow "Category 1"
     Then I should see "Course 1" in the "My courses" "block"
     And I should see "Course 2" in the "My courses" "block"
     And I should see "Course 3" in the "My courses" "block"
     And I should not see "Course 4" in the "My courses" "block"
     And I follow "All courses"
-    And I should see "Miscellaneous"
+    And I should see "Category 1"
 
   Scenario: Add the course list block on category page and navigate to another course
     Given I log in as "admin"
     And I am on site homepage
     And I turn editing mode on
     And I am on course index
-    And I follow "Miscellaneous"
+    And I follow "Category 1"
     And I add the "Courses" block
     And I log out
     When I log in as "teacher1"
     And I am on course index
-    And I follow "Miscellaneous"
+    And I follow "Category 1"
     Then I should see "Course 1" in the "My courses" "block"
     And I should see "Course 2" in the "My courses" "block"
     And I should see "Course 3" in the "My courses" "block"
@@ -66,13 +66,13 @@ Feature: Enable the course_list block on a category page and view it's contents
     And I am on site homepage
     And I turn editing mode on
     And I am on course index
-    And I follow "Miscellaneous"
+    And I follow "Category 1"
     When I add the "Courses" block
-    Then I should see "Miscellaneous" in the "Course categories" "block"
-    And I should see "Category 1" in the "Course categories" "block"
-    And I should see "Category 2" in the "Course categories" "block"
-    And I should not see "Category 3" in the "Course categories" "block"
+    Then I should see "Category 1" in the "Course categories" "block"
+    And I should see "Category A" in the "Course categories" "block"
+    And I should see "Category B" in the "Course categories" "block"
+    And I should not see "Category C" in the "Course categories" "block"
     And I should not see "Course 1" in the "Course categories" "block"
     And I should not see "Course 2" in the "Course categories" "block"
     And I follow "All courses"
-    And I should see "Miscellaneous"
+    And I should see "Category 1"
index 8a14d9e..e88161b 100644 (file)
@@ -7,15 +7,15 @@ Feature: Enable the course_list block on a course page and view it's contents
   Background:
     Given the following "categories" exist:
       | name        | category | idnumber |
-      | Category 1  | 0        | CAT1     |
-      | Category 2  | 0        | CAT2     |
-      | Category 3  | CAT2     | CAT3     |
+      | Category A  | 0        | CATA     |
+      | Category B  | 0        | CATB     |
+      | Category C  | CATB     | CATC     |
     And the following "courses" exist:
       | fullname | shortname | category |
       | Course 1 | C1        | 0        |
-      | Course 2 | C2        | CAT1     |
-      | Course 3 | C3        | CAT2     |
-      | Course 4 | C4        | CAT3     |
+      | Course 2 | C2        | CATA     |
+      | Course 3 | C3        | CATB     |
+      | Course 4 | C4        | CATC     |
     And the following "users" exist:
       | username | firstname | lastname | email                |
       | teacher1 | Teacher   | First    | teacher1@example.com |
@@ -34,7 +34,7 @@ Feature: Enable the course_list block on a course page and view it's contents
     And I should see "Course 3" in the "My courses" "block"
     And I should not see "Course 4" in the "My courses" "block"
     And I follow "All courses"
-    And I should see "Miscellaneous"
+    And I should see "Category 1"
 
   Scenario: Add the course list block on course page and navigate to another course
     Given I log in as "teacher1"
@@ -51,14 +51,14 @@ Feature: Enable the course_list block on a course page and view it's contents
     Given I log in as "admin"
     And I am on "Course 1" course homepage with editing mode on
     When I add the "Courses" block
-    Then I should see "Miscellaneous" in the "Course categories" "block"
-    And I should see "Category 1" in the "Course categories" "block"
-    And I should see "Category 2" in the "Course categories" "block"
-    And I should not see "Category 3" in the "Course categories" "block"
+    Then I should see "Category 1" in the "Course categories" "block"
+    And I should see "Category A" in the "Course categories" "block"
+    And I should see "Category B" in the "Course categories" "block"
+    And I should not see "Category C" in the "Course categories" "block"
     And I should not see "Course 1" in the "Course categories" "block"
     And I should not see "Course 2" in the "Course categories" "block"
     And I follow "All courses"
-    And I should see "Miscellaneous"
+    And I should see "Category 1"
 
   Scenario: View the course list block on course page with hide all courses link enabled
     Given the following config values are set as admin:
@@ -77,11 +77,11 @@ Feature: Enable the course_list block on a course page and view it's contents
     And I log in as "admin"
     And I am on "Course 1" course homepage with editing mode on
     When I add the "Courses" block
-    Then I should not see "Miscellaneous" in the "My courses" "block"
-    And I should not see "Category 1" in the "My courses" "block"
-    And I should not see "Category 2" in the "My courses" "block"
-    And I should not see "Category 3" in the "My courses" "block"
+    Then I should not see "Category 1" in the "My courses" "block"
+    And I should not see "Category A" in the "My courses" "block"
+    And I should not see "Category B" in the "My courses" "block"
+    And I should not see "Category C" in the "My courses" "block"
     And I should see "Course 1" in the "My courses" "block"
     And I should not see "Course 2" in the "My courses" "block"
     And I follow "All courses"
-    And I should see "Miscellaneous"
+    And I should see "Category 1"
index f31ae94..873208b 100644 (file)
@@ -7,15 +7,15 @@ Feature: Enable the course_list block on the dashboard and view it's contents
   Background:
     Given the following "categories" exist:
       | name        | category | idnumber |
-      | Category 1  | 0        | CAT1     |
-      | Category 2  | 0        | CAT2     |
-      | Category 3  | CAT2     | CAT3     |
+      | Category A  | 0        | CATA     |
+      | Category B  | 0        | CATB     |
+      | Category C  | CATB     | CATC     |
     And the following "courses" exist:
       | fullname | shortname | category |
       | Course 1 | C1        | 0        |
-      | Course 2 | C2        | CAT1     |
-      | Course 3 | C3        | CAT2     |
-      | Course 4 | C4        | CAT3     |
+      | Course 2 | C2        | CATA     |
+      | Course 3 | C3        | CATB     |
+      | Course 4 | C4        | CATC     |
     And the following "users" exist:
       | username | firstname | lastname | email                |
       | teacher1 | Teacher   | First    | teacher1@example.com |
@@ -34,7 +34,7 @@ Feature: Enable the course_list block on the dashboard and view it's contents
     And I should see "Course 3" in the "My courses" "block"
     And I should not see "Course 4" in the "My courses" "block"
     And I follow "All courses"
-    And I should see "Miscellaneous"
+    And I should see "Category 1"
 
   Scenario: Add the course list block on the dashboard and navigate to another course
     Given I log in as "teacher1"
@@ -51,11 +51,11 @@ Feature: Enable the course_list block on the dashboard and view it's contents
     Given I log in as "admin"
     And I press "Customise this page"
     When I add the "Courses" block
-    Then I should see "Miscellaneous" in the "Course categories" "block"
-    And I should see "Category 1" in the "Course categories" "block"
-    And I should see "Category 2" in the "Course categories" "block"
-    And I should not see "Category 3" in the "Course categories" "block"
+    Then I should see "Category 1" in the "Course categories" "block"
+    And I should see "Category A" in the "Course categories" "block"
+    And I should see "Category B" in the "Course categories" "block"
+    And I should not see "Category C" in the "Course categories" "block"
     And I should not see "Course 1" in the "Course categories" "block"
     And I should not see "Course 2" in the "Course categories" "block"
     And I follow "All courses"
-    And I should see "Miscellaneous"
+    And I should see "Category 1"
index 93dfa54..0a897ba 100644 (file)
@@ -7,15 +7,15 @@ Feature: Enable the course_list block on the frontpage and view it's contents
   Background:
     Given the following "categories" exist:
       | name        | category | idnumber |
-      | Category 1  | 0        | CAT1     |
-      | Category 2  | 0        | CAT2     |
-      | Category 3  | CAT2     | CAT3     |
+      | Category A  | 0        | CATA     |
+      | Category B  | 0        | CATB     |
+      | Category C  | CATB     | CATC     |
     And the following "courses" exist:
       | fullname | shortname | category |
       | Course 1 | C1        | 0        |
-      | Course 2 | C2        | CAT1     |
-      | Course 3 | C3        | CAT2     |
-      | Course 4 | C4        | CAT3     |
+      | Course 2 | C2        | CATA     |
+      | Course 3 | C3        | CATB     |
+      | Course 4 | C4        | CATC     |
     And the following "users" exist:
       | username | firstname | lastname | email                |
       | teacher1 | Teacher   | First    | teacher1@example.com |
@@ -38,7 +38,7 @@ Feature: Enable the course_list block on the frontpage and view it's contents
     And I should see "Course 3" in the "My courses" "block"
     And I should not see "Course 4" in the "My courses" "block"
     And I follow "All courses"
-    And I should see "Miscellaneous"
+    And I should see "Category 1"
 
   Scenario: Add the course list block on the frontpage page and navigate to another course
     Given I log in as "admin"
@@ -60,14 +60,14 @@ Feature: Enable the course_list block on the frontpage and view it's contents
     And I am on site homepage
     And I navigate to "Turn editing on" in current page administration
     When I add the "Courses" block
-    Then I should see "Miscellaneous" in the "Course categories" "block"
-    And I should see "Category 1" in the "Course categories" "block"
-    And I should see "Category 2" in the "Course categories" "block"
-    And I should not see "Category 3" in the "Course categories" "block"
+    Then I should see "Category 1" in the "Course categories" "block"
+    And I should see "Category A" in the "Course categories" "block"
+    And I should see "Category B" in the "Course categories" "block"
+    And I should not see "Category C" in the "Course categories" "block"
     And I should not see "Course 1" in the "Course categories" "block"
     And I should not see "Course 2" in the "Course categories" "block"
     And I follow "All courses"
-    And I should see "Miscellaneous"
+    And I should see "Category 1"
 
   Scenario: Add the course list block on the frontpage page and view as a guest
     Given I log in as "admin"
@@ -76,11 +76,11 @@ Feature: Enable the course_list block on the frontpage and view it's contents
     And I add the "Courses" block
     And I log out
     When I log in as "guest"
-    Then I should see "Miscellaneous" in the "Course categories" "block"
-    And I should see "Category 1" in the "Course categories" "block"
-    And I should see "Category 2" in the "Course categories" "block"
-    And I should not see "Category 3" in the "Course categories" "block"
+    Then I should see "Category 1" in the "Course categories" "block"
+    And I should see "Category A" in the "Course categories" "block"
+    And I should see "Category B" in the "Course categories" "block"
+    And I should not see "Category C" in the "Course categories" "block"
     And I should not see "Course 1" in the "Course categories" "block"
     And I should not see "Course 2" in the "Course categories" "block"
     And I follow "All courses"
-    And I should see "Miscellaneous"
+    And I should see "Category 1"
index 03196b9..c354a64 100644 (file)
@@ -29,7 +29,7 @@
                 "fullname": "course 3",
                 "hasprogress": true,
                 "progress": 10,
-                "coursecategory": "Miscellaneous",
+                "coursecategory": "Category 1",
                 "visible": true
             }
         ]
index 82ce492..38079f0 100644 (file)
@@ -29,7 +29,7 @@
                 "fullname": "course 3",
                 "hasprogress": true,
                 "progress": 10,
-                "coursecategory": "Miscellaneous",
+                "coursecategory": "Category 1",
                 "visible": true
             }
         ]
index aecd376..33a69ff 100644 (file)
@@ -30,7 +30,7 @@
                 "summary": "This course is about assignments",
                 "hasprogress": true,
                 "progress": 10,
-                "coursecategory": "Miscellaneous",
+                "coursecategory": "Category 1",
                 "visible": true
             }
         ]
index 731e1ef..5e9ae8c 100644 (file)
@@ -10,13 +10,13 @@ Feature: The recently accessed courses block allows users to easily access their
       | student1 | Student   | 1        | student1@example.com |
     And the following "categories" exist:
       | name        | category | idnumber |
-      | Category 1  | 0        | CAT1     |
+      | Category A  | 0        | CATA     |
     And the following "courses" exist:
       | fullname | shortname | category |
       | Course 1 | C1        | 0        |
       | Course 2 | C2        | 0        |
       | Course 3 | C3        | 0        |
-      | Course 4 | C4        | CAT1     |
+      | Course 4 | C4        | CATA     |
       | Course 5 | C5        | 0        |
     And the following "course enrolments" exist:
       | user     | course | role    |
@@ -51,8 +51,8 @@ Feature: The recently accessed courses block allows users to easily access their
     And I am on "Course 1" course homepage
     And I am on "Course 4" course homepage
     And I follow "Dashboard" in the user menu
-    And I should see "Miscellaneous" in the "Recently accessed courses" "block"
     And I should see "Category 1" in the "Recently accessed courses" "block"
+    And I should see "Category A" in the "Recently accessed courses" "block"
 
   Scenario: Hide course category name
     Given the following config values are set as admin:
@@ -61,8 +61,8 @@ Feature: The recently accessed courses block allows users to easily access their
     And I am on "Course 1" course homepage
     And I am on "Course 4" course homepage
     And I follow "Dashboard" in the user menu
-    And I should not see "Miscellaneous" in the "Recently accessed courses" "block"
     And I should not see "Category 1" in the "Recently accessed courses" "block"
+    And I should not see "Category A" in the "Recently accessed courses" "block"
 
   Scenario: Show short course name
     Given the following config values are set as admin:
index f495281..f42cdf3 100644 (file)
@@ -66,7 +66,7 @@ Feature: Import and edit calendar events
       | Calendar name  | Test Import |
       | Import from    | Calendar file (.ics) |
       | Type of event  | Category             |
-      | Category       | Miscellaneous   |
+      | Category       | Category 1           |
     And I upload "calendar/tests/fixtures/import.ics" file to "Calendar file (.ics)" filemanager
     And I press "Import calendar"
     And I should see "Category events"
index 9fbf46c..449a9fc 100644 (file)
@@ -2154,11 +2154,7 @@ class core_calendar_rrule_manager_testcase extends advanced_testcase {
         // Change our event's date to the 20th Monday of the current year.
         $twentiethmonday = new DateTime(date('Y-01-01'));
         $twentiethmonday->modify('+20 Monday');
-        $startdatetime = $this->change_event_startdate($twentiethmonday->format('Ymd\T090000'), 'US/Eastern');
-
-        $startdate = new DateTime($startdatetime->format('Y-m-d'));
-
-        $offset = $startdatetime->diff($startdate, true);
+        $startdatetime = $this->change_event_startdate($twentiethmonday->format('Ymd\T000000'), 'US/Eastern');
 
         $interval = new DateInterval('P1Y');
 
@@ -2183,7 +2179,6 @@ class core_calendar_rrule_manager_testcase extends advanced_testcase {
             $expecteddate->modify('January 1');
             $expecteddate->add($interval);
             $expecteddate->modify("+20 Monday");
-            $expecteddate->add($offset);
         }
     }
 
index c94e262..4143f5c 100644 (file)
@@ -22,7 +22,7 @@ Feature: A privileged user can create cohorts using a CSV file
       | name          | idnumber  | description       | Context       | visible | Status |
       | cohort name 1 | cohortid1 | first description | System        | 1       |        |
       | cohort name 2 | cohortid2 |                   | System        | 1       |        |
-      | cohort name 3 | cohortid3 |                   | Miscellaneous | 0       |        |
+      | cohort name 3 | cohortid3 |                   | Category 1    | 0       |        |
       | cohort name 4 | cohortid4 |                   | Cat 1         | 1       |        |
       | cohort name 5 | cohortid5 |                   | Cat 2         | 0       |        |
       | cohort name 6 | cohortid6 |                   | Cat 3         | 1       |        |
@@ -38,7 +38,7 @@ Feature: A privileged user can create cohorts using a CSV file
       | Category      | Name          | Cohort ID | Description       | Cohort size | Source           |
       | System        | cohort name 1 | cohortid1 | first description | 0           | Created manually |
       | System        | cohort name 2 | cohortid2 |                   | 0           | Created manually |
-      | Miscellaneous | cohort name 3 | cohortid3 |                   | 0           | Created manually |
+      | Category 1    | cohort name 3 | cohortid3 |                   | 0           | Created manually |
       | Cat 1         | cohort name 4 | cohortid4 |                   | 0           | Created manually |
       | Cat 2         | cohort name 5 | cohortid5 |                   | 0           | Created manually |
       | Cat 3         | cohort name 6 | cohortid6 |                   | 0           | Created manually |
@@ -62,7 +62,7 @@ Feature: A privileged user can create cohorts using a CSV file
       | name          | idnumber  | description       | Context                 | Status |
       | cohort name 1 | cohortid1 | first description | Cat 3         |        |
       | cohort name 2 | cohortid2 |                   | Cat 3         |        |
-      | cohort name 3 | cohortid3 |                   | Miscellaneous |        |
+      | cohort name 3 | cohortid3 |                   | Category 1    |        |
       | cohort name 4 | cohortid4 |                   | Cat 1         |        |
       | cohort name 5 | cohortid5 |                   | Cat 2         |        |
       | cohort name 6 | cohortid6 |                   | Cat 3         |        |
@@ -76,7 +76,7 @@ Feature: A privileged user can create cohorts using a CSV file
       | Category      | Name          | Cohort ID | Description       | Cohort size | Source           |
       | Cat 3         | cohort name 1 | cohortid1 | first description | 0           | Created manually |
       | Cat 3         | cohort name 2 | cohortid2 |                   | 0           | Created manually |
-      | Miscellaneous | cohort name 3 | cohortid3 |                   | 0           | Created manually |
+      | Category 1    | cohort name 3 | cohortid3 |                   | 0           | Created manually |
       | Cat 1         | cohort name 4 | cohortid4 |                   | 0           | Created manually |
       | Cat 2         | cohort name 5 | cohortid5 |                   | 0           | Created manually |
       | Cat 3         | cohort name 6 | cohortid6 |                   | 0           | Created manually |
@@ -100,7 +100,7 @@ Feature: A privileged user can create cohorts using a CSV file
       | name          | idnumber  | description       | Context | Status |
       | cohort name 1 | cohortid1 | first description | Cat 1   |        |
       | cohort name 2 | cohortid2 |                   | Cat 1   |        |
-      | cohort name 3 | cohortid3 |                   | Cat 1   | Category Miscellaneous not found or you don't have permission to create a cohort there. The default context will be used. |
+      | cohort name 3 | cohortid3 |                   | Cat 1   | Category Category 1 not found or you don't have permission to create a cohort there. The default context will be used. |
       | cohort name 4 | cohortid4 |                   | Cat 1   |        |
       | cohort name 5 | cohortid5 |                   | Cat 1   | Category CAT2 not found or you don't have permission to create a cohort there. The default context will be used. |
       | cohort name 6 | cohortid6 |                   | Cat 3   |        |
@@ -122,7 +122,7 @@ Feature: A privileged user can create cohorts using a CSV file
       | name | idnumber | description | Context | Status |
       | cohort name 1 | cohortid1 | first description | System |  |
       | cohort name 2 | cohortid2 |  | System | Cohort with the same ID number already exists |
-      | cohort name 3 | cohortid3 |  | Miscellaneous |  |
+      | cohort name 3 | cohortid3 |  | Category 1 |  |
       | cohort name 4 | cohortid4 |  | Cat 1 |  |
       | cohort name 5 | cohortid5 |  | Cat 2 |  |
       | cohort name 6 | cohortid6 |  | Cat 3 |  |
@@ -137,11 +137,11 @@ Feature: A privileged user can create cohorts using a CSV file
     And I click on "Preview" "button"
     Then the following should exist in the "previewuploadedcohorts" table:
       | name                         | idnumber  | description | Context       | Status |
-      | Specify category as name     | cohortid1 |             | Miscellaneous |        |
+      | Specify category as name     | cohortid1 |             | Category 1 |        |
       | Specify category as idnumber | cohortid2 |             | Cat 1         |        |
-      | Specify category as id       | cohortid3 |             | Miscellaneous |        |
+      | Specify category as id       | cohortid3 |             | Category 1 |        |
       | Specify category as path     | cohortid4 |             | Cat 3         |        |
-      | Specify category_id          | cohortid5 |             | Miscellaneous |        |
+      | Specify category_id          | cohortid5 |             | Category 1 |        |
       | Specify category_idnumber    | cohortid6 |             | Cat 1         |        |
       | Specify category_path        | cohortid7 |             | Cat 3         |        |
     And I should not see "not found or you"
@@ -173,7 +173,7 @@ Feature: A privileged user can create cohorts using a CSV file
       | name          | idnumber  | description       | Context       | visible | theme    | Status |
       | cohort name 1 | cohortid1 | first description | System        | 1       | boost    |        |
       | cohort name 2 | cohortid2 |                   | System        | 1       |          |        |
-      | cohort name 3 | cohortid3 |                   | Miscellaneous | 0       | boost    |        |
+      | cohort name 3 | cohortid3 |                   | Category 1    | 0       | boost    |        |
       | cohort name 4 | cohortid4 |                   | Cat 1         | 1       | classic  |        |
       | cohort name 5 | cohortid5 |                   | Cat 2         | 0       |          |        |
       | cohort name 6 | cohortid6 |                   | Cat 3         | 1       | classic  |        |
@@ -189,7 +189,7 @@ Feature: A privileged user can create cohorts using a CSV file
       | Category      | Name          | Cohort ID | Description       | Cohort size | Source           |
       | System        | cohort name 1 | cohortid1 | first description | 0           | Created manually |
       | System        | cohort name 2 | cohortid2 |                   | 0           | Created manually |
-      | Miscellaneous | cohort name 3 | cohortid3 |                   | 0           | Created manually |
+      | Category 1    | cohort name 3 | cohortid3 |                   | 0           | Created manually |
       | Cat 1         | cohort name 4 | cohortid4 |                   | 0           | Created manually |
       | Cat 2         | cohort name 5 | cohortid5 |                   | 0           | Created manually |
       | Cat 3         | cohort name 6 | cohortid6 |                   | 0           | Created manually |
index 8fe77ed..a1d0c41 100644 (file)
@@ -1,7 +1,7 @@
 name,idnumber,description,category,visible
 cohort name 1,cohortid1,first description,,
 cohort name 2,cohortid2,,,
-cohort name 3,cohortid3,,Miscellaneous,no
+cohort name 3,cohortid3,,Category 1,no
 cohort name 4,cohortid4,,CAT1,yes
 cohort name 5,cohortid5,,CAT2,0
 cohort name 6,cohortid6,,CAT3,1
index f2fd9f3..c3ff617 100644 (file)
@@ -1,5 +1,5 @@
 name,idnumber,description,category,category_id,category_idnumber,category_path
-Specify category as name,cohortid1,,Miscellaneous,,,
+Specify category as name,cohortid1,,Category 1,,,
 Specify category as idnumber,cohortid2,,CAT1,,,
 Specify category as id,cohortid3,,1,,,
 Specify category as path,cohortid4,,Cat 1 / Cat 3,,,
index 61dc609..9b773d0 100644 (file)
@@ -1,7 +1,7 @@
 name,idnumber,description,category,visible,theme
 cohort name 1,cohortid1,first description,,,boost
 cohort name 2,cohortid2,,,,
-cohort name 3,cohortid3,,Miscellaneous,no,boost
+cohort name 3,cohortid3,,Category 1,no,boost
 cohort name 4,cohortid4,,CAT1,yes,classic
 cohort name 5,cohortid5,,CAT2,0,
 cohort name 6,cohortid6,,CAT3,1,classic
index 567732c..fbae0b7 100644 (file)
@@ -1,7 +1,7 @@
 name,idnumber,description,category
 cohort name 1,cid1,first description,
 cohort name 2,cid2,,
-cohort name 3,cid3,,Miscellaneous
+cohort name 3,cid3,,Category 1
 cohort name 4,cid4,,CAT1
 cohort name 5,cid5,,CAT2
 cohort name 6,cid6,,CAT3
index 4c299ed..d081961 100644 (file)
@@ -65,7 +65,9 @@ class cm_completion_details {
      */
     public function __construct(completion_info $completioninfo, cm_info $cminfo, int $userid, bool $returndetails = true) {
         $this->completioninfo = $completioninfo;
-        $this->completiondata = $completioninfo->get_data($cminfo, false, $userid);
+        // We need to pass wholecourse = true here for better performance. All the course's completion data for the current
+        // logged-in user will get in a single query instead of multiple queries and loaded to cache.
+        $this->completiondata = $completioninfo->get_data($cminfo, true, $userid);
         $this->cminfo = $cminfo;
         $this->userid = $userid;
         $this->returndetails = $returndetails;
index 70af810..317fd95 100644 (file)
@@ -880,6 +880,10 @@ $CFG->admin = 'admin';
 // $CFG->behat_wwwroot = 'http://127.0.0.1/moodle';
 // $CFG->behat_prefix = 'bht_';
 // $CFG->behat_dataroot = '/home/example/bht_moodledata';
+// $CFG->behat_dbname = 'behat'; // optional
+// $CFG->behat_dbuser = 'username'; // optional
+// $CFG->behat_dbpass = 'password'; // optional
+// $CFG->behat_dbhost = 'localhost'; // optional
 //
 // You can override default Moodle configuration for Behat and add your own
 // params; here you can add more profiles, use different Mink drivers than Selenium...
index ea41ffb..e573a70 100644 (file)
@@ -42,6 +42,12 @@ class content extends \core_contentbank\content {
     public function is_view_allowed(): bool {
         // Force H5P content to be deployed.
         $fileurl = $this->get_file_url();
+        if (empty($fileurl)) {
+            // This should never happen because H5P contents should have always a file. However, this extra-checked has been added
+            // to avoid the contentbank stop working if, for any unkonwn/weird reason, the file doesn't exist.
+            return false;
+        }
+
         // Skip capability check when creating the H5P content (because it has been created by trusted users).
         $h5pplayer = new \core_h5p\player($fileurl, new \stdClass(), true, '', true);
         // Flush error messages.
index d64d4fe..1fca150 100644 (file)
Binary files a/course/amd/build/actions.min.js and b/course/amd/build/actions.min.js differ
index 4227116..d435aed 100644 (file)
Binary files a/course/amd/build/actions.min.js.map and b/course/amd/build/actions.min.js.map differ
index 54420c4..f7c62fa 100644 (file)
@@ -279,11 +279,12 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
          *
          * Used after d&d of the module to another section
          *
-         * @param {JQuery} activityElement
+         * @param {JQuery|Element} element
          * @param {Number} cmid
          * @param {Number} sectionreturn
          */
-        var refreshModule = function(activityElement, cmid, sectionreturn) {
+        var refreshModule = function(element, cmid, sectionreturn) {
+            const activityElement = $(element);
             var spinner = addActivitySpinner(activityElement);
             var promises = ajax.call([{
                 methodname: 'core_course_get_module',
@@ -766,6 +767,8 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
                 log.debug('replaceSectionActionItem() is deprecated and will be removed.');
                 var actionitem = sectionelement.find(SELECTOR.SECTIONACTIONMENU + ' ' + selector);
                 replaceActionItem(actionitem, image, stringname, stringcomponent, newaction);
-            }
+            },
+            // Method to refresh a module.
+            refreshModule,
         };
     });
index e01b31c..e609232 100644 (file)
@@ -56,7 +56,8 @@ class course_image implements cache_data_source {
      * @return string|bool Returns course image url as a string or false if the image is not exist
      */
     public function load_for_cache($key) {
-        $course = get_fast_modinfo($key)->get_course();
+        // We should use get_course() instead of get_fast_modinfo() for better performance.
+        $course = get_course($key);
         return $this->get_image_url_from_overview_files($course);
     }
 
index a92e6d2..1733d52 100644 (file)
@@ -823,7 +823,7 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
             // No categories found.
             // This may happen after upgrade of a very old moodle version.
             // In new versions the default category is created on install.
-            $defcoursecat = self::create(array('name' => get_string('miscellaneous')));
+            $defcoursecat = self::create(array('name' => get_string('defaultcategoryname')));
             set_config('defaultrequestcategory', $defcoursecat->id);
             $all[0] = array($defcoursecat->id);
             $all[$defcoursecat->id] = array();
@@ -2552,7 +2552,7 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
      * Returns ids of all parents of the category. Last element in the return array is the direct parent
      *
      * For example, if you have a tree of categories like:
-     *   Miscellaneous (id = 1)
+     *   Category (id = 1)
      *      Subcategory (id = 2)
      *         Sub-subcategory (id = 4)
      *   Other category (id = 3)
@@ -2578,14 +2578,14 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
      * List is cached for 10 minutes
      *
      * For example, if you have a tree of categories like:
-     *   Miscellaneous (id = 1)
+     *   Category (id = 1)
      *      Subcategory (id = 2)
      *         Sub-subcategory (id = 4)
      *   Other category (id = 3)
      * Then after calling this function you will have
-     * array(1 => 'Miscellaneous',
-     *       2 => 'Miscellaneous / Subcategory',
-     *       4 => 'Miscellaneous / Subcategory / Sub-subcategory',
+     * array(1 => 'Category',
+     *       2 => 'Category / Subcategory',
+     *       4 => 'Category / Subcategory / Sub-subcategory',
      *       3 => 'Other category');
      *
      * If you specify $requiredcapability, then only categories where the current
index fd66829..8ad8834 100644 (file)
Binary files a/course/format/amd/build/courseeditor.min.js.map and b/course/format/amd/build/courseeditor.min.js.map differ
diff --git a/course/format/amd/build/local/content.min.js b/course/format/amd/build/local/content.min.js
new file mode 100644 (file)
index 0000000..0433933
Binary files /dev/null and b/course/format/amd/build/local/content.min.js differ
diff --git a/course/format/amd/build/local/content.min.js.map b/course/format/amd/build/local/content.min.js.map
new file mode 100644 (file)
index 0000000..4aaa1c8
Binary files /dev/null and b/course/format/amd/build/local/content.min.js.map differ
index 21e7feb..7940e7b 100644 (file)
Binary files a/course/format/amd/build/local/courseeditor/courseeditor.min.js and b/course/format/amd/build/local/courseeditor/courseeditor.min.js differ
index 2312686..f9c0f7f 100644 (file)
Binary files a/course/format/amd/build/local/courseeditor/courseeditor.min.js.map and b/course/format/amd/build/local/courseeditor/courseeditor.min.js.map differ
diff --git a/course/format/amd/build/local/courseeditor/dndcmitem.min.js b/course/format/amd/build/local/courseeditor/dndcmitem.min.js
new file mode 100644 (file)
index 0000000..7bd1d49
Binary files /dev/null and b/course/format/amd/build/local/courseeditor/dndcmitem.min.js differ
diff --git a/course/format/amd/build/local/courseeditor/dndcmitem.min.js.map b/course/format/amd/build/local/courseeditor/dndcmitem.min.js.map
new file mode 100644 (file)
index 0000000..4047724
Binary files /dev/null and b/course/format/amd/build/local/courseeditor/dndcmitem.min.js.map differ
diff --git a/course/format/amd/build/local/courseeditor/dndsection.min.js b/course/format/amd/build/local/courseeditor/dndsection.min.js
new file mode 100644 (file)
index 0000000..df8e275
Binary files /dev/null and b/course/format/amd/build/local/courseeditor/dndsection.min.js differ
diff --git a/course/format/amd/build/local/courseeditor/dndsection.min.js.map b/course/format/amd/build/local/courseeditor/dndsection.min.js.map
new file mode 100644 (file)
index 0000000..0e1b3ff
Binary files /dev/null and b/course/format/amd/build/local/courseeditor/dndsection.min.js.map differ
diff --git a/course/format/amd/build/local/courseeditor/dndsectionitem.min.js b/course/format/amd/build/local/courseeditor/dndsectionitem.min.js
new file mode 100644 (file)
index 0000000..b863857
Binary files /dev/null and b/course/format/amd/build/local/courseeditor/dndsectionitem.min.js differ
diff --git a/course/format/amd/build/local/courseeditor/dndsectionitem.min.js.map b/course/format/amd/build/local/courseeditor/dndsectionitem.min.js.map
new file mode 100644 (file)
index 0000000..be6a44b
Binary files /dev/null and b/course/format/amd/build/local/courseeditor/dndsectionitem.min.js.map differ
index e0be958..f9adccd 100644 (file)
Binary files a/course/format/amd/build/local/courseeditor/exporter.min.js and b/course/format/amd/build/local/courseeditor/exporter.min.js differ
index a13af87..4b4b7f8 100644 (file)
Binary files a/course/format/amd/build/local/courseeditor/exporter.min.js.map and b/course/format/amd/build/local/courseeditor/exporter.min.js.map differ
index c247ae2..bafbd33 100644 (file)
Binary files a/course/format/amd/build/local/courseeditor/mutations.min.js and b/course/format/amd/build/local/courseeditor/mutations.min.js differ
index c3fb1ff..ee4983b 100644 (file)
Binary files a/course/format/amd/build/local/courseeditor/mutations.min.js.map and b/course/format/amd/build/local/courseeditor/mutations.min.js.map differ
index 9c94cb3..3e0d337 100644 (file)
Binary files a/course/format/amd/build/local/courseindex/cm.min.js and b/course/format/amd/build/local/courseindex/cm.min.js differ
index 4020b5e..242ac8f 100644 (file)
Binary files a/course/format/amd/build/local/courseindex/cm.min.js.map and b/course/format/amd/build/local/courseindex/cm.min.js.map differ
diff --git a/course/format/amd/build/local/courseindex/section.min.js b/course/format/amd/build/local/courseindex/section.min.js
new file mode 100644 (file)
index 0000000..6f408af
Binary files /dev/null and b/course/format/amd/build/local/courseindex/section.min.js differ
diff --git a/course/format/amd/build/local/courseindex/section.min.js.map b/course/format/amd/build/local/courseindex/section.min.js.map
new file mode 100644 (file)
index 0000000..cab19de
Binary files /dev/null and b/course/format/amd/build/local/courseindex/section.min.js.map differ
diff --git a/course/format/amd/build/local/courseindex/sectiontitle.min.js b/course/format/amd/build/local/courseindex/sectiontitle.min.js
new file mode 100644 (file)
index 0000000..f63442f
Binary files /dev/null and b/course/format/amd/build/local/courseindex/sectiontitle.min.js differ
diff --git a/course/format/amd/build/local/courseindex/sectiontitle.min.js.map b/course/format/amd/build/local/courseindex/sectiontitle.min.js.map
new file mode 100644 (file)
index 0000000..4e81671
Binary files /dev/null and b/course/format/amd/build/local/courseindex/sectiontitle.min.js.map differ
index a17a5f9..056434e 100644 (file)
@@ -54,6 +54,7 @@ function dispatchStateChangedEvent(detail, target) {
  * @param {number} courseId the course id
  * @param {setup} setup format, page and course settings
  * @property {boolean} setup.editing if the page is in edit mode
+ * @property {boolean} setup.supportscomponents if the format supports components for content
  */
 export const setViewFormat = (courseId, setup) => {
     const editor = getCourseEditor(courseId);
diff --git a/course/format/amd/src/local/content.js b/course/format/amd/src/local/content.js
new file mode 100644 (file)
index 0000000..1a25150
--- /dev/null
@@ -0,0 +1,230 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Course index main component.
+ *
+ * @module     core_courseformat/local/content
+ * @class      core_courseformat/local/content
+ * @copyright  2020 Ferran Recio <ferran@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {BaseComponent} from 'core/reactive';
+import {getCurrentCourseEditor} from 'core_courseformat/courseeditor';
+import inplaceeditable from 'core/inplace_editable';
+// Course actions is needed for actions that are not migrated to components.
+import courseActions from 'core_course/actions';
+
+export default class Component extends BaseComponent {
+
+    /**
+     * Constructor hook.
+     */
+    create() {
+        // Optional component name for debugging.
+        this.name = 'course_format';
+        // Default query selectors.
+        this.selectors = {
+            SECTION: `[data-for='section']`,
+            SECTION_ITEM: `[data-for='section_item']`,
+            SECTION_TITLE: `[data-for='section_title']`,
+            SECTION_CMLIST: `[data-for='cmlist']`,
+            COURSE_SECTIONLIST: `[data-for='course_sectionlist']`,
+            CM: `[data-for='cmitem']`,
+        };
+        // Array to save dettached elements during element resorting.
+        this.dettachedCms = {};
+        this.dettachedSections = {};
+    }
+
+    /**
+     * Static method to create a component instance form the mustahce template.
+     *
+     * @param {string} target the DOM main element or its ID
+     * @param {object} selectors optional css selector overrides
+     * @return {Component}
+     */
+    static init(target, selectors) {
+        return new Component({
+            element: document.getElementById(target),
+            reactive: getCurrentCourseEditor(),
+            selectors,
+        });
+    }
+
+    /**
+     * Return the component watchers.
+     *
+     * @returns {Array} of watchers
+     */
+    getWatchers() {
+        // Check if the course format is compatible with reactive components.
+        if (!this.reactive.supportComponents) {
+            return [];
+        }
+        return [
+            // State changes that require to reload some course modules.
+            {watch: `cm.visible:updated`, handler: this._reloadCm},
+            // Update section number and title.
+            {watch: `section.number:updated`, handler: this._refreshSectionNumber},
+            // Sections and cm sorting.
+            {watch: `transaction:start`, handler: this._startProcessing},
+            {watch: `course.sectionlist:updated`, handler: this._refreshCourseSectionlist},
+            {watch: `section.cmlist:updated`, handler: this._refreshSectionCmlist},
+        ];
+    }
+
+    /**
+     * Reload a course module.
+     *
+     * Most course module HTML is still strongly backend dependant.
+     * Some changes require to get a new version af the module.
+     *
+     * @param {Object} update the state update data
+     */
+    _reloadCm({element}) {
+        const cmitem = this.getElement(this.selectors.CM, element.id);
+        if (cmitem) {
+            courseActions.refreshModule(cmitem, element.id);
+        }
+    }
+
+    /**
+     * Setup the component to start a transaction.
+     *
+     * Some of the course actions replaces the current DOM element with a new one before updating the
+     * course state. This means the component cannot preload any index properly until the transaction starts.
+     *
+     */
+    _startProcessing() {
+        // During a section or cm sorting, some elements could be dettached from the DOM and we
+        // need to store somewhare in case they are needed later.
+        this.dettachedCms = {};
+        this.dettachedSections = {};
+    }
+
+    /**
+     * Update a course section when the section number changes.
+     *
+     * The courseActions module used for most course section tools still depends on css classes and
+     * section numbers (not id). To prevent inconsistencies when a section is moved, we need to refresh
+     * the
+     *
+     * Course formats can override the section title rendering so the frontend depends heavily on backend
+     * rendering. Luckily in edit mode we can trigger a title update using the inplace_editable module.
+     *
+     * @param {Object} details the update details.
+     */
+    _refreshSectionNumber({element}) {
+        // Find the element.
+        const target = this.getElement(this.selectors.SECTION, element.id);
+        if (!target) {
+            throw new Error(`Unkown section with ID ${element.id}`);
+        }
+        // Update section numbers in all data, css and YUI attributes.
+        target.id = `section-${element.number}`;
+        // YUI uses section number as section id in data-sectionid, in principle if a format use components
+        // don't need this sectionid attribute anymore, but we keep the compatibility in case some plugin
+        // use it for legacy purposes.
+        target.dataset.sectionid = element.number;
+        // The data-number is the attribute used by components to store the section number.
+        target.dataset.number = element.number;
+
+        // Update title and title inplace editable, if any.
+        const inplace = inplaceeditable.getInplaceEditable(target.querySelector(this.selectors.SECTION_TITLE));
+        if (inplace) {
+            // The course content HTML can be modified at any moment, so the function need to do some checkings
+            // to make sure the inplace editable still represents the same itemid.
+            const currentvalue = inplace.getValue();
+            const currentitemid = inplace.getItemId();
+            // Unnamed sections must be recalculated.
+            if (inplace.getValue() === '') {
+                // The value to send can be an empty value if it is a default name.
+                if (currentitemid == element.id && (currentvalue != element.rawtitle || element.rawtitle == '')) {
+                    inplace.setValue(element.rawtitle);
+                }
+            }
+        }
+    }
+
+    /**
+     * Refresh a section cm list.
+     *
+     * @param {Object} details the update details.
+     */
+    _refreshSectionCmlist({element}) {
+        const cmlist = element.cmlist ?? [];
+        const section = this.getElement(this.selectors.SECTION, element.id);
+        const listparent = section?.querySelector(this.selectors.SECTION_CMLIST);
+        if (listparent) {
+            this._fixOrder(listparent, cmlist, this.selectors.CM, this.dettachedCms);
+        }
+    }
+
+    /**
+     * Refresh the section list.
+     *
+     * @param {Object} details the update details.
+     */
+    _refreshCourseSectionlist({element}) {
+        const sectionlist = element.sectionlist ?? [];
+        const listparent = this.getElement(this.selectors.COURSE_SECTIONLIST);
+        if (listparent) {
+            this._fixOrder(listparent, sectionlist, this.selectors.SECTION, this.dettachedSections);
+        }
+    }
+
+    /**
+     * Fix/reorder the section or cms order.
+     *
+     * @param {Element} container the HTML element to reorder.
+     * @param {Array} neworder an array with the ids order
+     * @param {string} selector the element selector
+     * @param {Object} dettachedelements a list of dettached elements
+     */
+    _fixOrder(container, neworder, selector, dettachedelements) {
+
+        // Empty lists should not be visible.
+        if (!neworder.length) {
+            container.classList.add('hidden');
+            container.innerHTML = '';
+            return;
+        }
+
+        // Grant the list is visible (in case it was empty).
+        container.classList.remove('hidden');
+
+        // Move the elements in order at the beginning of the list.
+        neworder.forEach((itemid, index) => {
+            const item = this.getElement(selector, itemid) ?? dettachedelements[itemid];
+            // Get the current elemnt at that position.
+            const currentitem = container.children[index];
+            if (currentitem === undefined) {
+                container.append(item);
+                return;
+            }
+            if (currentitem !== item) {
+                container.insertBefore(item, currentitem);
+            }
+        });
+        // Remove the remaining elements.
+        while (container.children.length > neworder.length) {
+            const lastchild = container.lastChild;
+            dettachedelements[lastchild?.dataset?.id ?? 0] = lastchild;
+            container.removeChild(lastchild);
+        }
+    }
+}
index 5578417..c8f44c6 100644 (file)
@@ -47,6 +47,7 @@ export default class extends Reactive {
 
         // Default view format setup.
         this._editing = false;
+        this._supportscomponents = false;
 
         this.courseId = courseId;
 
@@ -68,9 +69,11 @@ export default class extends Reactive {
      *
      * @param {Object} setup format, page and course settings
      * @property {boolean} setup.editing if the page is in edit mode
+     * @property {boolean} setup.supportscomponents if the format supports components for content
      */
     setViewFormat(setup) {
         this._editing = setup.editing ?? false;
+        this._supportscomponents = setup.supportscomponents ?? false;
     }
 
     /**
@@ -116,6 +119,15 @@ export default class extends Reactive {
         return new Exporter(this);
     }
 
+    /**
+     * Return if the current course support components to refresh the content.
+     *
+     * @returns {boolean} if the current content support components
+     */
+    get supportComponents() {
+        return this._supportscomponents ?? false;
+    }
+
     /**
      * Dispatch a change in the state.
      *
diff --git a/course/format/amd/src/local/courseeditor/dndcmitem.js b/course/format/amd/src/local/courseeditor/dndcmitem.js
new file mode 100644 (file)
index 0000000..19e3aef
--- /dev/null
@@ -0,0 +1,113 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Course index cm component.
+ *
+ * This component is used to control specific course modules interactions like drag and drop
+ * in both course index and course content.
+ *
+ * @module     core_courseformat/local/courseeditor/dndcmitem
+ * @class      core_courseformat/local/courseeditor/dndcmitem
+ * @copyright  2021 Ferran Recio <ferran@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {BaseComponent, DragDrop} from 'core/reactive';
+
+export default class extends BaseComponent {
+
+    /**
+     * Configure the component drag and drop.
+     *
+     * @param {number} cmid course module id
+     */
+    configDragDrop(cmid) {
+
+        this.id = cmid;
+
+        // Drag and drop is only available for components compatible course formats.
+        if (this.reactive.isEditing && this.reactive.supportComponents) {
+            // Init element drag and drop.
+            this.dragdrop = new DragDrop(this);
+            // Save dropzone classes.
+            this.classes = this.dragdrop.getClasses();
+        }
+    }
+
+    /**
+     * Remove all subcomponents dependencies.
+     */
+    destroy() {
+        if (this.dragdrop !== undefined) {
+            this.dragdrop.unregister();
+        }
+    }
+
+    // Drag and drop methods.
+
+    /**
+     * Get the draggable data of this component.
+     *
+     * @returns {Object} exported course module drop data
+     */
+    getDraggableData() {
+        const exporter = this.reactive.getExporter();
+        return exporter.cmDraggableData(this.reactive.state, this.id);
+    }
+
+    /**
+     * Validate if the drop data can be dropped over the component.
+     *
+     * @param {Object} dropdata the exported drop data.
+     * @returns {boolean}
+     */
+    validateDropData(dropdata) {
+        return dropdata?.type === 'cm';
+    }
+
+    /**
+     * Display the component dropzone.
+     *
+     * @param {Object} dropdata the accepted drop data
+     */
+    showDropZone(dropdata) {
+        // If we are the next cmid of the dragged element we accept the drop because otherwise it
+        // will get captured by the section. However, we won't trigger any mutation.
+        if (dropdata.nextcmid != this.id && dropdata.id != this.id) {
+            this.element.classList.add(this.classes.DROPUP);
+        }
+    }
+
+    /**
+     * Hide the component dropzone.
+     */
+    hideDropZone() {
+        this.element.classList.remove(this.classes.DROPUP);
+    }
+
+    /**
+     * Drop event handler.
+     *
+     * @param {Object} dropdata the accepted drop data
+     */
+    drop(dropdata) {
+        // Call the move mutation if necessary.
+        if (dropdata.id != this.id && dropdata.nextcmid != this.id) {
+            this.reactive.dispatch('cmMove', [dropdata.id], null, this.id);
+        }
+    }
+
+}
diff --git a/course/format/amd/src/local/courseeditor/dndsection.js b/course/format/amd/src/local/courseeditor/dndsection.js
new file mode 100644 (file)
index 0000000..9d6568f
--- /dev/null
@@ -0,0 +1,146 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Course index section component.
+ *
+ * This component is used to control specific course section interactions like drag and drop
+ * in both course index and course content.
+ *
+ * @module     core_courseformat/local/courseeditor/dndsection
+ * @class      core_courseformat/local/courseeditor/dndsection
+ * @copyright  2021 Ferran Recio <ferran@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {BaseComponent, DragDrop} from 'core/reactive';
+
+export default class extends BaseComponent {
+
+    /**
+     * Save some values form the state.
+     *
+     * @param {Object} state the current state
+     */
+    configState(state) {
+        this.id = this.element.dataset.id;
+        this.section = state.section.get(this.id);
+        this.course = state.course;
+    }
+
+    /**
+     * Register state values and the drag and drop subcomponent.
+     *
+     * @param {BaseComponent} sectionitem section item component
+     */
+    configDragDrop(sectionitem) {
+        // Drag and drop is only available for components compatible course formats.
+        if (this.reactive.isEditing && this.reactive.supportComponents) {
+            // Init the inner dragable element.
+            this.sectionitem = sectionitem;
+            // Init the dropzone.
+            this.dragdrop = new DragDrop(this);
+            // Save dropzone classes.
+            this.classes = this.dragdrop.getClasses();
+        }
+    }
+
+    /**
+     * Remove all subcomponents dependencies.
+     */
+    destroy() {
+        if (this.sectionitem !== undefined) {
+            this.sectionitem.unregister();
+        }
+        if (this.dragdrop !== undefined) {
+            this.dragdrop.unregister();
+        }
+    }
+
+    /**
+     * Get the last CM element of that section.
+     *
+     * @returns {element|null} the las course module element of the section.
+     */
+    getLastCm() {
+        return null;
+    }
+
+    // Drag and drop methods.
+
+    /**
+     * Validate if the drop data can be dropped over the component.
+     *
+     * @param {Object} dropdata the exported drop data.
+     * @returns {boolean}
+     */
+    validateDropData(dropdata) {
+        // We accept any course module.
+        if (dropdata?.type === 'cm') {
+            return true;
+        }
+        // We accept any section bu the section 0 or ourself
+        if (dropdata?.type === 'section') {
+            const sectionzeroid = this.course.sectionlist[0];
+            return dropdata?.id != this.id && dropdata?.id != sectionzeroid && this.id != sectionzeroid;
+        }
+        return false;
+    }
+
+    /**
+     * Display the component dropzone.
+     *
+     * @param {Object} dropdata the accepted drop data
+     */
+    showDropZone(dropdata) {
+        if (dropdata.type == 'cm') {
+            this.getLastCm()?.classList.add(this.classes.DROPDOWN);
+        }
+        if (dropdata.type == 'section') {
+            // The relative move of section depends on the section number.
+            if (this.section.number > dropdata.number) {
+                this.element.classList.remove(this.classes.DROPUP);
+                this.element.classList.add(this.classes.DROPDOWN);
+            } else {
+                this.element.classList.add(this.classes.DROPUP);
+                this.element.classList.remove(this.classes.DROPDOWN);
+            }
+        }
+    }
+
+    /**
+     * Hide the component dropzone.
+     */
+    hideDropZone() {
+        this.getLastCm()?.classList.remove(this.classes.DROPDOWN);
+        this.element.classList.remove(this.classes.DROPUP);
+        this.element.classList.remove(this.classes.DROPDOWN);
+    }
+
+    /**
+     * Drop event handler.
+     *
+     * @param {Object} dropdata the accepted drop data
+     */
+    drop(dropdata) {
+        // Call the move mutation.
+        if (dropdata.type == 'cm') {
+            this.reactive.dispatch('cmMove', [dropdata.id], this.id);
+        }
+        if (dropdata.type == 'section') {
+            this.reactive.dispatch('sectionMove', [dropdata.id], this.id);
+        }
+    }
+}
diff --git a/course/format/amd/src/local/courseeditor/dndsectionitem.js b/course/format/amd/src/local/courseeditor/dndsectionitem.js
new file mode 100644 (file)
index 0000000..850c6e5
--- /dev/null
@@ -0,0 +1,129 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Course index section title draggable component.
+ *
+ * This component is used to control specific course section interactions like drag and drop
+ * in both course index and course content.
+ *
+ * @module     core_courseformat/local/courseeditor/dndsectionitem
+ * @class      core_courseformat/local/courseeditor/dndsectionitem
+ * @copyright  2021 Ferran Recio <ferran@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {BaseComponent, DragDrop} from 'core/reactive';
+
+export default class extends BaseComponent {
+
+    /**
+     * Initial state ready method.
+     *
+     * @param {number} sectionid the section id
+     * @param {Object} state the initial state
+     * @param {Element} fullregion the complete section region to mark as dragged
+     */
+    configDragDrop(sectionid, state, fullregion) {
+
+        this.id = sectionid;
+        if (this.section === undefined) {
+            this.section = state.section.get(this.id);
+        }
+        if (this.course === undefined) {
+            this.course = state.course;
+        }
+
+        // Prevent topic zero from being draggable.
+        if (this.section.number > 0) {
+            this.getDraggableData = this._getDraggableData;
+        }
+
+        this.fullregion = fullregion;
+
+        // Drag and drop is only available for components compatible course formats.
+        if (this.reactive.isEditing && this.reactive.supportComponents) {
+            // Init the dropzone.
+            this.dragdrop = new DragDrop(this);
+            // Save dropzone classes.
+            this.classes = this.dragdrop.getClasses();
+        }
+    }
+
+    /**
+     * Remove all subcomponents dependencies.
+     */
+    destroy() {
+        if (this.dragdrop !== undefined) {
+            this.dragdrop.unregister();
+        }
+    }
+
+    // Drag and drop methods.
+
+    /**
+     * Get the draggable data of this component.
+     *
+     * @returns {Object} exported course module drop data
+     */
+    _getDraggableData() {
+        const exporter = this.reactive.getExporter();
+        return exporter.sectionDraggableData(this.reactive.state, this.id);
+    }
+
+    /**
+     * Validate if the drop data can be dropped over the component.
+     *
+     * @param {Object} dropdata the exported drop data.
+     * @returns {boolean}
+     */
+    validateDropData(dropdata) {
+        // Course module validation.
+        if (dropdata?.type === 'cm') {
+            // The first section element is already there so we can ignore it.
+            const firstcmid = this.section?.cmlist[0];
+            return dropdata.id !== firstcmid;
+        }
+        return false;
+    }
+
+    /**
+     * Display the component dropzone.
+     *
+     * @param {Object} dropdata the accepted drop data
+     */
+    showDropZone() {
+        this.element.classList.add(this.classes.DROPZONE);
+    }
+
+    /**
+     * Hide the component dropzone.
+     */
+    hideDropZone() {
+        this.element.classList.remove(this.classes.DROPZONE);
+    }
+
+    /**
+     * Drop event handler.
+     *
+     * @param {Object} dropdata the accepted drop data
+     */
+    drop(dropdata) {
+        // Call the move mutation.
+        if (dropdata.type == 'cm') {
+            this.reactive.dispatch('cmMove', [dropdata.id], this.id, this.section?.cmlist[0]);
+        }
+    }
+}
index c76d076..cb738e8 100644 (file)
@@ -94,4 +94,59 @@ export default class {
         };
         return cm;
     }
+
+    /**
+     * Generate a dragable cm data structure.
+     *
+     * This method is used by any draggable course module element to generate drop data
+     * for its reactive/dragdrop instance.
+     *
+     * @param {*} state the state object
+     * @param {*} cmid the cours emodule id
+     * @returns {Object|null}
+     */
+    cmDraggableData(state, cmid) {
+        const cminfo = state.cm.get(cmid);
+        if (!cminfo) {
+            return null;
+        }
+
+        // Drop an activity over the next activity is the same as doing anything.
+        let nextcmid;
+        const section = state.section.get(cminfo.sectionid);
+        const currentindex = section?.cmlist.indexOf(cminfo.id);
+        if (currentindex !== undefined) {
+            nextcmid = section?.cmlist[currentindex + 1];
+        }
+
+        return {
+            type: 'cm',
+            id: cminfo.id,
+            name: cminfo.name,
+            nextcmid,
+        };
+    }
+
+    /**
+     * Generate a dragable cm data structure.
+     *
+     * This method is used by any draggable section element to generate drop data
+     * for its reactive/dragdrop instance.
+     *
+     * @param {*} state the state object
+     * @param {*} sectionid the cours section id
+     * @returns {Object|null}
+     */
+    sectionDraggableData(state, sectionid) {
+        const sectioninfo = state.section.get(sectionid);
+        if (!sectioninfo) {
+            return null;
+        }
+        return {
+            type: 'section',
+            id: sectioninfo.id,
+            name: sectioninfo.name,
+            number: sectioninfo.number,
+        };
+    }
 }
index 23d3e33..6b431d6 100644 (file)
@@ -34,19 +34,68 @@ export default class {
      * @param {string} action
      * @param {number} courseId
      * @param {array} ids
+     * @param {number} targetSectionId optional target section id (for moving actions)
+     * @param {number} targetCmId optional target cm id (for moving actions)
      */
-    async _callEditWebservice(action, courseId, ids) {
+    async _callEditWebservice(action, courseId, ids, targetSectionId, targetCmId) {
+        const args = {
+            action,
+            courseid: courseId,
+            ids,
+        };
+        if (targetSectionId) {
+            args.targetsectionid = targetSectionId;
+        }
+        if (targetCmId) {
+            args.targetcmid = targetCmId;
+        }
         let ajaxresult = await ajax.call([{
             methodname: 'core_courseformat_update_course',
-            args: {
-                action,
-                courseid: courseId,
-                ids,
-            }
+            args,
         }])[0];
         return JSON.parse(ajaxresult);
     }
 
+    /**
+     * Move course modules to specific course location.
+     *
+     * Note that one of targetSectionId or targetCmId should be provided in order to identify the
+     * new location:
+     *  - targetCmId: the activities will be located avobe the target cm. The targetSectionId
+     *                value will be ignored in this case.
+     *  - targetSectionId: the activities will be appended to the section. In this case
+     *                     targetSectionId should not be present.
+     *
+     * @param {StateManager} stateManager the current state manager
+     * @param {array} cmids the list of cm ids to move
+     * @param {number} targetSectionId the target section id
+     * @param {number} targetCmId the target course module id
+     */
+    async cmMove(stateManager, cmids, targetSectionId, targetCmId) {
+        if (!targetSectionId && !targetCmId) {
+            throw new Error(`Mutation cmMove requires targetSectionId or targetCmId`);
+        }
+        const course = stateManager.get('course');
+        const updates = await this._callEditWebservice('cm_move', course.id, cmids, targetSectionId, targetCmId);
+        stateManager.processUpdates(updates);
+    }
+
+    /**
+     * Move course modules to specific course location.
+     *
+     * @param {StateManager} stateManager the current state manager
+     * @param {array} sectionIds the list of section ids to move
+     * @param {number} targetSectionId the target section id
+     */
+    async sectionMove(stateManager, sectionIds, targetSectionId) {
+        if (!targetSectionId) {
+            throw new Error(`Mutation sectionMove requires targetSectionId`);
+        }
+        const course = stateManager.get('course');
+        const updates = await this._callEditWebservice('section_move', course.id, sectionIds, targetSectionId);
+        stateManager.processUpdates(updates);
+    }
+
     /**
      * Get updated state data related to some cm ids.
      *
index b264bcb..f0e8b8c 100644 (file)
@@ -24,9 +24,9 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-import {BaseComponent} from 'core/reactive';
+import DndCmItem from 'core_courseformat/local/courseeditor/dndcmitem';
 
-export default class Component extends BaseComponent {
+export default class Component extends DndCmItem {
 
     /**
      * Constructor hook.
@@ -34,9 +34,6 @@ export default class Component extends BaseComponent {
     create() {
         // Optional component name for debugging.
         this.name = 'courseindex_cm';
-        // Default query selectors.
-        this.selectors = {
-        };
         // We need our id to watch specific events.
         this.id = this.element.dataset.id;
     }
@@ -59,9 +56,14 @@ export default class Component extends BaseComponent {
      * Initial state ready method.
      */
     stateReady() {
-        // Activate drag and drop soon.
+        this.configDragDrop(this.id);
     }
 
+    /**
+     * Component watchers.
+     *
+     * @returns {Array} of watchers
+     */
     getWatchers() {
         return [
             {watch: `cm[${this.id}]:deleted`, handler: this.remove},
diff --git a/course/format/amd/src/local/courseindex/section.js b/course/format/amd/src/local/courseindex/section.js
new file mode 100644 (file)
index 0000000..98b1a48
--- /dev/null
@@ -0,0 +1,86 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Course index section component.
+ *
+ * This component is used to control specific course section interactions like drag and drop.
+ *
+ * @module     core_courseformat/local/courseindex/section
+ * @class      core_courseformat/local/courseindex/section
+ * @copyright  2021 Ferran Recio <ferran@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import SectionTitle from 'core_courseformat/local/courseindex/sectiontitle';
+import DndSection from 'core_courseformat/local/courseeditor/dndsection';
+
+export default class Component extends DndSection {
+
+    /**
+     * Constructor hook.
+     */
+    create() {
+        // Optional component name for debugging.
+        this.name = 'courseindex_section';
+        // Default query selectors.
+        this.selectors = {
+            SECTION_ITEM: `[data-for='section_item']`,
+            CM_LAST: `[data-for="cm"]:last-child`,
+        };
+    }
+
+    /**
+     * Static method to create a component instance form the mustahce template.
+     *
+     * @param {string} target the DOM main element or its ID
+     * @param {object} selectors optional css selector overrides
+     * @return {Component}
+     */
+    static init(target, selectors) {
+        return new Component({
+            element: document.getElementById(target),
+            selectors,
+        });
+    }
+
+    /**
+     * Initial state ready method.
+     *
+     * @param {Object} state the initial state
+     */
+    stateReady(state) {
+        this.configState(state);
+        // Drag and drop is only available for components compatible course formats.
+        if (this.reactive.isEditing && this.reactive.supportComponents) {
+            // Init the inner dragable element passing the full section as affected region.
+            const titleitem = new SectionTitle({
+                ...this,
+                element: this.getElement(this.selectors.SECTION_ITEM),
+                fullregion: this.element,
+            });
+            this.configDragDrop(titleitem);
+        }
+    }
+
+    /**
+     * Get the last CM element of that section.
+     *
+     * @returns {element|null}
+     */
+    getLastCm() {
+        return this.getElement(this.selectors.CM_LAST);
+    }
+}
diff --git a/course/format/amd/src/local/courseindex/sectiontitle.js b/course/format/amd/src/local/courseindex/sectiontitle.js
new file mode 100644 (file)
index 0000000..12d05ee
--- /dev/null
@@ -0,0 +1,73 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Course index section title component.
+ *
+ * This component is used to control specific course section interactions like drag and drop.
+ *
+ * @module     core_courseformat/local/courseindex/sectiontitle
+ * @class      core_courseformat/local/courseindex/sectiontitle
+ * @copyright  2021 Ferran Recio <ferran@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import DndSectionItem from 'core_courseformat/local/courseeditor/dndsectionitem';
+
+export default class Component extends DndSectionItem {
+
+    /**
+     * Constructor hook.
+     *
+     * @param {Object} descriptor
+     */
+    create(descriptor) {
+        // Optional component name for debugging.
+        this.name = 'courseindex_sectiontitle';
+
+        this.id = descriptor.id;
+        this.section = descriptor.section;
+        this.course = descriptor.course;
+        this.fullregion = descriptor.fullregion;
+
+        // Prevent topic zero from being draggable.
+        if (this.section.number > 0) {
+            this.getDraggableData = this._getDraggableData;
+        }
+    }
+
+    /**
+     * Static method to create a component instance form the mustahce template.
+     *
+     * @param {element|string} target the DOM main element or its ID
+     * @param {object} selectors optional css selector overrides
+     * @return {Component}
+     */
+    static init(target, selectors) {
+        return new Component({
+            element: document.getElementById(target),
+            selectors,
+        });
+    }
+
+    /**
+     * Initial state ready method.
+     *
+     * @param {Object} state the initial state
+     */
+    stateReady(state) {
+        this.configDragDrop(this.id, state, this.fullregion);
+    }
+}
index 333edd7..dc8f584 100644 (file)
@@ -494,6 +494,21 @@ abstract class base {
         return $ajaxsupport;
     }
 
+    /**
+     * Returns true if this course format is compatible with content components.
+     *
+     * Using components means the content elements can watch the frontend course state and
+     * react to the changes. Formats with component compatibility can have more interactions
+     * without refreshing the page, like having drag and drop from the course index to reorder
+     * sections and activities.
+     *
+     * @return bool if the format is compatible with components.
+     */
+    public function supports_components() {
+        return false;
+    }
+
+
     /**
      * Custom action after section has been moved in AJAX mode
      *
index b56671a..4c3f8cc 100644 (file)
@@ -64,6 +64,7 @@ class section implements renderable {
             'section' => $section->section,
             'number' => $section->section,
             'title' => $format->get_section_name($section),
+            'rawtitle' => $section->name,
             'cmlist' => [],
             'visible' => !empty($section->visible),
             'sectionurl' => course_get_url($course, $section->section)->out(),
index f37bf70..c42d463 100644 (file)
 namespace core_courseformat;
 
 use core_courseformat\stateupdates;
+use cm_info;
+use section_info;
 use stdClass;
 use course_modinfo;
 use moodle_exception;
+use context_module;
+use context_course;
 
 /**
  * Contains the core course state actions.
@@ -36,6 +40,159 @@ use moodle_exception;
  */
 class stateactions {
 
+    /**
+     * Move course modules to another location in the same course.
+     *
+     * @param stateupdates $updates the affected course elements track
+     * @param stdClass $course the course object
+     * @param int[] $ids the list of affected course module ids
+     * @param int $targetsectionid optional target section id
+     * @param int $targetcmid optional target cm id
+     */
+    public function cm_move(
+        stateupdates $updates,
+        stdClass $course,
+        array $ids,
+        ?int $targetsectionid = null,
+        ?int $targetcmid = null
+    ): void {
+        // Validate target elements.
+        if (!$targetsectionid && !$targetcmid) {
+            throw new moodle_exception("Action cm_move requires targetsectionid or targetcmid");
+        }
+
+        $this->validate_cms($course, $ids, __FUNCTION__);
+
+        // Check capabilities on every activity context.
+        foreach ($ids as $cmid) {
+            $modcontext = context_module::instance($cmid);
+            require_capability('moodle/course:manageactivities', $modcontext);
+        }
+
+        $modinfo = get_fast_modinfo($course);
+
+        // Target cm has more priority than target section.
+        if (!empty($targetcmid)) {
+            $this->validate_cms($course, [$targetcmid], __FUNCTION__);
+            $targetcm = $modinfo->get_cm($targetcmid);
+            $targetsection = $modinfo->get_section_info_by_id($targetcm->section, MUST_EXIST);
+        } else {
+            $this->validate_sections($course, [$targetsectionid], __FUNCTION__);
+            $targetcm = null;
+            $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
+        }
+
+        // The origin sections must be updated as well.
+        $originalsections = [];
+
+        $cms = $this->get_cm_info($modinfo, $ids);
+        foreach ($cms as $cm) {
+            $currentsection = $modinfo->get_section_info_by_id($cm->section, MUST_EXIST);
+            moveto_module($cm, $targetsection, $targetcm);
+            $updates->add_cm_put($cm->id);
+            if ($currentsection->id != $targetsection->id) {
+                $originalsections[$currentsection->id] = true;
+            }
+            // If some of the original sections are also target sections, we don't need to update them.
+            if (array_key_exists($targetsection->id, $originalsections)) {
+                unset($originalsections[$targetsection->id]);
+            }
+        }
+
+        // Use section_state to return the full affected section and activities updated state.
+        $this->cm_state($updates, $course, $ids, $targetsectionid, $targetcmid);
+
+        foreach (array_keys($originalsections) as $sectionid) {
+            $updates->add_section_put($sectionid);
+        }
+    }
+
+    /**
+     * Move course sections to another location in the same course.
+     *
+     * @param stateupdates $updates the affected course elements track
+     * @param stdClass $course the course object
+     * @param int[] $ids the list of affected course module ids
+     * @param int $targetsectionid optional target section id
+     * @param int $targetcmid optional target cm id
+     */
+    public function section_move(
+        stateupdates $updates,
+        stdClass $course,
+        array $ids,
+        ?int $targetsectionid = null,
+        ?int $targetcmid = null
+    ): void {
+        // Validate target elements.
+        if (!$targetsectionid) {
+            throw new moodle_exception("Action cm_move requires targetsectionid");
+        }
+
+        $this->validate_sections($course, $ids, __FUNCTION__);
+
+        $coursecontext = context_course::instance($course->id);
+        require_capability('moodle/course:movesections', $coursecontext);
+
+        $modinfo = get_fast_modinfo($course);
+
+        // Target section.
+        $this->validate_sections($course, [$targetsectionid], __FUNCTION__);
+        $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
+
+        $affectedsections = [$targetsection->section => true];
+
+        $sections = $this->get_section_info($modinfo, $ids);
+        foreach ($sections as $section) {
+            $affectedsections[$section->section] = true;
+            move_section_to($course, $section->section, $targetsection->section);
+        }
+
+        // Use section_state to return the section and activities updated state.
+        $this->section_state($updates, $course, $ids, $targetsectionid);
+
+        // All course sections can be renamed because of the resort.
+        $allsections = $modinfo->get_section_info_all();
+        foreach ($allsections as $section) {
+            // Ignore the affected sections because they are already in the updates.
+            if (isset($affectedsections[$section->section])) {
+                continue;
+            }
+            $updates->add_section_put($section->id);
+        }
+        // The section order is at a course level.
+        $updates->add_course_put();
+    }
+
+    /**
+     * Extract several cm_info from the course_modinfo.
+     *
+     * @param course_modinfo $modinfo the course modinfo.
+     * @param int[] $ids the course modules $ids
+     * @return cm_info[] the extracted cm_info objects
+     */
+    protected function get_cm_info (course_modinfo $modinfo, array $ids): array {
+        $cms = [];
+        foreach ($ids as $cmid) {
+            $cms[$cmid] = $modinfo->get_cm($cmid);
+        }
+        return $cms;
+    }
+
+    /**
+     * Extract several section_info from the course_modinfo.
+     *
+     * @param course_modinfo $modinfo the course modinfo.
+     * @param int[] $ids the course modules $ids
+     * @return section_info[] the extracted section_info objects
+     */
+    protected function get_section_info(course_modinfo $modinfo, array $ids): array {
+        $sections = [];
+        foreach ($ids as $sectionid) {
+            $sections[$sectionid] = $modinfo->get_section_info_by_id($sectionid);
+        }
+        return $sections;
+    }
+
     /**
      * Add the update messages of the updated version of any cm and section related to the cm ids.
      *
index 1b6ab8f..b36d7be 100644 (file)
         }
     }
 }}
-<h2 class="accesshide">{{{title}}}</h2>
-{{{completionhelp}}}
-<ul class="{{format}}">
-    {{#initialsection}}
-        {{> core_courseformat/local/content/section }}
-    {{/initialsection}}
-    {{#sections}}
-        {{> core_courseformat/local/content/section }}
-    {{/sections}}
-</ul>
-{{#hasnavigation}}
-<div class="single-section">
-    {{#sectionnavigation}} {{> core_courseformat/local/content/sectionnavigation }} {{/sectionnavigation}}
-    <ul class="{{format}}">
-    {{#singlesection}}
-        {{> core_courseformat/local/content/section }}
-    {{/singlesection}}
+<div id="{{uniqid}}-course-format">
+    <h2 class="accesshide">{{{title}}}</h2>
+    {{{completionhelp}}}
+    <ul class="{{format}}" data-for="course_sectionlist">
+        {{#initialsection}}
+            {{> core_courseformat/local/content/section }}
+        {{/initialsection}}
+        {{#sections}}
+            {{> core_courseformat/local/content/section }}
+        {{/sections}}
     </ul>
-    {{#sectionselector}} {{> core_courseformat/local/content/sectionselector }} {{/sectionselector}}
+    {{#hasnavigation}}
+    <div class="single-section">
+        {{#sectionnavigation}} {{> core_courseformat/local/content/sectionnavigation }} {{/sectionnavigation}}
+        <ul class="{{format}}">
+        {{#singlesection}}
+            {{> core_courseformat/local/content/section }}
+        {{/singlesection}}
+        </ul>
+        {{#sectionselector}} {{> core_courseformat/local/content/sectionselector }} {{/sectionselector}}
+    </div>
+    {{/hasnavigation}}
+    {{#numsections}} {{> core_courseformat/local/content/addsection}} {{/numsections}}
 </div>
-{{/hasnavigation}}
-{{#numsections}} {{> core_courseformat/local/content/addsection}} {{/numsections}}
+{{#js}}
+require(['core_courseformat/local/content'], function(component) {
+    component.init('{{uniqid}}-course-format');
+});
+{{/js}}
index 20301e6..0e51f35 100644 (file)
     }
 }}
 <li id="section-{{num}}"
-        class="section main {{#onlysummary}} section-summary {{/onlysummary}} clearfix
-              {{#ishidden}} hidden {{/ishidden}} {{#iscurrent}} current {{/iscurrent}}
-              {{#isstealth}} orphaned {{/isstealth}}"
-        role="region"
-        aria-labelledby="sectionid-{{id}}-title"
-        data-sectionid="{{num}}"
-        data-sectionreturnid="{{sectionreturnid}}">
+    class="section main {{#onlysummary}} section-summary {{/onlysummary}} clearfix
+            {{#ishidden}} hidden {{/ishidden}} {{#iscurrent}} current {{/iscurrent}}
+            {{#isstealth}} orphaned {{/isstealth}}"
+    role="region"
+    aria-labelledby="sectionid-{{id}}-title"
+    data-sectionid="{{num}}"
+    data-sectionreturnid="{{sectionreturnid}}"
+    data-for="section"
+    data-id="{{id}}"
+    data-number="{{num}}"
+>
     {{#singleheader}} {{> core_courseformat/local/content/section/header }} {{/singleheader}}
     <div class="left side">{{#iscurrent}} {{{currentlink}}} {{/iscurrent}}</div>
     <div class="right side">
index c9ef2cc..7c6ff32 100644 (file)
         "extraclasses": "newmessages"
     }
 }}
-<li class="activity {{module}} modtype_{{module}} {{extraclasses}} {{#hasinfo}}hasinfo{{/hasinfo}}" id="module-{{id}}">
+<li
+    class="activity {{module}} modtype_{{module}} {{extraclasses}} {{#hasinfo}}hasinfo{{/hasinfo}}"
+    id="module-{{id}}"
+    data-for="cmitem"
+    data-id="{{id}}"
+>
 {{#cmformat}}
     {{> core_courseformat/local/content/cm}}
 {{/cmformat}}
index af632b2..34f627c 100644 (file)
 {{#showmovehere}}
 <p>{{movingstr}} (<a href="{{{cancelcopyurl}}}">{{#str}} cancel {{/str}}</a>)</p>
 {{/showmovehere}}
-{{#hascms}}
-    <ul class="section img-text">
-    {{#cms}}
-        {{#showmovehere}}
-            <li class="movehere">
-                <a href="{{{moveurl}}}" title="{{strmovefull}}" class="movehere"></a>
-            </li>
-        {{/showmovehere}}
-        {{#cmitem}}
-            {{> core_courseformat/local/content/section/cmitem}}
-        {{/cmitem}}
-    {{/cms}}
+<ul class="section img-text {{#hascms}} d-block {{/hascms}}" data-for="cmlist">
+{{#cms}}
     {{#showmovehere}}
     <li class="movehere">
-        <a href="{{{movetosectionurl}}}" title="{{strmovefull}}" class="movehere"></a>
+        <a href="{{{moveurl}}}" title="{{strmovefull}}" class="movehere"></a>
     </li>
     {{/showmovehere}}
+    {{#cmitem}}
+        {{> core_courseformat/local/content/section/cmitem}}
+    {{/cmitem}}
+{{/cms}}
+{{#showmovehere}}
+    <li class="movehere">
+        <a href="{{{movetosectionurl}}}" title="{{strmovefull}}" class="movehere"></a>
+    </li>
+{{/showmovehere}}
     </ul>
-{{/hascms}}
index 14d0a18..7359a29 100644 (file)
@@ -27,7 +27,7 @@
     }
 }}
 
-<h3 class="sectionid-{{id}}-title sectionname">
+<h3 class="sectionid-{{id}}-title sectionname" data-for="section_title">
     {{#url}}
     <a href="{{{url}}}" class="{{#ishidden}} dimmed_text {{/ishidden}}">{{name}}</a>
     {{/url}}
index c17b17a..ef89ba5 100644 (file)
@@ -56,6 +56,7 @@
         {{{name}}}
     </span>
     {{/url}}
+    <span class="dragicon ml-auto">{{#pix}}i/dragdrop{{/pix}}</span>
 </li>
 {{#js}}
 require(['core_courseformat/local/courseindex/cm'], function(component) {
index 91899cb..96ba678 100644 (file)
@@ -89,6 +89,7 @@
         >
             {{{title}}}
         </a>
+        <span class="dragicon ml-auto">{{#pix}}i/dragdrop{{/pix}}</span>
     </div>
     <div id="courseindexcollapse{{number}}"
         class="courseindex-item-content collapse {{#isactive}}show{{/isactive}}"
         </ul>
     </div>
 </div>
+{{#js}}
+require(['core_courseformat/local/courseindex/section'], function(component) {
+    component.init('{{uniqid}}-course-index-section-{{id}}');
+});
+{{/js}}
index b0e84ef..d4e34e8 100644 (file)
@@ -159,6 +159,10 @@ class format_topics extends core_courseformat\base {
         return $ajaxsupport;
     }
 
+    public function supports_components() {
+        return true;
+    }
+
     /**
      * Loads all of the course sections into the navigation.
      *
index bf173ba..ecf5c18 100644 (file)
@@ -3,7 +3,8 @@ This files describes API changes for course formats
 Overview of this plugin type at http://docs.moodle.org/dev/Course_formats
 
 === 4.0 ===
-* New core_courseformat\uses_course_index() to define whether the course format uses course index or not.
+* New core_courseformat\base::uses_course_index() to define whether the course format uses course index or not.
+* New core_courseformat\base::supports_components() to specify if the format is compatible with reactive components.
 
 === 3.10 ===
 * Added the missing callback supports_ajax() to format_social.
index 5526b01..cdb38b3 100644 (file)
@@ -160,6 +160,10 @@ class format_weeks extends core_courseformat\base {
         return $ajaxsupport;
     }
 
+    public function supports_components() {
+        return true;
+    }
+
     /**
      * Loads all of the course sections into the navigation
      *
index 1f6b876..5b9bd4f 100644 (file)
@@ -3308,6 +3308,7 @@ function include_course_editor(course_format $format) {
     // Edition mode and some format specs must be passed to the init method.
     $setup = (object)[
         'editing' => $format->show_editor(),
+        'supportscomponents' => $format->supports_components(),
     ];
     // All the new editor elements will be loaded after the course is presented and
     // the initial course state will be generated using core_course_get_state webservice.
index 88ae6be..f7ba6ed 100644 (file)
@@ -29,7 +29,7 @@
                 "fullname": "course 3",
                 "hasprogress": true,
                 "progress": 10,
-                "coursecategory": "Miscellaneous"
+                "coursecategory": "Category 1"
             }
         ]
     }
index ba0d63c..60563b3 100644 (file)
@@ -28,7 +28,7 @@
                 "courseimageurl": "https://moodlesite/pluginfile/123/course/overviewfiles/123.jpg",
                 "fullname": "course 3",
                 "isfavourite": true,
-                "coursecategory": "Miscellaneous"
+                "coursecategory": "Category 1"
             }
         ]
     }
index a7a26dc..a477f39 100644 (file)
@@ -121,8 +121,8 @@ class behat_course extends behat_base {
         // Ensure you are on course management page.
         $this->execute("behat_course::i_should_see_the_courses_management_page", get_string('categories'));
 
-        // Select Miscellaneous category.
-        $this->i_click_on_category_in_the_management_interface(get_string('miscellaneous'));
+        // Select default course category.
+        $this->i_click_on_category_in_the_management_interface(get_string('defaultcategoryname'));
         $this->execute("behat_course::i_should_see_the_courses_management_page", get_string('categoriesandcourses'));
 
         // Click create new course.
index 14ad0b2..9b1c9f1 100644 (file)
@@ -195,7 +195,7 @@ Feature: Test category management actions
     And "What to do" "select" should not exist
     And "Move into" "select" should exist
     And the "Move into" select box should contain "Cat 2"
-    And the "Move into" select box should contain "Miscellaneous"
+    And the "Move into" select box should contain "Category 1"
     And I press "Cancel"
 
   @javascript
@@ -230,7 +230,7 @@ Feature: Test category management actions
     And "What to do" "select" should exist
     And I expand the "Move into" autocomplete
     And "Cat 2" "autocomplete_suggestions" should not exist
-    And "Miscellaneous" "autocomplete_selection" should be visible
+    And "Category 1" "autocomplete_selection" should be visible
     And I set the field "What to do" to "Delete all - cannot be undone"
     And "Move into" "select" should not be visible
     And I press "Cancel"
index 18319fb..42e0987 100644 (file)
@@ -44,7 +44,7 @@ Feature: Course category management interface performs as expected
     And I start watching to see if a new page loads
     And I should see the "Course categories and courses" management page
     And I should see "Course categories" in the "#category-listing h3" "css_element"
-    And I should see "Miscellaneous" in the "#course-listing h3" "css_element"
+    And I should see "Category 1" in the "#course-listing h3" "css_element"
     And I should see "Cat 1" in the "#category-listing" "css_element"
     And I should see "No courses in this category" in the "#course-listing" "css_element"
     And I click on category "Cat 1" in the management interface
index 57d844e..23d20c8 100644 (file)
@@ -36,7 +36,7 @@ Feature: Managers can create courses
     And I log in as "admin"
     And I go to the courses management page
     And I should see the "Categories" management page
-    And I click on category "Miscellaneous" in the management interface
+    And I click on category "Category 1" in the management interface
     And I should see the "Course categories and courses" management page
     And I click on "Create new course" "link" in the "#course-listing" "css_element"
     When I set the following fields to these values:
index 19fa953..6fc76aa 100644 (file)
@@ -38,7 +38,7 @@ Feature: Users can request and approve courses
     And I log in as "user2"
     And I am on course index
     And I press "Courses pending approval"
-    And I should see "Miscellaneous" in the "My new course" "table_row"
+    And I should see "Category 1" in the "My new course" "table_row"
     And I click on "Approve" "button" in the "My new course" "table_row"
     And I press "Save and return"
     And I should see "There are no courses pending approval"
index c736f3a..f2664bb 100644 (file)
@@ -7,21 +7,21 @@ Feature: Front page displays items in different modes
   Background:
     Given the following "categories" exist:
       | name                   | category | idnumber |
-      | Category 1             | 0        | CAT1     |
-      | Category 2             | 0        | CAT2     |
-      | Category 1 child       | CAT1     | CAT11    |
-      | Category 2 child       | CAT2     | CAT21    |
-      | Category 1 child child | CAT11    | CAT111   |
-      | Category 3             | 0        | CAT3     |
+      | Category A             | 0        | CATA     |
+      | Category B             | 0        | CATB     |
+      | Category A child       | CATA     | CATA1    |
+      | Category B child       | CATB     | CATB1    |
+      | Category A child child | CATA1    | CATA11   |
+      | Category C             | 0        | CATC     |
     And the following "courses" exist:
       | fullname     | shortname   | category |
-      | Course 1 1   | COURSE1_1   | CAT1     |
-      | Course 2 1   | COURSE2_1   | CAT2     |
-      | Course 11 1  | COURSE11_1  | CAT11    |
-      | Course 2 2   | COURSE2_2   | CAT2     |
-      | Course 21 1  | COURSE21_1  | CAT21    |
-      | Course 111 1 | COURSE111_1 | CAT111   |
-      | Course 111 2 | COURSE111_2 | CAT111   |
+      | Course 1 1   | COURSE1_1   | CATA     |
+      | Course 2 1   | COURSE2_1   | CATB     |
+      | Course 11 1  | COURSE11_1  | CATA1    |
+      | Course 2 2   | COURSE2_2   | CATB     |
+      | Course 21 1  | COURSE21_1  | CATB1    |
+      | Course 111 1 | COURSE111_1 | CATA11   |
+      | Course 111 2 | COURSE111_2 | CATA11   |
     And I log in as "admin"
 
   @javascript
@@ -30,15 +30,15 @@ Feature: Front page displays items in different modes
       | Front page items when logged in | List of categories |
       | Maximum category depth | 2 |
     And I am on site homepage
-    Then I should see "Category 1" in the "region-main" "region"
-    And I should see "Category 1 child" in the "region-main" "region"
-    And I should not see "Category 1 child child" in the "region-main" "region"
-    And I toggle "Category 1" category children visibility in frontpage
-    And I should not see "Category 1 child" in the "region-main" "region"
-    And I toggle "Category 1" category children visibility in frontpage
-    And I should see "Category 1 child" in the "region-main" "region"
-    And I toggle "Category 1 child" category children visibility in frontpage
-    And I should see "Category 1 child child" in the "region-main" "region"
+    Then I should see "Category A" in the "region-main" "region"
+    And I should see "Category A child" in the "region-main" "region"
+    And I should not see "Category A child child" in the "region-main" "region"
+    And I toggle "Category A" category children visibility in frontpage
+    And I should not see "Category A child" in the "region-main" "region"
+    And I toggle "Category A" category children visibility in frontpage
+    And I should see "Category A child" in the "region-main" "region"
+    And I toggle "Category A child" category children visibility in frontpage
+    And I should see "Category A child child" in the "region-main" "region"
 
   @javascript
   Scenario: Displays a combo list
@@ -46,17 +46,17 @@ Feature: Front page displays items in different modes
       | Front page items when logged in | Combo list |
       | Maximum category depth | 2 |
     And I am on site homepage
-    Then I should see "Category 1" in the "region-main" "region"
-    And I should see "Category 1 child" in the "region-main" "region"
-    And I should not see "Category 1 child child" in the "region-main" "region"
+    Then I should see "Category A" in the "region-main" "region"
+    And I should see "Category A child" in the "region-main" "region"
+    And I should not see "Category A child child" in the "region-main" "region"
     And I should see "Course 1 1" in the "region-main" "region"
     And I should see "Course 2 2" in the "region-main" "region"
     And I should not see "Course 11 1" in the "region-main" "region"
-    And I toggle "Category 1 child" category children visibility in frontpage
+    And I toggle "Category A child" category children visibility in frontpage
     And I should see "Course 11 1" in the "region-main" "region"
-    And I should see "Category 1 child child" in the "region-main" "region"
-    And I toggle "Category 1" category children visibility in frontpage
+    And I should see "Category A child child" in the "region-main" "region"
+    And I toggle "Category A" category children visibility in frontpage
     And I should not see "Course 1 1" in the "region-main" "region"
-    And I should not see "Category 1 child" in the "region-main" "region"
-    And I toggle "Category 1" category children visibility in frontpage
+    And I should not see "Category A child" in the "region-main" "region"
+    And I toggle "Category A" category children visibility in frontpage
     And I should see "Course 11 1" in the "region-main" "region"
index 4f7be58..c027e62 100644 (file)
@@ -20,7 +20,7 @@ Feature: Browse course list and return back from enrolment page
   Scenario: A user can return to the category page from enrolment page
     When I log in as "user2"
     And I am on course index
-    And I follow "Miscellaneous"
+    And I follow "Category 1"
     And I follow "Sample course"
     And I press "Continue"
     Then I should see "Courses" in the ".breadcrumb" "css_element"
@@ -67,7 +67,7 @@ Feature: Browse course list and return back from enrolment page
     And I log out
     When I log in as "user1"
     And I am on course index
-    And I follow "Miscellaneous"
+    And I follow "Category 1"
     And I follow "Sample course"
     And I follow "Test choice"
     And I should see "Sorry, only enrolled users are allowed to make choices."
index 467f89d..9ed5fb9 100644 (file)
@@ -330,7 +330,7 @@ class core_course_category_testcase extends advanced_testcase {
         //   $course4
         // structure.
 
-        // Note that we also have default 'Miscellaneous' category and default 'site' course.
+        // Note that we also have default course category and default 'site' course.
         $this->assertEquals(1, $DB->get_field_sql('SELECT count(*) FROM {course_categories} WHERE id > ?', array($initialcatid)));
         $this->assertEquals($category1->id, $DB->get_field_sql('SELECT max(id) FROM {course_categories}'));
         $this->assertEquals(1, $DB->get_field_sql('SELECT count(*) FROM {course} WHERE id <> ?', array(SITEID)));
index d101de6..23839e2 100644 (file)
@@ -42,7 +42,7 @@ Feature: Teachers can edit course custom fields
     When I log in as "admin"
     And I go to the courses management page
     And I should see the "Categories" management page
-    And I click on category "Miscellaneous" in the management interface
+    And I click on category "Category 1" in the management interface
     And I should see the "Course categories and courses" management page
     And I click on "Create new course" "link" in the "#course-listing" "css_element"
     And I set the following fields to these values:
index 20168f6..98c3a4a 100644 (file)
@@ -38,7 +38,6 @@ $context = context_course::instance($course->id, MUST_EXIST);
 require_login($course);
 $canenrol = has_capability('enrol/manual:enrol', $context);
 $canunenrol = has_capability('enrol/manual:unenrol', $context);
-$viewfullnames = has_capability('moodle/site:viewfullnames', $context);
 
 // Note: manage capability not used here because it is used for editing
 // of existing enrolments which is not possible here.
@@ -76,9 +75,7 @@ navigation_node::override_active_url(new moodle_url('/user/index.php', array('id
 $options = array('enrolid' => $enrolid, 'accesscontext' => $context);
 
 $potentialuserselector = new enrol_manual_potential_participant('addselect', $options);
-$potentialuserselector->viewfullnames = $viewfullnames;
 $currentuserselector = new enrol_manual_current_participant('removeselect', $options);
-$currentuserselector->viewfullnames = $viewfullnames;
 
 // Build the list of options for the enrolment period dropdown.
 $unlimitedperiod = get_string('unlimited');
diff --git a/enrol/manual/tests/behat/manage.feature b/enrol/manual/tests/behat/manage.feature
new file mode 100644 (file)
index 0000000..b092d05
--- /dev/null
@@ -0,0 +1,64 @@
+@enrol @enrol_manual
+Feature: A teacher can manage manually enrolled users in their course
+  In order to manage manually enrolled students in my course
+  As a teacher
+  I can manually add and remove users in my course
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | middlename | lastname | email               |
+      | teacher  | Teacher   |            | User     | teacher@example.com |
+      | user1    | First     | Alice      | User     | first@example.com   |
+      | user2    | Second    | Bob        | User     | second@example.com  |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+    And the following "course enrolments" exist:
+      | user    | course | role           |
+      | teacher | C1     | editingteacher |
+
+  @javascript
+  Scenario Outline: Manually enrolling users should observe alternative fullname format
+    Given the following config values are set as admin:
+      | alternativefullnameformat | firstname middlename lastname |
+    And the following "permission overrides" exist:
+      | capability                | permission   | role           | contextlevel | reference |
+      | moodle/site:viewfullnames | <permission> | editingteacher | Course       | C1        |
+    When I log in as "teacher"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Enrolment methods" in current page administration
+    And I click on "Enrol users" "link" in the "Manual enrolments" "table_row"
+    And I set the field "addselect_searchtext" to "First"
+    And I wait "1" seconds
+    And I set the field "Not enrolled users" to "<expectedfullname> (first@example.com)"
+    And I press "Add"
+    Then the "Enrolled users" select box should contain "<expectedfullname> (first@example.com)"
+    Examples:
+      | permission | expectedfullname |
+      | Allow      | First Alice User |
+      | Prohibit   | First User       |
+
+  @javascript
+  Scenario Outline: Manually unenrolling users should observe alternative fullname format
+    Given the following config values are set as admin:
+      | alternativefullnameformat | firstname middlename lastname |
+    And the following "permission overrides" exist:
+      | capability                | permission   | role           | contextlevel | reference |
+      | moodle/site:viewfullnames | <permission> | editingteacher | Course       | C1        |
+    And the following "course enrolments" exist:
+      | user  | course | role    |
+      | user1 | C1     | student |
+      | user2 | C1     | student |
+    When I log in as "teacher"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Enrolment methods" in current page administration
+    And I click on "Enrol users" "link" in the "Manual enrolments" "table_row"
+    And I set the field "removeselect_searchtext" to "First"
+    And I wait "1" seconds
+    And I set the field "Enrolled users" to "<expectedfullname> (first@example.com)"
+    And I press "Remove"
+    Then the "Not enrolled users" select box should contain "<expectedfullname> (first@example.com)"
+    Examples:
+      | permission | expectedfullname |
+      | Allow      | First Alice User |
+      | Prohibit   | First User       |
index 3e951d7..a627077 100644 (file)
@@ -245,7 +245,7 @@ class core_files_externallib_testcase extends advanced_testcase {
                                           'filearea' => null,
                                           'itemid' => null,
                                           'filepath' => null,
-                                          'filename' => 'Miscellaneous');
+                                          'filename' => get_string('defaultcategoryname'));
         $testdata['parents']['2'] = array('contextid' => $coursecontext->id,
                                           'component' => null,
                                           'filearea' => null,
index e09f7e9..c23170a 100644 (file)
          * @param int $fontsize the font size
          * @return string the latex document
          */
-        function construct_latex_document( $formula, $fontsize=12 ) {
-            global $CFG;
-
-            $formula = filter_tex_sanitize_formula($formula);
-
+        function construct_latex_document($formula, $fontsize = 12) {
             // $fontsize don't affects to formula's size. $density can change size
-            $doc =  "\\documentclass[{$fontsize}pt]{article}\n";
+            $doc = "\\documentclass[{$fontsize}pt]{article}\n";
             $doc .= get_config('filter_tex', 'latexpreamble');
             $doc .= "\\pagestyle{empty}\n";
             $doc .= "\\begin{document}\n";
-//dlnsk            $doc .= "$ {$formula} $\n";
-            if (preg_match("/^[[:space:]]*\\\\begin\\{(gather|align|alignat|multline).?\\}/i",$formula)) {
+            if (preg_match("/^[[:space:]]*\\\\begin\\{(gather|align|alignat|multline).?\\}/i", $formula)) {
                $doc .= "$formula\n";
             } else {
                $doc .= "$ {$formula} $\n";
             }
             $doc .= "\\end{document}\n";
+
+            // Sanitize the whole document (rather than just the formula) to make sure no one can bypass sanitization
+            // by using \newcommand in preamble to give an alias to a blocked command.
+            $doc = filter_tex_sanitize_formula($doc);
+
             return $doc;
         }
 
                 $convertformat = 'png';
             }
             $filename = str_replace(".{$convertformat}", '', $filename);
-            $tex = "{$this->temp_dir}/$filename.tex";
+            $tex = "$filename.tex"; // Absolute paths won't work with openin_any = p setting.
             $dvi = "{$this->temp_dir}/$filename.dvi";
             $ps  = "{$this->temp_dir}/$filename.ps";
             $img = "{$this->temp_dir}/$filename.{$convertformat}";
 
+            // Change directory to temp dir so that we can work with relative paths.
+            chdir($this->temp_dir);
+
             // turn the latex doc into a .tex file in the temp area
             $fh = fopen( $tex, 'w' );
             fputs( $fh, $doc );
 
             // run latex on document
             $command = "$pathlatex --interaction=nonstopmode --halt-on-error $tex";
-            chdir( $this->temp_dir );
+
             if ($this->execute($command, $log)) { // It allways False on Windows
 //                return false;
             }
index 875c84c..9a0fe3c 100644 (file)
@@ -86,7 +86,35 @@ function filter_tex_sanitize_formula(string $texexp): string {
         '\noexpand', '\line', '\mathcode', '\item', '\section', '\mbox', '\declarerobustcommand',
     ];
 
-    return str_ireplace($denylist, 'forbiddenkeyword', $texexp);
+    $allowlist = ['inputenc'];
+
+    // Prepare the denylist for regular expression.
+    $denylist = array_map(function($value){
+        return '/' . preg_quote($value, '/') . '/i';
+    }, $denylist);
+
+    // Prepare the allowlist for regular expression.
+    $allowlist = array_map(function($value){
+        return '/\bforbiddenkeyword_(' . preg_quote($value, '/') . ')\b/i';
+    }, $allowlist);
+
+    // First, mangle all denied words.
+    $texexp = preg_replace_callback($denylist,
+        function($matches) {
+            return 'forbiddenkeyword_' . $matches[0];
+        },
+        $texexp
+    );
+
+    // Then, change back the allowed words.
+    $texexp = preg_replace_callback($allowlist,
+        function($matches) {
+            return $matches[1];
+        },
+        $texexp
+    );
+
+    return $texexp;
 }
 
 function filter_tex_get_cmd($pathname, $texexp) {
diff --git a/filter/tex/tests/lib_test.php b/filter/tex/tests/lib_test.php
new file mode 100644 (file)
index 0000000..cc19e2a
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Tex filter library functions tests
+ *
+ * @package   filter_tex
+ * @category  test
+ * @copyright 2021 Shamim Rezaie <shamim@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+declare(strict_types=1);
+
+namespace filter_tex;
+
+use advanced_testcase;
+
+global $CFG;
+require_once($CFG->dirroot . '/filter/tex/lib.php');
+
+/**
+ * Tex filter library functions tests
+ *
+ * @copyright 2021 Shamim Rezaie <shamim@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class lib_test extends advanced_testcase {
+    /**
+     * Data provider for test_filter_tex_sanitize_formula.
+     *
+     * @return array
+     */
+    public function filter_tex_sanitize_formula_provider() : array {
+        return [
+            ['x\ =\ \frac{\sqrt{144}}{2}\ \times\ (y\ +\ 12)', 'x\ =\ \frac{\sqrt{144}}{2}\ \times\ (y\ +\ 12)'],
+            ['\usepackage[latin1]{inputenc}', '\usepackage[latin1]{inputenc}'],
+            ['\newcommand{\A}{\verbatiminput}', '\newforbiddenkeyword_command{\A}{\verbatimforbiddenkeyword_input}'],
+        ];
+    }
+
+    /**
+     * Tests for filter_tex_sanitize_formula() function.
+     *
+     * @dataProvider filter_tex_sanitize_formula_provider
+     * @param $formula The formula to test
+     * @param $expected The sanitized version of the formula we expect to get
+     */
+    public function test_filter_tex_sanitize_formula(string $formula, string $expected) {
+        $this->assertEquals($expected, filter_tex_sanitize_formula($formula));
+    }
+}
index dadbe18..36808f1 100644 (file)
         $output .= "<p>base filename for expression is '$md5'</p>\n";
 
         // temporary paths
-        $tex = "$latex->temp_dir/$md5.tex";
+        $tex = "$md5.tex"; // Absolute paths won't work with openin_any = p setting.
         $dvi = "$latex->temp_dir/$md5.dvi";
         $ps = "$latex->temp_dir/$md5.ps";
         $convertformat = get_config('filter_tex', 'convertformat');
         $img = "$latex->temp_dir/$md5.{$convertformat}";
 
+        // Change directory to temp dir so that we can work with relative paths.
+        chdir($latex->temp_dir);
+
         // put the expression as a file into the temp area
         $expression = html_entity_decode($expression);
         $output .= "<p>Processing TeX expression:</p><pre>$expression</pre>\n";
         fputs($fh, $doc);
         fclose($fh);
 
-        // cd to temp dir
-        chdir($latex->temp_dir);
-
         // step 1: latex command
         $pathlatex = escapeshellarg($pathlatex);
         $cmd = "$pathlatex --interaction=nonstopmode --halt-on-error $tex";
index ab0c1b9..0fbe082 100644 (file)
@@ -49,7 +49,9 @@ class gradeimport_direct_import_form extends moodleform {
 
         $mform->addElement('header', 'general', get_string('pluginname', 'gradeimport_direct'));
         // Data upload from copy/paste.
-        $mform->addElement('textarea', 'userdata', 'Data', array('rows' => 10, 'class' => 'gradeimport_data_area'));
+        $mform->addElement('textarea', 'userdata', get_string('importdata', 'core_grades'),
+            array('rows' => 10, 'class' => 'gradeimport_data_area'));
+        $mform->addHelpButton('userdata', 'importdata', 'core_grades');
         $mform->addRule('userdata', null, 'required');
         $mform->setType('userdata', PARAM_RAW);
 
index 8255c44..9e433a1 100644 (file)
@@ -97,7 +97,7 @@ function UpdatableMembersCombo(wwwRoot, courseId) {
                             var optionEl = document.createElement("option");
                             optionEl.setAttribute("value", roles[i].users[j].id);
                             optionEl.title = roles[i].users[j].name;
-                            optionEl.innerHTML = roles[i].users[j].name;
+                            optionEl.innerHTML = Y.Escape.html(roles[i].users[j].name);
                             optgroupEl.appendChild(optionEl);
                         }
                         selectEl.appendChild(optgroupEl);
index c6b6652..f2296fc 100644 (file)
@@ -104,7 +104,8 @@ switch ($action) {
                     if ($extrafields) {
                         $extrafieldsdisplay = [];
                         foreach ($extrafields as $field) {
-                            $extrafieldsdisplay[] = s($member->{$field});
+                            // No escaping here, handled client side in response to AJAX request.
+                            $extrafieldsdisplay[] = $member->{$field};
                         }
                         $shortmember->name .= ' (' . implode(', ', $extrafieldsdisplay) . ')';
                     }
@@ -200,7 +201,7 @@ if ($groups) {
         $groupoptions[] = (object) [
             'value' => $group->id,
             'selected' => $selected,
-            'text' => $groupname
+            'text' => s($groupname)
         ];
     }
 }
index 30e6d92..4a5b4d1 100644 (file)
                         {{#members}}
                             <optgroup label="{{role}}">
                                 {{#rolemembers}}
-                                    <option value="{{value}}">{{{text}}}‎</option>
+                                    <option value="{{value}}" title="{{{text}}}">{{{text}}}‎</option>
                                 {{/rolemembers}}
                             </optgroup>
                         {{/members}}
index c006061..76fa699 100644 (file)
@@ -55,7 +55,7 @@ $string['pathserrcreatedataroot'] = 'O programa de instalação não conseguiu c
 $string['pathshead'] = 'Confirmar caminhos';
 $string['pathsrodataroot'] = 'A pasta de dados não tem permissões de escrita.';
 $string['pathsroparentdataroot'] = 'A pasta ascendente <b>{$a->parent}</b> não tem permissões de escrita. O programa de instalação não conseguiu criar a pasta <b>{$a->dataroot}</b>.';
-$string['pathssubadmindir'] = 'Alguns servidores Web utilizam a pasta <strong>admin</strong> em URLs especiais de acesso a funcionalidades especiais, como é o caso de painéis de controlo. Algumas situações podem criar conflitos com a localização normal das páginas de administração do Moodle. Estes problemas podem ser resolvidos renomeando a pasta <strong>admin</strong> na instalação do Moodle e indicando aqui o novo nome a utilizar. Por exemplo:<br /><br /><b>moodleadmin</b><br /><br />Esta ação resolverá os problemas de acesso das hiperligações para as funcionalidades de administração do Moodle.';
+$string['pathssubadmindir'] = 'Alguns servidores Web utilizam a pasta <strong>admin</strong> em URLs especiais de acesso a funcionalidades especiais, como é o caso de painéis de controlo. Algumas situações podem criar conflitos com a localização normal das páginas de administração do Moodle. Estes problemas podem ser resolvidos renomeando a pasta <strong>admin</strong> na instalação do Moodle e indicando aqui o novo nome a utilizar. Exemplo:<br /><br /><b>moodleadmin</b><br /><br />Esta ação resolverá os problemas de acesso das hiperligações para as funcionalidades de administração do Moodle.';
 $string['pathssubdataroot'] = '<p>Pasta onde o Moodle irá armazenar todo o conteúdo de ficheiros enviados pelos utilizadores.</p>
 <p>Esta pasta deve ser legível e gravável pelo utilizador do servidor web (geralmente \'www-data\', \'nobody\', ou \'apache\').</p>
 <p>Não deve ser acessível diretamente através da web.</p>
index c94a35c..2098a29 100644 (file)
@@ -34,7 +34,7 @@ $string['availablelangs'] = 'Tillgängliga språkpaket';
 $string['chooselanguagehead'] = 'Välj ett språk';
 $string['chooselanguagesub'] = 'Vänligen välj ett språk för installationen. Du kommer att ha möjlighet att välja språk för webbplatsen och användarna på en senare skärm.';
 $string['clialreadyconfigured'] = 'Filen <em>config.php</em> finns redan. Använd <code>admin/cli/install_database.php</code> för att installera Moodle på denna server.';
-$string['clialreadyinstalled'] = 'Filen config.php finns redan. Vänligen använd admin/cli/upgrade.php om Du vill uppgradera Din webbplats.';
+$string['clialreadyinstalled'] = 'Filen <code>config.php</code> finns redan. Vänligen använd <code>admin/cli/upgrade.php</code> om du vill uppgradera Moodle på den här webbplatsen.';
 $string['cliinstallheader'] = 'Kommandoradsbaserat installationsprogram för Moodle {$a}';
 $string['clitablesexist'] = 'Databastabellerna finns redan. CLI-installationen kan inte fortsätta.';
 $string['databasehost'] = 'Databasserver';
index 645ce63..0e4755e 100644 (file)
@@ -1305,7 +1305,7 @@ $string['task_result:failed'] = 'Fail';
 $string['task_stats:dbreads'] = '{$a} reads';
 $string['task_stats:dbwrites'] = '{$a} writes';
 $string['task_status'] = 'Task status';
-$string['task_status_desc'] = 'The task <q>{$a->name}</q> is <strong>{$a->status}</strong>.<br />See its <a href="{$a->gotourl}">details</a>.<br />Class: {$a->class}{$a->extradescription}';
+$string['task_status_desc'] = 'The task \'{$a->name}\' is {$a->status}. For details, see {$a->class}{$a->extradescription} in <a href="{$a->gotourl}">Scheduled tasks</a>.';
 $string['task_starttime'] = 'Start time';
 $string['task_duration'] = 'Duration';
 $string['task_dbstats'] = 'Database';
@@ -1360,7 +1360,7 @@ $string['templates'] = 'Templates';
 $string['testoutgoingmailconf'] = 'Test outgoing mail configuration';
 $string['testoutgoingmaildetail'] = 'Note: Before testing, please save your configuration.<br />{$a}';
 $string['testoutgoingmailconf_errorcommunications'] = 'Your site couldn\'t communicate with your mail server. Please check your outgoing mail configuration.';
-$string['testoutgoingmailconf_message'] = "This is a test message to confirm that you have successfully configured your site's outgoing mail.\n\n Sent:" . '{$a}';
+$string['testoutgoingmailconf_message'] = 'This is a test message to confirm that you have successfully configured your site\'s outgoing mail.  Sent: {$a}';
 $string['testoutgoingmailconf_fromemail'] = 'From username or email address';
 $string['testoutgoingmailconf_fromemail_help'] = 'This field emulates sending the message from that user, but the From header used in the real email sent will depend on other settings such as allowedemaildomains';
 $string['testoutgoingmailconf_fromemail_invalid'] = 'Invalid From username or email. Must be a valid email format or an existing username in Moodle.';
index 05169ff..78bf434 100644 (file)
@@ -289,7 +289,7 @@ $string['originalwwwroot'] = 'URL of backup';
 $string['overwrite'] = 'Overwrite';
 $string['pendingasyncdetail'] = 'Asynchronous backups only allow a user to have one pending backup for a resource at a time. <br/> Multiple asynchronous backups of the same resource can\'t be queued, as this would likely result in multiple backups with the same content.';
 $string['pendingasyncdeletedetail'] = 'This course has an asynchronous backup pending. <br/> Courses can\'t be deleted until this backup finishes.';
-$string['pendingasyncedit'] = 'There is a pending asynchronous backup for this course. Please do not edit this course until backup is complete.';
+$string['pendingasyncedit'] = 'There is a pending backup or copy requested for this course. Please do not edit the course until this is complete.';
 $string['pendingasyncerror'] = 'Backup pending for this resource';
 $string['previousstage'] = 'Previous';
 $string['preparingui'] = 'Preparing to display page';
index ed467bf..0524cbb 100644 (file)
@@ -140,9 +140,7 @@ $string['backpackprovider'] = 'Backpack provider';
 $string['badges']&