Merge branch 'MDL-63915_master' of git://github.com/markn86/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Wed, 27 Feb 2019 04:08:09 +0000 (12:08 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Wed, 27 Feb 2019 04:08:09 +0000 (12:08 +0800)
288 files changed:
admin/cli/install_database.php
admin/settings.php
admin/templates/setting.mustache
admin/templates/setting_configcolourpicker.mustache
admin/templates/setting_configduration.mustache
admin/templates/setting_configfile.mustache
admin/templates/setting_configmultiselect.mustache
admin/templates/setting_configmultiselect_optgroup.mustache
admin/templates/setting_configselect.mustache
admin/templates/setting_configselect_optgroup.mustache
admin/templates/setting_configtext.mustache
admin/templates/setting_configtextarea.mustache
admin/templates/setting_configtime.mustache
admin/templates/setting_courselist_frontpage.mustache
admin/templates/setting_description.mustache
admin/templates/setting_devicedetectregex.mustache
admin/templates/setting_emoticons.mustache
admin/templates/setting_gradecat_combo.mustache
admin/templates/settings.mustache
admin/templates/settings_search_results.mustache
admin/tests/behat/behat_admin.php
admin/tool/behat/tests/fixtures/core/behat_test_context_1.php
admin/tool/behat/tests/fixtures/core/behat_test_context_2.php
admin/tool/behat/tests/fixtures/theme/defaulttheme/behat_theme_defaulttheme_test_context_1.php
admin/tool/behat/tests/fixtures/theme/nofeatures/behat_theme_nofeatures_behat_test_context_2.php
admin/tool/behat/tests/fixtures/theme/nofeatures/behat_theme_nofeatures_test_context_1.php
admin/tool/behat/tests/fixtures/theme/withfeatures/behat_theme_withfeatures_behat_test_context_1.php
admin/tool/behat/tests/fixtures/theme/withfeatures/behat_theme_withfeatures_test_context_2.php
admin/tool/behat/tests/manager_util_test.php
admin/tool/behat/upgrade.txt
admin/tool/lp/templates/progress_bar.mustache
admin/tool/task/tests/behat/clear_fail_delay.feature
admin/tool/task/tests/behat/manage_tasks.feature
admin/tool/usertours/amd/build/managesteps.min.js
admin/tool/usertours/amd/build/managetours.min.js
admin/tool/usertours/amd/src/managesteps.js
admin/tool/usertours/amd/src/managetours.js
admin/tool/usertours/classes/helper.php
admin/tool/usertours/templates/tourstep.mustache
blocks/admin_bookmarks/tests/behat/bookmark_admin_pages.feature
blocks/myoverview/amd/build/view.min.js
blocks/myoverview/amd/src/view.js
blocks/recentlyaccessedcourses/amd/build/main.min.js
blocks/recentlyaccessedcourses/amd/src/main.js
blocks/recentlyaccessedcourses/classes/output/main.php
blocks/recentlyaccessedcourses/templates/course-card.mustache [new file with mode: 0644]
blocks/recentlyaccessedcourses/templates/no-courses.mustache [deleted file]
blocks/recentlyaccessedcourses/templates/recentlyaccessedcourses-view.mustache
blocks/search_forums/templates/search_form.mustache
blocks/settings/renderer.php
blocks/settings/templates/search_form.mustache [moved from theme/boost/templates/block_settings/search_form.mustache with 100% similarity]
blocks/tests/behat/behat_blocks.php
calendar/templates/minicalendar_day_link.mustache
course/classes/management_renderer.php
course/renderer.php
course/templates/course_search_form.mustache [moved from theme/boost/templates/course_search_form.mustache with 100% similarity]
course/templates/coursecard.mustache [new file with mode: 0644]
course/templates/coursecards.mustache
course/tests/behat/behat_course.php
enrol/self/tests/self_test.php
files/renderer.php
grade/grading/form/guide/templates/comment_chooser.mustache
grade/report/history/classes/output/renderer.php
grade/report/history/templates/user_button.mustache [moved from theme/boost/templates/gradereport_history/user_button.mustache with 100% similarity]
grade/report/singleview/templates/bulk_insert.mustache
grade/report/singleview/templates/button.mustache
grade/report/singleview/templates/dropdown_attribute.mustache
grade/report/singleview/templates/text_attribute.mustache
grade/tests/behat/behat_grade.php
grade/tests/importlib_test.php
grades/templates/edit_tree.mustache [moved from theme/boost/templates/core_grades/edit_tree.mustache with 100% similarity]
grades/templates/weight_field.mustache [moved from theme/boost/templates/core_grades/weight_field.mustache with 100% similarity]
grades/templates/weight_override_field.mustache [moved from theme/boost/templates/core_grades/weight_override_field.mustache with 100% similarity]
lang/en/admin.php
lib/adminlib.php
lib/amd/build/paged_content_paging_bar.min.js
lib/amd/build/showhidesettings.min.js [new file with mode: 0644]
lib/amd/src/paged_content_paging_bar.js
lib/amd/src/showhidesettings.js [new file with mode: 0644]
lib/behat/classes/behat_config_util.php
lib/behat/classes/behat_context_helper.php
lib/behat/core_behat_file_helper.php [moved from lib/behat/behat_files.php with 94% similarity]
lib/outputrenderers.php
lib/templates/action_menu.mustache
lib/templates/action_menu_link.mustache
lib/templates/action_menu_trigger.mustache
lib/templates/auth_digital_minor_page.mustache
lib/templates/auth_verify_age_location_page.mustache
lib/templates/availability_info.mustache
lib/templates/block.mustache [moved from theme/boost/templates/core/block.mustache with 100% similarity]
lib/templates/chooser.mustache
lib/templates/columns-1to1to1.mustache
lib/templates/columns-1to2.mustache
lib/templates/columns-2to1.mustache
lib/templates/custom_menu_item.mustache [moved from theme/boost/templates/core/custom_menu_item.mustache with 100% similarity]
lib/templates/dataformat_selector.mustache
lib/templates/filemanager_confirmdialog.mustache [moved from theme/boost/templates/core/filemanager_confirmdialog.mustache with 100% similarity]
lib/templates/filemanager_default_searchform.mustache [moved from theme/boost/templates/core/filemanager_default_searchform.mustache with 100% similarity]
lib/templates/filemanager_fileselect.mustache [moved from theme/boost/templates/core/filemanager_fileselect.mustache with 100% similarity]
lib/templates/filemanager_loginform.mustache [moved from theme/boost/templates/core/filemanager_loginform.mustache with 100% similarity]
lib/templates/filemanager_modal_generallayout.mustache [moved from theme/boost/templates/core/filemanager_modal_generallayout.mustache with 100% similarity]
lib/templates/filemanager_page_generallayout.mustache [moved from theme/boost/templates/core/filemanager_page_generallayout.mustache with 100% similarity]
lib/templates/filemanager_processexistingfile.mustache [moved from theme/boost/templates/core/filemanager_processexistingfile.mustache with 100% similarity]
lib/templates/filemanager_processexistingfilemultiple.mustache [moved from theme/boost/templates/core/filemanager_processexistingfilemultiple.mustache with 100% similarity]
lib/templates/filemanager_selectlayout.mustache [moved from theme/boost/templates/core/filemanager_selectlayout.mustache with 100% similarity]
lib/templates/filemanager_uploadform.mustache [moved from theme/boost/templates/core/filemanager_uploadform.mustache with 100% similarity]
lib/templates/form_autocomplete_input.mustache
lib/templates/form_autocomplete_selection.mustache
lib/templates/full_header.mustache [moved from theme/boost/templates/header.mustache with 100% similarity]
lib/templates/help_icon.mustache
lib/templates/initials_bar.mustache
lib/templates/loginform.mustache
lib/templates/modal.mustache
lib/templates/modal_backdrop.mustache
lib/templates/navbar.mustache [moved from theme/boost/templates/core/navbar.mustache with 100% similarity]
lib/templates/notification_error.mustache
lib/templates/notification_info.mustache
lib/templates/notification_success.mustache
lib/templates/notification_warning.mustache
lib/templates/preferences_groups.mustache [moved from theme/boost/templates/core/preferences_groups.mustache with 100% similarity]
lib/templates/progress_bar.mustache
lib/templates/select_time.mustache
lib/templates/settings_link_page.mustache
lib/templates/settings_link_page_single.mustache
lib/templates/signup_form_layout.mustache
lib/templates/single_button.mustache
lib/templates/skip_links.mustache
lib/templates/tabtree.mustache [moved from theme/boost/templates/core/tabtree.mustache with 100% similarity]
lib/tests/behat/behat_action_menu.php
lib/tests/behat/behat_deprecated.php
lib/tests/behat/behat_navigation.php
lib/tests/weblib_test.php
lib/upgrade.txt
lib/weblib.php
message/classes/api.php
message/tests/behat/message_admin_settings.feature [new file with mode: 0644]
message/tests/externallib_test.php
mod/assign/classes/task/cron_task.php [new file with mode: 0644]
mod/assign/db/tasks.php [new file with mode: 0644]
mod/assign/lang/en/assign.php
mod/assign/lib.php
mod/assign/templates/grading_actions.mustache
mod/assign/templates/grading_navigation.mustache
mod/assign/templates/grading_navigation_user_selector.mustache
mod/assign/version.php
mod/feedback/amd/build/edit.min.js
mod/feedback/amd/src/edit.js
mod/forum/templates/big_search_form.mustache
mod/forum/templates/quick_search_form.mustache
mod/lesson/locallib.php
mod/quiz/accessrule/timelimit/rule.php
mod/quiz/accessrule/timelimit/tests/rule_test.php
mod/quiz/classes/task/legacy_quiz_accessrules_cron.php [new file with mode: 0644]
mod/quiz/classes/task/legacy_quiz_reports_cron.php [moved from theme/boost/classes/output/gradereport_history_renderer.php with 58% similarity]
mod/quiz/classes/task/update_overdue_attempts.php [new file with mode: 0644]
mod/quiz/db/tasks.php [new file with mode: 0644]
mod/quiz/lang/en/quiz.php
mod/quiz/lib.php
mod/quiz/report/statistics/classes/task/quiz_statistics_cleanup.php [new file with mode: 0644]
mod/quiz/report/statistics/db/tasks.php [new file with mode: 0644]
mod/quiz/report/statistics/lang/en/quiz_statistics.php
mod/quiz/report/statistics/lib.php
mod/quiz/report/statistics/version.php
mod/quiz/tests/behat/behat_mod_quiz.php
mod/quiz/upgrade.txt
mod/quiz/version.php
mod/wiki/lib.php
mod/wiki/version.php
mod/workshop/mod_form.php
mod/workshop/tests/behat/grade_to_pass.feature
question/renderer.php
repository/tests/behat/behat_filepicker.php
repository/upload/tests/behat/behat_repository_upload.php
theme/boost/amd/build/drawer.min.js
theme/boost/amd/src/drawer.js
theme/boost/classes/output/core/files_renderer.php [deleted file]
theme/boost/classes/output/core_renderer.php
theme/boost/classes/output/core_renderer_maintenance.php [deleted file]
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/core.scss
theme/boost/style/moodle.css
theme/boost/templates/block_search_forums/search_form.mustache [deleted file]
theme/boost/templates/core/action_menu_trigger.mustache [deleted file]
theme/boost/templates/core/auth_digital_minor_page.mustache [deleted file]
theme/boost/templates/core/auth_verify_age_location_page.mustache [deleted file]
theme/boost/templates/core/availability_info.mustache [deleted file]
theme/boost/templates/core/help_icon.mustache [deleted file]
theme/boost/templates/core/loginform.mustache [deleted file]
theme/boost/templates/core/settings_link_page.mustache [deleted file]
theme/boost/templates/core/signup_form_layout.mustache [deleted file]
theme/boost/templates/core_admin/setting_configduration.mustache [deleted file]
theme/boost/templates/core_admin/setting_configmultiselect_optgroup.mustache [deleted file]
theme/boost/templates/core_admin/setting_configtime.mustache [deleted file]
theme/boost/templates/core_calendar/minicalendar_day_link.mustache [deleted file]
theme/boost/templates/mod_forum/quick_search_form.mustache [deleted file]
theme/boost/tests/behat/behat_theme_boost_behat_blocks.php [deleted file]
theme/boost/tests/behat/behat_theme_boost_behat_navigation.php [deleted file]
theme/boost/upgrade.txt
theme/boost/version.php
theme/bootstrapbase/less/moodle/admin.less
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/forms.less
theme/bootstrapbase/less/moodle/responsive.less
theme/bootstrapbase/renderers.php
theme/bootstrapbase/renderers/block_settings_renderer.php [moved from theme/boost/classes/output/block_settings_renderer.php with 60% similarity]
theme/bootstrapbase/renderers/core/course_renderer.php [moved from theme/boost/classes/output/core/course_renderer.php with 62% similarity]
theme/bootstrapbase/renderers/core/files_renderer.php [new file with mode: 0644]
theme/bootstrapbase/renderers/core_course/management/renderer.php [moved from theme/boost/classes/output/core_course/management/renderer.php with 56% similarity]
theme/bootstrapbase/renderers/core_question/bank_renderer.php [moved from theme/boost/classes/output/core_question/bank_renderer.php with 62% similarity]
theme/bootstrapbase/renderers/core_renderer.php
theme/bootstrapbase/renderers/gradereport_history_renderer.php [new file with mode: 0644]
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/block_search_forums/search_form.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/core/action_menu.mustache [moved from theme/boost/templates/core/action_menu.mustache with 55% similarity]
theme/bootstrapbase/templates/core/action_menu_item.mustache [moved from theme/boost/templates/core/action_menu_item.mustache with 100% similarity]
theme/bootstrapbase/templates/core/action_menu_link.mustache [moved from theme/boost/templates/core/action_menu_link.mustache with 88% similarity]
theme/bootstrapbase/templates/core/action_menu_trigger.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/core/auth_digital_minor_page.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/core/auth_verify_age_location_page.mustache [moved from theme/boost/templates/core_admin/setting_configfile.mustache with 57% similarity]
theme/bootstrapbase/templates/core/chooser.mustache [moved from theme/boost/templates/core/chooser.mustache with 94% similarity]
theme/bootstrapbase/templates/core/columns-1to1to1.mustache [moved from theme/boost/templates/core/columns-1to1to1.mustache with 86% similarity]
theme/bootstrapbase/templates/core/columns-1to2.mustache [moved from theme/boost/templates/core/columns-1to2.mustache with 88% similarity]
theme/bootstrapbase/templates/core/columns-2to1.mustache [moved from theme/boost/templates/core/columns-2to1.mustache with 88% similarity]
theme/bootstrapbase/templates/core/dataformat_selector.mustache [moved from theme/boost/templates/core/dataformat_selector.mustache with 68% similarity]
theme/bootstrapbase/templates/core/form_autocomplete_input.mustache [moved from theme/boost/templates/core/form_autocomplete_input.mustache with 83% similarity]
theme/bootstrapbase/templates/core/form_autocomplete_selection.mustache [moved from theme/boost/templates/core/form_autocomplete_selection.mustache with 83% similarity]
theme/bootstrapbase/templates/core/help_icon.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/core/initials_bar.mustache [moved from theme/boost/templates/core/initials_bar.mustache with 75% similarity]
theme/bootstrapbase/templates/core/loginform.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/core/modal.mustache [moved from theme/boost/templates/core/modal.mustache with 53% similarity]
theme/bootstrapbase/templates/core/modal_backdrop.mustache [moved from theme/boost/templates/core/modal_backdrop.mustache with 91% similarity]
theme/bootstrapbase/templates/core/notification_error.mustache [moved from theme/boost/templates/core/notification_error.mustache with 84% similarity]
theme/bootstrapbase/templates/core/notification_info.mustache [moved from theme/boost/templates/core/notification_info.mustache with 88% similarity]
theme/bootstrapbase/templates/core/notification_success.mustache [moved from theme/boost/templates/core/notification_success.mustache with 88% similarity]
theme/bootstrapbase/templates/core/notification_warning.mustache [moved from theme/boost/templates/core/notification_warning.mustache with 88% similarity]
theme/bootstrapbase/templates/core/progress_bar.mustache [moved from theme/boost/templates/core/progress_bar.mustache with 67% similarity]
theme/bootstrapbase/templates/core/select_time.mustache [moved from theme/boost/templates/core/select_time.mustache with 92% similarity]
theme/bootstrapbase/templates/core/settings_link_page.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/core/settings_link_page_single.mustache [moved from theme/boost/templates/core/settings_link_page_single.mustache with 96% similarity]
theme/bootstrapbase/templates/core/signup_form_layout.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/core/single_button.mustache [moved from theme/boost/templates/core/single_button.mustache with 97% similarity]
theme/bootstrapbase/templates/core/skip_links.mustache [moved from theme/boost/templates/core/skip_links.mustache with 60% similarity]
theme/bootstrapbase/templates/core_admin/setting.mustache [moved from theme/boost/templates/core_admin/setting.mustache with 50% similarity]
theme/bootstrapbase/templates/core_admin/setting_configcolourpicker.mustache [moved from theme/boost/templates/core_admin/setting_configcolourpicker.mustache with 67% similarity]
theme/bootstrapbase/templates/core_admin/setting_configduration.mustache [moved from theme/boost/templates/core_admin/setting_configselect.mustache with 53% similarity]
theme/bootstrapbase/templates/core_admin/setting_configfile.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/core_admin/setting_configmultiselect.mustache [moved from theme/boost/templates/core_admin/setting_configmultiselect.mustache with 60% similarity]
theme/bootstrapbase/templates/core_admin/setting_configmultiselect_optgroup.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/core_admin/setting_configselect.mustache [moved from theme/boost/templates/core_admin/setting_configselect_optgroup.mustache with 62% similarity]
theme/bootstrapbase/templates/core_admin/setting_configselect_optgroup.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/core_admin/setting_configtext.mustache [moved from theme/boost/templates/core_admin/setting_configtext.mustache with 52% similarity]
theme/bootstrapbase/templates/core_admin/setting_configtextarea.mustache [moved from theme/boost/templates/core_admin/setting_configtextarea.mustache with 56% similarity]
theme/bootstrapbase/templates/core_admin/setting_configtime.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/core_admin/setting_courselist_frontpage.mustache [moved from theme/boost/templates/core_admin/setting_courselist_frontpage.mustache with 57% similarity]
theme/bootstrapbase/templates/core_admin/setting_description.mustache [moved from theme/boost/templates/core_admin/setting_description.mustache with 85% similarity]
theme/bootstrapbase/templates/core_admin/setting_devicedetectregex.mustache [moved from theme/boost/templates/core_admin/setting_devicedetectregex.mustache with 69% similarity]
theme/bootstrapbase/templates/core_admin/setting_emoticons.mustache [moved from theme/boost/templates/core_admin/setting_emoticons.mustache with 80% similarity]
theme/bootstrapbase/templates/core_admin/setting_gradecat_combo.mustache [moved from theme/boost/templates/core_admin/setting_gradecat_combo.mustache with 69% similarity]
theme/bootstrapbase/templates/core_admin/settings.mustache [moved from theme/boost/templates/core_admin/settings.mustache with 62% similarity]
theme/bootstrapbase/templates/core_admin/settings_search_results.mustache [moved from theme/boost/templates/core_admin/settings_search_results.mustache with 91% similarity]
theme/bootstrapbase/templates/gradereport_singleview/bulk_insert.mustache [moved from theme/boost/templates/gradereport_singleview/bulk_insert.mustache with 92% similarity]
theme/bootstrapbase/templates/gradereport_singleview/button.mustache [moved from theme/boost/templates/gradereport_singleview/button.mustache with 89% similarity]
theme/bootstrapbase/templates/gradereport_singleview/dropdown_attribute.mustache [moved from theme/boost/templates/gradereport_singleview/dropdown_attribute.mustache with 89% similarity]
theme/bootstrapbase/templates/gradereport_singleview/text_attribute.mustache [moved from theme/boost/templates/gradereport_singleview/text_attribute.mustache with 89% similarity]
theme/bootstrapbase/templates/gradingform_guide/comment_chooser.mustache [moved from theme/boost/templates/gradingform_guide/comment_chooser.mustache with 87% similarity]
theme/bootstrapbase/templates/mod_assign/grading_actions.mustache [moved from theme/boost/templates/mod_assign/grading_actions.mustache with 75% similarity]
theme/bootstrapbase/templates/mod_assign/grading_navigation.mustache [moved from theme/boost/templates/mod_assign/grading_navigation.mustache with 90% similarity]
theme/bootstrapbase/templates/mod_assign/grading_navigation_user_selector.mustache [moved from theme/boost/templates/mod_assign/grading_navigation_user_selector.mustache with 57% similarity]
theme/bootstrapbase/templates/mod_forum/big_search_form.mustache [moved from theme/boost/templates/mod_forum/big_search_form.mustache with 64% similarity]
theme/bootstrapbase/templates/mod_forum/quick_search_form.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/tool_lp/progress_bar.mustache [moved from theme/boost/templates/tool_lp/progress_bar.mustache with 69% similarity]
theme/bootstrapbase/templates/tool_usertours/tourstep.mustache [moved from theme/boost/templates/tool_usertours/tourstep.mustache with 66% similarity]
theme/bootstrapbase/tests/behat/behat_theme_bootstrapbase_behat_action_menu.php [moved from theme/boost/tests/behat/behat_theme_boost_behat_action_menu.php with 71% similarity]
theme/bootstrapbase/tests/behat/behat_theme_bootstrapbase_behat_admin.php [moved from theme/boost/tests/behat/behat_theme_boost_behat_admin.php with 68% similarity]
theme/bootstrapbase/tests/behat/behat_theme_bootstrapbase_behat_blocks.php [new file with mode: 0644]
theme/bootstrapbase/tests/behat/behat_theme_bootstrapbase_behat_course.php [moved from theme/boost/tests/behat/behat_theme_boost_behat_course.php with 68% similarity]
theme/bootstrapbase/tests/behat/behat_theme_bootstrapbase_behat_deprecated.php [moved from theme/boost/tests/behat/behat_theme_boost_behat_deprecated.php with 59% similarity]
theme/bootstrapbase/tests/behat/behat_theme_bootstrapbase_behat_filepicker.php [moved from theme/boost/tests/behat/behat_theme_boost_behat_filepicker.php with 57% similarity]
theme/bootstrapbase/tests/behat/behat_theme_bootstrapbase_behat_grade.php [moved from theme/boost/tests/behat/behat_theme_boost_behat_grade.php with 72% similarity]
theme/bootstrapbase/tests/behat/behat_theme_bootstrapbase_behat_mod_quiz.php [moved from theme/boost/tests/behat/behat_theme_boost_behat_mod_quiz.php with 79% similarity]
theme/bootstrapbase/tests/behat/behat_theme_bootstrapbase_behat_navigation.php [new file with mode: 0644]
theme/bootstrapbase/tests/behat/behat_theme_bootstrapbase_behat_repository_upload.php [moved from theme/boost/tests/behat/behat_theme_boost_behat_repository_upload.php with 74% similarity]
theme/bootstrapbase/tests/behat/blacklist.json [new file with mode: 0644]
theme/bootstrapbase/tests/behat/theme_bootstrapbase_behat_file_helper.php [moved from theme/boost/tests/behat/behat_theme_boost_behat_files.php with 73% similarity]
theme/bootstrapbase/upgrade.txt
theme/bootstrapbase/version.php
theme/upgrade.txt

index 0dd6122..8b5fe93 100644 (file)
@@ -82,11 +82,6 @@ require_once($CFG->libdir.'/installlib.php');
 require_once($CFG->libdir.'/adminlib.php');
 require_once($CFG->libdir.'/componentlib.class.php');
 
-// make sure no tables are installed yet
-if ($DB->get_tables() ) {
-    cli_error(get_string('clitablesexist', 'install'));
-}
-
 $CFG->early_install_lang = true;
 get_string_manager(true);
 
@@ -109,12 +104,17 @@ list($options, $unrecognized) = cli_get_params(
     )
 );
 
-
+// We show help text even if tables are installed.
 if ($options['help']) {
     echo $help;
     die;
 }
 
+// Make sure no tables are installed yet.
+if ($DB->get_tables() ) {
+    cli_error(get_string('clitablesexist', 'install'));
+}
+
 if (!$options['agree-license']) {
     cli_error('You have to agree to the license. --help prints out the help'); // TODO: localize
 }
index ecd2455..0872e03 100644 (file)
@@ -156,4 +156,11 @@ $PAGE->requires->yui_module('moodle-core-formchangechecker',
 );
 $PAGE->requires->string_for_js('changesmadereallygoaway', 'moodle');
 
+if ($settingspage->has_dependencies()) {
+    $opts = [
+        'dependencies' => $settingspage->get_dependencies_for_javascript()
+    ];
+    $PAGE->requires->js_call_amd('core/showhidesettings', 'init', [$opts]);
+}
+
 echo $OUTPUT->footer();
index ed17246..af78e7e 100644 (file)
@@ -29,6 +29,7 @@
     * element - The Element HTML
     * forceltr - Force this element to be displayed LTR
     * default - Default value
+    * dependenton - optional message listing the settings this one is dependent on
 
     Example context (json):
     {
         "default": "Default value"
     }
 }}
-<div class="form-item clearfix" id="{{id}}">
-    <div class="form-label">
+{{!
+    Setting.
+}}
+<div class="form-item row" id="{{id}}">
+    <div class="form-label col-sm-3 text-sm-right">
         <label {{#labelfor}}for="{{labelfor}}"{{/labelfor}}>
             {{{title}}}
             {{#override}}
                 <div class="form-warning">{{warning}}</div>
             {{/warning}}
         </label>
-        <span class="form-shortname">{{{name}}}</span>
+        <span class="form-shortname d-block small text-muted">{{{name}}}</span>
     </div>
-    <div class="form-setting">
+    <div class="form-setting col-sm-9">
         {{#error}}
             <div><span class="error">{{error}}</span></div>
         {{/error}}
         {{{element}}}
         {{#default}}
-            <div class="form-defaultinfo {{#forceltr}}text-ltr{{/forceltr}}">{{{default}}}</div>
+            <div class="form-defaultinfo text-muted {{#forceltr}}text-ltr{{/forceltr}}">{{{default}}}</div>
         {{/default}}
+        <div class="form-description mt-3">{{{description}}}</div>
+        {{#dependenton}}<div class="form-dependenton mb-4 text-muted">{{{.}}}</div>{{/dependenton}}
     </div>
-    <div class="form-description">{{{description}}}</div>
 </div>
index eb8c487..3de5aac 100644 (file)
         "haspreviewconfig": false
     }
 }}
+{{!
+    Setting configcolourpicker.
+}}
 <div class="form-colourpicker defaultsnext">
     <div class="admin_colourpicker clearfix">
         {{#icon}}
             {{>core/pix_icon}}
         {{/icon}}
     </div>
-    <input type="text" name="{{name}}" id="{{id}}" value="{{value}}" size="12" class="text-ltr">
+    <input type="text" name="{{name}}" id="{{id}}" value="{{value}}" size="12" class="form-control text-ltr">
     {{#haspreviewconfig}}
         <input type="button" id="{{id}}_preview" value={{#quote}}{{#str}}preview{{/str}}{{/quote}} class="admin_colourpicker_preview">
     {{/haspreviewconfig}}
index 3bad98c..d2faa4a 100644 (file)
         "options": [ { "name": "Minutes", "value": "mins", "selected": true } ]
     }
 }}
+{{!
+    Setting configduration.
+}}
 <div class="form-duration defaultsnext">
-    <input type="text" size="5" id="{{id}}v" name="{{name}}[v]" value="{{value}}" class="text-ltr">
-    <label class="accesshide" for="{{id}}u">{{#str}}durationunits, admin{{/str}}</label>
-    <select id="{{id}}u" name="{{name}}[u]">
-        {{#options}}
-            <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
-        {{/options}}
-    </select>
+    <div class="form-inline">
+        <input type="text" size="5" id="{{id}}v" name="{{name}}[v]" value="{{value}}" class="form-control text-ltr">
+        <label class="sr-only" for="{{id}}u">{{#str}}durationunits, admin{{/str}}</label>
+        <select id="{{id}}u" name="{{name}}[u]" class="form-control">
+            {{#options}}
+                <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
+            {{/options}}
+        </select>
+    </div>
 </div>
 
index f249fe6..1f0b5d5 100644 (file)
         "valid": false
     }
 }}
+{{!
+    Setting configfile.
+}}
 <div class="form-file defaultsnext">
-    <input type="text" name="{{name}}" id="{{id}}" size="{{size}}" value="{{value}}" class="text-ltr" {{#readonly}}readonly{{/readonly}}>
-    {{#showvalidity}}
-        {{#valid}}
-            <span class="pathok">&#x2714;</span>
-        {{/valid}}
-        {{^valid}}
-            <span class="patherror">&#x2718;</span>
-        {{/valid}}
-    {{/showvalidity}}
+    <div class="form-inline">
+        <input type="text" name="{{name}}" id="{{id}}" size="{{size}}" value="{{value}}" class="form-control text-ltr" {{#readonly}}readonly{{/readonly}}>
+        {{#showvalidity}}
+            {{#valid}}
+                <span class="text-success">&#x2714;</span>
+            {{/valid}}
+            {{^valid}}
+                <span class="text-danger">&#x2718;</span>
+            {{/valid}}
+        {{/showvalidity}}
+    </div>
 </div>
 
index 7465a7b..49eb55c 100644 (file)
                      { "name": "Option 2", "value": "V", "selected": true } ]
     }
 }}
+{{!
+    Setting configmultiselect.
+}}
 <div class="form-select">
     <input type="hidden" name="{{name}}[xxxxx]" value="1">
-    <select id="{{id}}" name="{{name}}[]" size="{{size}}" multiple>
+    <select id="{{id}}" name="{{name}}[]" size="{{size}}" class="form-control" multiple>
         {{#options}}
             <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
         {{/options}}
index 9fa61c5..20d66e1 100644 (file)
         ]
     }
 }}
+{{!
+    Setting configmultiselect with optgroup support.
+}}
 <div class="form-select">
     <input type="hidden" name="{{name}}[xxxxx]" value="1">
-    <select id="{{id}}" name="{{name}}[]" size="{{size}}" multiple>
-        {{#options}}
-            <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
-        {{/options}}
-        {{#optgroups}}
+    <select id="{{id}}" name="{{name}}[]" size="{{size}}" class="form-control" multiple>
+    {{#options}}
+        <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
+    {{/options}}
+    {{#optgroups}}
             <optgroup label="{{label}}">
                 {{#options}}
                     <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
index a90c260..9b6c6e1 100644 (file)
         ]
     }
 }}
+{{!
+    Setting configselect.
+}}
 <div class="form-select defaultsnext">
-    <select id="{{id}}" name="{{name}}">
+    <select id="{{id}}" name="{{name}}" class="custom-select">
         {{#options}}
             <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
         {{/options}}
index ea5da12..95f818b 100644 (file)
         ]
     }
 }}
+{{!
+    Setting configselect with optgroup support.
+}}
 <div class="form-select defaultsnext">
-    <select id="{{id}}" name="{{name}}">
+    <select id="{{id}}" name="{{name}}" class="custom-select">
         {{#options}}
             <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
         {{/options}}
@@ -66,3 +69,4 @@
     </select>
 </div>
 
+
index 236228a..a31f73a 100644 (file)
@@ -37,6 +37,9 @@
         "attributes": [ { "name": "readonly", "value": "readonly" } ]
     }
 }}
+{{!
+    Setting configtext.
+}}
 <div class="form-text defaultsnext">
-    <input type="text" name="{{name}}" value="{{value}}" size="{{size}}" id="{{id}}" class="{{#forceltr}}text-ltr{{/forceltr}}"{{#attributes}} {{name}}="{{value}}"{{/attributes}}>
+    <input type="text" name="{{name}}" value="{{value}}" size="{{size}}" id="{{id}}" class="form-control {{#forceltr}}text-ltr{{/forceltr}}">
 </div>
index ec6ded3..002b0cd 100644 (file)
@@ -36,6 +36,9 @@
         "id": "test0"
     }
 }}
+{{!
+    Setting configtextarea.
+}}
 <div class="form-textarea">
-    <textarea rows="{{rows}}" cols="{{cols}}" id="{{id}}" name="{{name}}" spellcheck="true" class="{{#forceltr}}text-ltr{{/forceltr}}">{{value}}</textarea>
+    <textarea rows="{{rows}}" cols="{{cols}}" id="{{id}}" name="{{name}}" spellcheck="true" class="form-control {{#forceltr}}text-ltr{{/forceltr}}">{{value}}</textarea>
 </div>
index b86b691..90d37b3 100644 (file)
         ]
     }
 }}
-<div class="form-time defaultsnext text-ltr">
-    <label class="accesshide" for="{{id}}h">{{#str}}hours{{/str}}</label>
-    <select id="{{id}}h" name="{{name}}[h]">
-        {{#hours}}
-            <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
-        {{/hours}}
-    </select>:
-    <label class="accesshide" for="{{id}}m">{{#str}}minutes{{/str}}</label>
-    <select id="{{id}}m" name="{{name}}[m]">
-        {{#minutes}}
-            <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
-        {{/minutes}}
-    </select>
+{{!
+    Setting configtime.
+}}
+<div class="form-time defaultsnext">
+    <div class="form-inline text-ltr">
+        <label class="sr-only" for="{{id}}h">{{#str}}hours{{/str}}</label>
+        <select id="{{id}}h" name="{{name}}[h]" class="custom-select">
+            {{#hours}}
+                <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
+            {{/hours}}
+        </select>:
+        <label class="sr-only" for="{{id}}m">{{#str}}minutes{{/str}}</label>
+        <select id="{{id}}m" name="{{name}}[m]" class="custom-select">
+            {{#minutes}}
+                <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
+            {{/minutes}}
+        </select>
+    </div>
 </div>
 
index 2fe18cc..ef89725 100644 (file)
         ]
     }
 }}
+{{!
+    Setting courselist_frontpage.
+}}
 <div class="form-group">
     {{#selects}}
-        <select id="{{id}}{{key}}" name="{{name}}[]" class="form-select">
+        <select id="{{id}}{{key}}" name="{{name}}[]" class="custom-select">
             {{#options}}
                 <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
             {{/options}}
index c6d0e57..09bda33 100644 (file)
 {{!
     Setting description.
 }}
-<div class="form-item form-horizontal clearfix">
-    <div class="form-label">
+<div class="form-item row">
+    <div class="form-label col-sm-3 text-sm-right">
         <label>
             {{{title}}}
         </label>
-        <span class="form-shortname ">{{{name}}}</span>
     </div>
-    <div class="controls felement fstatic">{{{description}}}</div>
-</div>
\ No newline at end of file
+    <div class="form-setting col-sm-9">
+        <div class="form-description">{{{description}}}</div>
+    </div>
+</div>
index cc88862..97f97f2 100644 (file)
         ]
     }
 }}
-<table class="generaltable">
+{{!
+    Setting devicedetectregex.
+}}
+<table class="table table-striped">
     <thead>
         <tr>
             <th>{{#str}}devicedetectregexexpression, admin{{/str}}</th>
         {{#expressions}}
             <tr>
                 <td class="c{{index}}">
-                    <input type="text" name="{{name}}[expression{{index}}]" class="form-text text-ltr" value="{{expression}}">
+                    <input type="text" name="{{name}}[expression{{index}}]" class="form-control" value="{{expression}}">
                 </td>
                 <td class="c{{index}}">
-                    <input type="text" name="{{name}}[value{{index}}]" class="form-text text-ltr" value="{{value}}">
+                    <input type="text" name="{{name}}[value{{index}}]" class="form-control" value="{{value}}">
                 </td>
             </tr>
         {{/expressions}}
index 4c36174..8a245dd 100644 (file)
@@ -32,6 +32,9 @@
         ]
     }
 }}
+{{!
+    Setting emoticons.
+}}
 <div class="form-group">
     <table id="emoticonsetting" class="admintable generaltable">
         <thead>
@@ -48,7 +51,7 @@
                 <tr>
                     {{#fields}}
                         <td class="c{{index}}">
-                            <input type="text" name="{{name}}[{{field}}]" class="form-text text-ltr" value="{{value}}">
+                            <input type="text" name="{{name}}[{{field}}]" class="form-text form-control text-ltr" value="{{value}}">
                         </td>
                     {{/fields}}
                     <td>
index d64d75f..9e71c27 100644 (file)
         "advanced": true
     }
 }}
+{{!
+    Setting configselect.
+}}
 <div class="form-group">
-    <select id="{{id}}" name="{{name}}[value]" class="form-select">
+    <select id="{{id}}" name="{{name}}[value]" class="form-select custom-select">
         {{#options}}
             <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
         {{/options}}
index 0097305..0067f8e 100644 (file)
         "showsave": true
     }
 }}
+{{!
+    Settings.
+}}
 <form action="{{actionurl}}" method="post" id="adminsettings">
-    <div class="settingsform clearfix">
+    <div class="settingsform">
         {{#params}}
             <input type="hidden" name="{{name}}" value="{{value}}">
             <input type="hidden" name="action" value="save-settings">
         {{/title}}
         {{{settings}}}
         {{#showsave}}
-            <div class="form-buttons">
-                <input type="submit" class="form-submit" value={{#quote}}{{#str}}savechanges, admin{{/str}}{{/quote}}>
+            <div class="row">
+                <div class="offset-sm-3 col-sm-3">
+                    <button type="submit" class="btn btn-primary">{{#str}}savechanges, admin{{/str}}</button>
+                </div>
             </div>
         {{/showsave}}
     </div>
index df82b0f..6653154 100644 (file)
                 </fieldset>
             {{/results}}
             {{#showsave}}
-                <div class="form-buttons">
-                    <input type="submit" class="form-submit" value={{#quote}}{{#str}}savechanges, admin{{/str}}{{/quote}}>
+                <div class="row">
+                    <div class="offset-sm-3 col-sm-3">
+                        <button type="submit" class="btn btn-primary">{{#str}}savechanges, admin{{/str}}</button>
+                    </div>
                 </div>
             {{/showsave}}
         {{/hasresults}}
index 73ce572..b2fd070 100644 (file)
@@ -55,16 +55,12 @@ class behat_admin extends behat_base {
 
         foreach ($data as $label => $value) {
 
-            // We expect admin block to be visible, otherwise go to homepage.
-            if (!$this->getSession()->getPage()->find('css', '.block_settings')) {
-                $this->getSession()->visit($this->locate_path('/'));
-                $this->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
-            }
+            $this->execute('behat_navigation::i_select_from_flat_navigation_drawer', [get_string('administrationsite')]);
 
             // Search by label.
-            $searchbox = $this->find_field(get_string('searchinsettings', 'admin'));
+            $searchbox = $this->find_field(get_string('query', 'admin'));
             $searchbox->setValue($label);
-            $submitsearch = $this->find('css', 'form.adminsearchform input[type=submit]');
+            $submitsearch = $this->find('css', 'form input[type=submit][name=search]');
             $submitsearch->press();
 
             $this->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
@@ -78,21 +74,24 @@ class behat_admin extends behat_base {
 
             // Single element settings.
             try {
-                $fieldxpath = "//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]" .
-                    "[@id=//label[contains(normalize-space(.), $label)]/@for or " .
-                    "@id=//span[contains(normalize-space(.), $label)]/preceding-sibling::label[1]/@for]";
+                $fieldxpath = "//*[self::input | self::textarea | self::select]" .
+                        "[not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]" .
+                        "[@id=//label[contains(normalize-space(.), $label)]/@for or " .
+                        "@id=//span[contains(normalize-space(.), $label)]/preceding-sibling::label[1]/@for]";
                 $fieldnode = $this->find('xpath', $fieldxpath, $exception);
 
-                $formfieldtypenode = $this->find('xpath', $fieldxpath . "/ancestor::div[@class='form-setting']" .
-                    "/child::div[contains(concat(' ', @class, ' '),  ' form-')]/child::*/parent::div");
+                $formfieldtypenode = $this->find('xpath', $fieldxpath .
+                        "/ancestor::div[contains(concat(' ', @class, ' '), ' form-setting ')]" .
+                        "/child::div[contains(concat(' ', @class, ' '),  ' form-')]/child::*/parent::div");
 
             } catch (ElementNotFoundException $e) {
 
                 // Multi element settings, interacting only the first one.
-                $fieldxpath = "//*[label[normalize-space(.)= $label]|span[normalize-space(.)= $label]]/" .
-                    "ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' form-item ')]" .
-                    "/descendant::div[@class='form-group']/descendant::*[self::input | self::textarea | self::select]" .
-                    "[not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]";
+                $fieldxpath = "//*[label[contains(., $label)]|span[contains(., $label)]]" .
+                        "/ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' form-item ')]" .
+                        "/descendant::div[contains(concat(' ', @class, ' '), ' form-group ')]" .
+                        "/descendant::*[self::input | self::textarea | self::select]" .
+                        "[not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]";
                 $fieldnode = $this->find('xpath', $fieldxpath);
 
                 // It is the same one that contains the type.
@@ -101,6 +100,7 @@ class behat_admin extends behat_base {
 
             // Getting the class which contains the field type.
             $classes = explode(' ', $formfieldtypenode->getAttribute('class'));
+            $type = false;
             foreach ($classes as $class) {
                 if (substr($class, 0, 5) == 'form-') {
                     $type = substr($class, 5);
index 01c0396..c8819d6 100644 (file)
@@ -24,7 +24,7 @@
 
 // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
 
-require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
+require_once(__DIR__ . '/../../../../../../lib/behat/behat_base.php');
 
 /**
  * Test context 1
index 0dbdbeb..9b423ae 100644 (file)
@@ -24,7 +24,7 @@
 
 // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
 
-require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
+require_once(__DIR__ . '/../../../../../../lib/behat/behat_base.php');
 
 /**
  * Test context 2
index ff164cd..f0d851c 100644 (file)
@@ -24,7 +24,7 @@
 
 // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
 
-require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
+require_once(__DIR__ . '/../../../../../../../lib/behat/behat_base.php');
 
 /**
  * Default Theme test context 1
index 710da0a..5db9807 100644 (file)
@@ -24,7 +24,7 @@
 
 // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
 
-require_once(__DIR__ . '/../behat_test_context_1.php');
+require_once(__DIR__ . '/../../core/behat_test_context_1.php');
 
 /**
  * Theme test context 1
@@ -33,6 +33,6 @@ require_once(__DIR__ . '/../behat_test_context_1.php');
  * @copyright  2016 Rajesh Taneja <rajesh@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class behat_theme_nofeatures_behat_test_context_1 extends behat_test_context_1 {
+class behat_theme_nofeatures_behat_test_context_2 extends behat_test_context_2 {
 
 }
\ No newline at end of file
index 87fe40c..2d8d121 100644 (file)
@@ -24,7 +24,7 @@
 
 // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
 
-require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
+require_once(__DIR__ . '/../../../../../../../lib/behat/behat_base.php');
 
 /**
  * Theme test context 2
@@ -33,6 +33,6 @@ require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
  * @copyright  2016 Rajesh Taneja <rajesh@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class behat_theme_nofeatures_test_context_2 extends behat_base {
+class behat_theme_nofeatures_test_context_1 extends behat_base {
 
 }
\ No newline at end of file
index 3941071..1111532 100644 (file)
@@ -24,7 +24,7 @@
 
 // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
 
-require_once(__DIR__ . '/../behat_test_context_1.php');
+require_once(__DIR__ . '/../../core/behat_test_context_1.php');
 
 /**
  * Theme test context 1
index 23eedff..2265320 100644 (file)
@@ -24,7 +24,7 @@
 
 // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
 
-require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
+require_once(__DIR__ . '/../../../../../../../lib/behat/behat_base.php');
 
 /**
  * Theme test context 2
index 2d10776..98ccd03 100644 (file)
@@ -108,7 +108,6 @@ class tool_behat_manager_util_testcase extends advanced_testcase {
             array('nofeatures', __DIR__.'/fixtures/theme/nofeatures'),
             array('defaulttheme', __DIR__.'/fixtures/theme/defaulttheme'),
         );
-
         // List of themes is const for test.
         if ($notheme) {
             $themelist = array('defaulttheme');
@@ -116,6 +115,13 @@ class tool_behat_manager_util_testcase extends advanced_testcase {
             $themelist = array('withfeatures', 'nofeatures', 'defaulttheme');
         }
 
+        $thememap = [];
+        foreach ($themelist as $themename) {
+            $mock = $this->getMockBuilder('theme_config');
+            $mock->disableOriginalConstructor();
+            $thememap[] = [$themename, $mock->getMock()];
+        }
+
         $behatconfigutil->expects($this->any())
             ->method('get_list_of_themes')
             ->will($this->returnValue($themelist));
@@ -125,6 +131,11 @@ class tool_behat_manager_util_testcase extends advanced_testcase {
             ->method('get_theme_test_directory')
             ->will($this->returnValueMap($map));
 
+        // Theme directory for testing.
+        $behatconfigutil->expects($this->any())
+                ->method('get_theme_config')
+                ->will($this->returnValueMap($thememap));
+
         $behatconfigutil->expects($this->any())
             ->method('get_default_theme')
             ->will($this->returnValue('defaulttheme'));
@@ -138,7 +149,7 @@ class tool_behat_manager_util_testcase extends advanced_testcase {
     public function test_get_config_file_contents_with_single_run() {
 
         $mockbuilder = $this->getMockBuilder('behat_config_util');
-        $mockbuilder->setMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_default_theme'));
+        $mockbuilder->setMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_default_theme', 'get_theme_config'));
 
         $behatconfigutil = $mockbuilder->getMock();
 
@@ -177,7 +188,7 @@ class tool_behat_manager_util_testcase extends advanced_testcase {
     public function test_get_config_file_contents_with_single_run_no_theme() {
 
         $mockbuilder = $this->getMockBuilder('behat_config_util');
-        $mockbuilder->setMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_default_theme'));
+        $mockbuilder->setMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_default_theme', 'get_theme_config'));
 
         $behatconfigutil = $mockbuilder->getMock();
 
@@ -231,7 +242,7 @@ class tool_behat_manager_util_testcase extends advanced_testcase {
     public function test_get_config_file_contents_with_parallel_run() {
 
         $mockbuilder = $this->getMockBuilder('behat_config_util');
-        $mockbuilder->setMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_default_theme'));
+        $mockbuilder->setMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_default_theme', 'get_theme_config'));
 
         $behatconfigutil = $mockbuilder->getMock();
 
@@ -334,7 +345,7 @@ class tool_behat_manager_util_testcase extends advanced_testcase {
     public function test_get_config_file_contents_with_parallel_run_optimize_tags() {
 
         $mockbuilder = $this->getMockBuilder('behat_config_util');
-        $mockbuilder->setMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_default_theme'));
+        $mockbuilder->setMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_default_theme', 'get_theme_config'));
 
         $behatconfigutil = $mockbuilder->getMock();
 
@@ -479,7 +490,7 @@ class tool_behat_manager_util_testcase extends advanced_testcase {
 
         $mockbuilder = $this->getMockBuilder('behat_config_util');
         $mockbuilder->setMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_blacklisted_tests_for_theme',
-            'get_default_theme'));
+            'get_default_theme', 'get_theme_config'));
 
         $behatconfigutil = $mockbuilder->getMock();
 
@@ -543,7 +554,7 @@ class tool_behat_manager_util_testcase extends advanced_testcase {
 
         $mockbuilder = $this->getMockBuilder('behat_config_util');
         $mockbuilder->setMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_blacklisted_tests_for_theme',
-            'get_default_theme'));
+            'get_default_theme', 'get_theme_config'));
 
         $behatconfigutil = $mockbuilder->getMock();
 
@@ -621,7 +632,7 @@ class tool_behat_manager_util_testcase extends advanced_testcase {
     public function test_core_features_to_include_in_specified_theme() {
 
         $mockbuilder = $this->getMockBuilder('behat_config_util');
-        $mockbuilder->setMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_default_theme'));
+        $mockbuilder->setMethods(array('get_theme_test_directory', 'get_list_of_themes', 'get_default_theme', 'get_theme_config'));
 
         $behatconfigutil = $mockbuilder->getMock();
 
index 525aa94..36b1184 100644 (file)
@@ -1,5 +1,8 @@
 This files describes API changes in the tool_behat code.
 
+=== 3.7 ===
+* Behat will now look for behat step definitions in the current
+  theme and any parents the theme may have.
 === 2.7 ===
 * Constants behat_base::cap_allow, behat_base::cap_prevent and
   behat_base::cap_prohibit have been removed in favour of the
index 118f761..b7029db 100644 (file)
@@ -20,7 +20,6 @@
     Moodle progress bar template for tool_lp.
 
     The purpose of this template is to render a progress bar with a brief description.
-    Inherits core/columns-1to2.
 
     Classes required for JS:
     * none
     }
 
 }}
-<div class="row-fluid rtl-compatible">
-    <div class="span4">
-        <div class="progresstext">
-            {{$progresstext}}{{progresstextvalue}}{{/progresstext}}
-        </div>
-    </div>
-    <div class="span8">
-        <div class="progress">
-            <div class="bar" style="width: {{$percentage}}{{percentagevalue}}{{/percentage}}%;" role="progressbar" aria-valuenow="{{$percentage}}{{percentagevalue}}{{/percentage}}" aria-valuemin="0" aria-valuemax="100">
-                {{$percentlabel}}{{percentlabelvalue}}{{/percentlabel}}
-            </div>
-        </div>
-    </div>
+<div id="progress-{{uniqid}}">
+    {{$progresstext}}{{progresstextvalue}}{{/progresstext}}
 </div>
+<progress class="progress" aria-describedby="progress-{{uniqid}}"
+    value="{{$percentage}}{{percentagevalue}}{{/percentage}}" max="100"></progress>
index bfa0e8d..aaa36d9 100644 (file)
@@ -7,7 +7,7 @@ Feature: Clear scheduled task fail delay
   Background:
     Given the scheduled task "\core\task\send_new_user_passwords_task" has a fail delay of "60" seconds
     And I log in as "admin"
-    And I navigate to "Server > Scheduled tasks" in site administration
+    And I navigate to "Server > Tasks > Scheduled tasks" in site administration
 
   Scenario: Clear fail delay
     When I click on "Clear" "text" in the "Send new user passwords" "table_row"
index 1f0a9df..4da19e5 100644 (file)
@@ -6,7 +6,7 @@ Feature: Manage scheduled tasks
 
   Background:
     Given I log in as "admin"
-    And I navigate to "Server > Scheduled tasks" in site administration
+    And I navigate to "Server > Tasks > Scheduled tasks" in site administration
 
   Scenario: Disable scheduled task
     When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row"
index 886c250..3771835 100644 (file)
Binary files a/admin/tool/usertours/amd/build/managesteps.min.js and b/admin/tool/usertours/amd/build/managesteps.min.js differ
index 9c9e2e6..75c7d1b 100644 (file)
Binary files a/admin/tool/usertours/amd/build/managetours.min.js and b/admin/tool/usertours/amd/build/managetours.min.js differ
index ba52019..420b89e 100644 (file)
@@ -18,6 +18,7 @@ function($, str, notification) {
          */
         removeStep: function(e) {
             e.preventDefault();
+            var targetUrl = $(e.currentTarget).attr('href');
             str.get_strings([
                 {
                     key:        'confirmstepremovaltitle',
@@ -35,11 +36,15 @@ function($, str, notification) {
                     key:        'no',
                     component:  'moodle'
                 }
-            ]).done(function(s) {
-                notification.confirm(s[0], s[1], s[2], s[3], $.proxy(function() {
-                    window.location = $(this).attr('href');
-                }, e.currentTarget));
-            });
+            ])
+            .then(function(s) {
+                notification.confirm(s[0], s[1], s[2], s[3], function() {
+                    window.location = targetUrl;
+                });
+
+                return;
+            })
+            .catch();
         },
 
         /**
index f5c601a..f6d3237 100644 (file)
@@ -18,7 +18,7 @@ function($, ajax, str, notification) {
          */
         removeTour: function(e) {
             e.preventDefault();
-
+            var targetUrl = $(e.currentTarget).attr('href');
             str.get_strings([
                 {
                     key:        'confirmtourremovaltitle',
@@ -36,11 +36,15 @@ function($, ajax, str, notification) {
                     key:        'no',
                     component:  'moodle'
                 }
-            ]).done(function(s) {
-                notification.confirm(s[0], s[1], s[2], s[3], $.proxy(function() {
-                    window.location = $(this).attr('href');
-                }, e.currentTarget));
-            });
+            ])
+            .then(function(s) {
+                notification.confirm(s[0], s[1], s[2], s[3], function() {
+                    window.location = targetUrl;
+                });
+
+                return;
+            })
+            .catch();
         },
 
         /**
index 14a96e0..1e4cdbb 100644 (file)
@@ -497,6 +497,12 @@ class helper {
             return;
         }
 
+        if (in_array($PAGE->pagelayout, ['maintenance', 'print', 'redirect'])) {
+            // Do not try to show user tours inside iframe, in maintenance mode,
+            // when printing, or during redirects.
+            return;
+        }
+
         if (self::$bootstrapped) {
             return;
         }
index a4fb976..192fec4 100644 (file)
     }
 
 }}
-<div class="modal" data-role="flexitour-step">
-    <div data-role="arrow"></div>
+<div class="modal-dialog" role="document" data-role="flexitour-step">
+  <div class="modal-content">
+    <div class="tooltip-arrow" data-role="arrow"></div>
     <div class="modal-header">
-      <button type="button" class="close" data-dismiss="modal" aria-hidden="true" data-role="end">&times;</button>
-      <h3 data-placeholder="title"></h3>
+      <h5 class="modal-title" data-placeholder="title"></h5>
+      <button type="button" class="close" data-dismiss="modal" aria-label="Close" data-role="end">
+        <span aria-hidden="true">&times;</span>
+      </button>
+
+    </div>
+    <div class="modal-body" data-placeholder="body">
     </div>
-    <div class="modal-body" data-placeholder="body"></div>
     <div class="modal-footer">
-        <div class="btn-group">
-            <button href="#" class="btn" data-role="previous">{{# str }} previous, moodle {{/ str }}</button>
-            <button href="#" class="btn btn-primary" data-role="next">{{# str }} next, moodle {{/ str }}</button>
-        </div>
-        <button class="btn" data-role="end"> {{# str }} endtour, tool_usertours {{/ str }} </button>
+      <button type="button" class="btn btn-secondary" data-role="previous">{{# str }} previous, moodle {{/ str }}</button>
+      <button type="button" class="btn btn-primary" data-role="next">{{# str }} next, moodle {{/ str }}</button>
+      <button class="btn btn-secondary" data-role="end"> {{# str }} endtour, tool_usertours {{/ str }} </button>
     </div>
+  </div>
 </div>
index faa02a4..5d6f205 100644 (file)
@@ -6,7 +6,7 @@ Feature: Add a bookmarks to an admin pages
 
   Background:
     Given I log in as "admin"
-    And I navigate to "Server > Scheduled tasks" in site administration
+    And I navigate to "Server > Tasks > Scheduled tasks" in site administration
     And I click on "Bookmark this page" "link" in the "Admin bookmarks" "block"
     And I log out
 
index 99b59c4..9d6dfd0 100644 (file)
Binary files a/blocks/myoverview/amd/build/view.min.js and b/blocks/myoverview/amd/build/view.min.js differ
index 85c81e1..747d33f 100644 (file)
@@ -217,7 +217,7 @@ function(
 
         setCourseFavouriteState(courseId, true).then(function(success) {
             if (success) {
-                PubSub.publish(CourseEvents.favourited);
+                PubSub.publish(CourseEvents.favourited, courseId);
                 removeAction.removeClass('hidden');
                 addAction.addClass('hidden');
                 showFavouriteIcon(root, courseId);
@@ -240,7 +240,7 @@ function(
 
         setCourseFavouriteState(courseId, false).then(function(success) {
             if (success) {
-                PubSub.publish(CourseEvents.unfavorited);
+                PubSub.publish(CourseEvents.unfavorited, courseId);
                 removeAction.addClass('hidden');
                 addAction.removeClass('hidden');
                 hideFavouriteIcon(root, courseId);
index 594bbde..a5bf1e2 100644 (file)
Binary files a/blocks/recentlyaccessedcourses/amd/build/main.min.js and b/blocks/recentlyaccessedcourses/amd/build/main.min.js differ
index 2fdd54c..203b1a0 100644 (file)
 define(
     [
         'jquery',
-        'core_course/repository',
-        'core/templates',
+        'core/custom_interaction_events',
         'core/notification',
         'core/pubsub',
-        'core_course/events'
+        'core/paged_content_paging_bar',
+        'core/templates',
+        'core_course/events',
+        'core_course/repository',
     ],
     function(
         $,
-        CoursesRepository,
-        Templates,
+        CustomEvents,
         Notification,
         PubSub,
-        CourseEvents
+        PagedContentPagingBar,
+        Templates,
+        CourseEvents,
+        CoursesRepository
     ) {
 
+        // Constants.
+        var NUM_COURSES_TOTAL = 10;
         var SELECTORS = {
-            COURSES_VIEW: '[data-region="recentlyaccessedcourses-view"]',
-            COURSES_VIEW_CONTENT: '[data-region="recentlyaccessedcourses-view-content"]'
+            CARD_CONTAINER: '[data-region="card-deck"]',
+            COURSE_IS_FAVOURITE: '[data-region="is-favourite"]',
+            CONTENT: '[data-region="view-content"]',
+            EMPTY_MESSAGE: '[data-region="empty-message"]',
+            LOADING_PLACEHOLDER: '[data-region="loading-placeholder"]',
+            PAGING_BAR: '[data-region="paging-bar"]',
+            PAGING_BAR_NEXT: '[data-control="next"]',
+            PAGING_BAR_PREVIOUS: '[data-control="previous"]'
         };
+        // Module variables.
+        var contentLoaded = false;
+        var allCourses = [];
+        var visibleCoursesId = null;
+        var cardWidth = null;
+        var viewIndex = 0;
+        var availableVisibleCards = 1;
 
-        var NUM_COURSES_TOTAL = 10;
+        /**
+         * Show the empty message when no course are found.
+         *
+         * @param {object} root The root element for the courses view.
+         */
+        var showEmptyMessage = function(root) {
+            root.find(SELECTORS.EMPTY_MESSAGE).removeClass('hidden');
+            root.find(SELECTORS.LOADING_PLACEHOLDER).addClass('hidden');
+            root.find(SELECTORS.CONTENT).addClass('hidden');
+        };
 
         /**
-         * Get enrolled courses from backend.
+         * Show the empty message when no course are found.
          *
-         * @method getRecentCourses
-         * @param {int} userid User from which the courses will be obtained
-         * @param {int} limit Only return this many results
-         * @return {array} Courses user has accessed
+         * @param {object} root The root element for the courses view.
          */
-        var getRecentCourses = function(userid, limit) {
-            return CoursesRepository.getLastAccessedCourses(userid, limit);
+        var showContent = function(root) {
+            root.find(SELECTORS.CONTENT).removeClass('hidden');
+            root.find(SELECTORS.EMPTY_MESSAGE).addClass('hidden');
+            root.find(SELECTORS.LOADING_PLACEHOLDER).addClass('hidden');
         };
 
         /**
-         * Render the dashboard courses.
+         * Show the paging bar.
          *
-         * @method renderCourses
          * @param {object} root The root element for the courses view.
-         * @param {array} courses containing array of returned courses.
-         * @return {promise} Resolved with HTML and JS strings
          */
-        var renderCourses = function(root, courses) {
-            if (courses.length > 0) {
-                return Templates.render('core_course/view-cards', {
-                    courses: courses
-                });
-            } else {
-                var nocoursesimgurl = root.attr('data-nocoursesimg');
-                return Templates.render('block_recentlyaccessedcourses/no-courses', {
-                    nocoursesimg: nocoursesimgurl
+        var showPagingBar = function(root) {
+            var pagingBar = root.find(SELECTORS.PAGING_BAR);
+            pagingBar.css('opacity', 1);
+            pagingBar.css('visibility', 'visible');
+            pagingBar.attr('aria-hidden', 'false');
+        };
+
+        /**
+         * Hide the paging bar.
+         *
+         * @param {object} root The root element for the courses view.
+         */
+        var hidePagingBar = function(root) {
+            var pagingBar = root.find(SELECTORS.PAGING_BAR);
+            pagingBar.css('opacity', 0);
+            pagingBar.css('visibility', 'hidden');
+            pagingBar.attr('aria-hidden', 'true');
+        };
+
+        /**
+         * Show the favourite indicator for the given course (if it's in the list).
+         *
+         * @param {object} root The root element for the courses view.
+         * @param {number} courseId The id of the course to be favourited.
+         */
+        var favouriteCourse = function(root, courseId) {
+            allCourses.forEach(function(course) {
+                if (course.attr('data-course-id') == courseId) {
+                    course.find(SELECTORS.COURSE_IS_FAVOURITE).removeClass('hidden');
+                }
+            });
+        };
+
+        /**
+         * Hide the favourite indicator for the given course (if it's in the list).
+         *
+         * @param {object} root The root element for the courses view.
+         * @param {number} courseId The id of the course to be unfavourited.
+         */
+        var unfavouriteCourse = function(root, courseId) {
+            allCourses.forEach(function(course) {
+                if (course.attr('data-course-id') == courseId) {
+                    course.find(SELECTORS.COURSE_IS_FAVOURITE).addClass('hidden');
+                }
+            });
+        };
+
+        /**
+         * Render the a list of courses.
+         *
+         * @param {array} courses containing array of courses.
+         * @return {promise} Resolved with list of rendered courses as jQuery objects.
+         */
+        var renderAllCourses = function(courses) {
+            var promises = courses.map(function(course) {
+                return Templates.render('block_recentlyaccessedcourses/course-card', course);
+            });
+
+            return $.when.apply(null, promises).then(function() {
+                var renderedCourses = [];
+
+                promises.forEach(function(promise) {
+                    promise.then(function(html) {
+                        renderedCourses.push($(html));
+                        return;
+                    })
+                    .catch(Notification.exception);
                 });
-            }
+
+                return renderedCourses;
+            });
         };
 
         /**
          * Fetch user's recently accessed courses and reload the content of the block.
          *
          * @param {int} userid User whose courses will be shown
-         * @param {object} root The root element for the recentlyaccessedcourses view.
          * @returns {promise} The updated content for the block.
          */
-        var reloadContent = function(userid, root) {
+        var loadContent = function(userid) {
+            return CoursesRepository.getLastAccessedCourses(userid, NUM_COURSES_TOTAL)
+                .then(function(courses) {
+                    return renderAllCourses(courses);
+                });
+        };
 
-            var recentcoursesViewRoot = root.find(SELECTORS.COURSES_VIEW);
-            var recentcoursesViewContent = root.find(SELECTORS.COURSES_VIEW_CONTENT);
+        /**
+         * Recalculate the number of courses that should be visible.
+         *
+         * @param {object} root The root element for the courses view.
+         */
+        var recalculateVisibleCourses = function(root) {
+            var container = root.find(SELECTORS.CONTENT).find(SELECTORS.CARD_CONTAINER);
+            var availableWidth = parseFloat(root.css('width'));
+            var numberOfCourses = allCourses.length;
+            var start = 0;
+
+            if (!cardWidth) {
+                container.html(allCourses[0]);
+                // Render one card initially to calculate the width of the cards
+                // including the margins.
+                cardWidth = allCourses[0].outerWidth(true);
+            }
+
+            availableVisibleCards = Math.floor(availableWidth / cardWidth);
+
+            if (viewIndex + availableVisibleCards < numberOfCourses) {
+                start = viewIndex;
+            } else {
+                var overflow = (viewIndex + availableVisibleCards) - numberOfCourses;
+                start = viewIndex - overflow;
+                start = start >= 0 ? start : 0;
+            }
+
+            var coursesToShow = allCourses.slice(start, start + availableVisibleCards);
+            // Create an id for the list of courses we expect to be displayed.
+            var newVisibleCoursesId = coursesToShow.reduce(function(carry, course) {
+                return carry + course.attr('data-course-id');
+            }, '');
 
-            var coursesPromise = getRecentCourses(userid, NUM_COURSES_TOTAL);
+            // Don't bother updating the DOM unless the visible courses have changed.
+            if (visibleCoursesId != newVisibleCoursesId) {
+                var pagingBar = root.find(PagedContentPagingBar.rootSelector);
+                container.html(coursesToShow);
+                visibleCoursesId = newVisibleCoursesId;
 
-            return coursesPromise.then(function(courses) {
-                var pagedContentPromise = renderCourses(recentcoursesViewRoot, courses);
+                if (availableVisibleCards >= allCourses.length) {
+                    hidePagingBar(root);
+                } else {
+                    showPagingBar(root);
 
-                pagedContentPromise.then(function(html, js) {
-                    return Templates.replaceNodeContents(recentcoursesViewContent, html, js);
-                }).catch(Notification.exception);
-                return coursesPromise;
-            }).catch(Notification.exception);
+                    if (viewIndex === 0) {
+                        PagedContentPagingBar.disablePreviousControlButtons(pagingBar);
+                    } else {
+                        PagedContentPagingBar.enablePreviousControlButtons(pagingBar);
+                    }
+
+                    if (viewIndex + availableVisibleCards >= allCourses.length) {
+                        PagedContentPagingBar.disableNextControlButtons(pagingBar);
+                    } else {
+                        PagedContentPagingBar.enableNextControlButtons(pagingBar);
+                    }
+                }
+            }
         };
 
         /**
          * Register event listeners for the block.
          *
-         * @param {int} userid User whose courses will be shown
          * @param {object} root The root element for the recentlyaccessedcourses block.
          */
-        var registerEventListeners = function(userid, root) {
-            PubSub.subscribe(CourseEvents.favourited, function() {
-                reloadContent(userid, root);
+        var registerEventListeners = function(root) {
+            var resizeTimeout = null;
+            var drawerToggling = false;
+
+            PubSub.subscribe(CourseEvents.favourited, function(courseId) {
+                favouriteCourse(root, courseId);
+            });
+
+            PubSub.subscribe(CourseEvents.unfavorited, function(courseId) {
+                unfavouriteCourse(root, courseId);
+            });
+
+            PubSub.subscribe('nav-drawer-toggle-start', function() {
+                if (!contentLoaded || !allCourses.length || drawerToggling) {
+                    // Nothing to recalculate.
+                    return;
+                }
+
+                drawerToggling = true;
+                var recalculationCount = 0;
+                // This function is going to recalculate the number of courses while
+                // the nav drawer is opening or closes (up to a maximum of 5 recalcs).
+                var doRecalculation = function() {
+                    setTimeout(function() {
+                        recalculateVisibleCourses(root);
+                        recalculationCount++;
+
+                        if (recalculationCount < 5 && drawerToggling) {
+                            // If we haven't done too many recalculations and the drawer
+                            // is still toggling then recurse.
+                            doRecalculation();
+                        }
+                    }, 100);
+                };
+
+                // Start the recalculations.
+                doRecalculation(root);
+            });
+
+            PubSub.subscribe('nav-drawer-toggle-end', function() {
+                drawerToggling = false;
+            });
+
+            $(window).on('resize', function() {
+                if (!contentLoaded || !allCourses.length) {
+                    // Nothing to reclculate.
+                    return;
+                }
+
+                // Resize events fire rapidly so recalculating the visible courses each
+                // time can be expensive. Let's debounce them,
+                if (!resizeTimeout) {
+                    resizeTimeout = setTimeout(function() {
+                        resizeTimeout = null;
+                        recalculateVisibleCourses(root);
+                    // The recalculateVisibleCourses function will execute at a rate of 15fps.
+                    }, 66);
+                }
+            });
+
+            CustomEvents.define(root, [CustomEvents.events.activate]);
+            root.on(CustomEvents.events.activate, SELECTORS.PAGING_BAR_NEXT, function(e, data) {
+                var button = $(e.target).closest(SELECTORS.PAGING_BAR_NEXT);
+                if (!button.hasClass('disabled')) {
+                    viewIndex = viewIndex + availableVisibleCards;
+                    recalculateVisibleCourses(root);
+                }
+
+                data.originalEvent.preventDefault();
             });
 
-            PubSub.subscribe(CourseEvents.unfavorited, function() {
-                reloadContent(userid, root);
+            root.on(CustomEvents.events.activate, SELECTORS.PAGING_BAR_PREVIOUS, function(e, data) {
+                var button = $(e.target).closest(SELECTORS.PAGING_BAR_PREVIOUS);
+                if (!button.hasClass('disabled')) {
+                    viewIndex = viewIndex - availableVisibleCards;
+                    viewIndex = viewIndex < 0 ? 0 : viewIndex;
+                    recalculateVisibleCourses(root);
+                }
+
+                data.originalEvent.preventDefault();
             });
         };
 
@@ -129,8 +334,22 @@ define(
         var init = function(userid, root) {
             root = $(root);
 
-            registerEventListeners(userid, root);
-            reloadContent(userid, root);
+            registerEventListeners(root);
+            loadContent(userid)
+                .then(function(renderedCourses) {
+                    allCourses = renderedCourses;
+                    contentLoaded = true;
+
+                    if (allCourses.length) {
+                        showContent(root);
+                        recalculateVisibleCourses(root);
+                    } else {
+                        showEmptyMessage(root);
+                    }
+
+                    return;
+                })
+                .catch(Notification.exception);
         };
 
         return {
index 06eb282..631af28 100644 (file)
@@ -45,11 +45,15 @@ class main implements renderable, templatable {
     public function export_for_template(renderer_base $output) {
         global $USER;
 
-        $nocoursesurl = $output->image_url('courses', 'block_recentlyaccessedcourses')->out();
+        $nocoursesurl = $output->image_url('courses', 'block_recentlyaccessedcourses')->out(false);
 
         return [
             'userid' => $USER->id,
-            'nocoursesimg' => $nocoursesurl
+            'nocoursesimgurl' => $nocoursesurl,
+            'pagingbar' => [
+                'next' => true,
+                'previous' => true
+            ]
         ];
     }
 }
diff --git a/blocks/recentlyaccessedcourses/templates/course-card.mustache b/blocks/recentlyaccessedcourses/templates/course-card.mustache
new file mode 100644 (file)
index 0000000..7b98a5e
--- /dev/null
@@ -0,0 +1,44 @@
+{{!
+    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 Licensebllsdsadfasfd
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template block_recentlyaccessedcourses/course-card
+
+    This template renders a course for the recentlyaccessedcourses block.
+
+    Example context (json):
+    {
+        "courses": [
+            {
+                "name": "Assignment due 1",
+                "viewurl": "https://moodlesite/course/view.php?id=2",
+                "courseimageurl": "https://moodlesite/pluginfile/123/course/overviewfiles/123.jpg",
+                "fullname": "course 3",
+                "isfavourite": true
+            }
+        ]
+    }
+}}
+
+{{< core_course/coursecard }}
+    {{$coursecategory}}
+        <span class="sr-only">
+            {{#str}}aria:coursecategory, core_course{{/str}}
+        </span>
+        <span class="text-truncate">{{{coursecategory}}}</span>
+    {{/coursecategory}}
+    {{$coursename}} <span class="text-truncate">{{{fullname}}}</span> {{/coursename}}
+{{/ core_course/coursecard }}
diff --git a/blocks/recentlyaccessedcourses/templates/no-courses.mustache b/blocks/recentlyaccessedcourses/templates/no-courses.mustache
deleted file mode 100644 (file)
index cf1b8b0..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-{{!
-    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/>.
-}}
-{{< core_course/no-courses}}
-    {{$nocoursestring}}
-        {{#str}} nocourses, block_recentlyaccessedcourses {{/str}}
-    {{/nocoursestring}}
-{{/ core_course/no-courses}}
index ec44dae..bb894d7 100644 (file)
 
     Example context (json):
     {
-        "nocoursesimg": "https://moodlesite/theme/image.php/boost/block_recentlyaccessedcourses/1535727318/courses"
     }
 }}
-<div id="recentlyaccessedcourses-view-{{uniqid}}"
-     data-region="recentlyaccessedcourses-view"
-     data-nocoursesimg="{{nocoursesimg}}">
-    <div data-region="recentlyaccessedcourses-view-content">
-        <div data-region="recentlyaccessedcourses-loading-placeholder">
-            <div class="card-deck dashboard-card-deck one-row" style="height: 11.1rem">
-                {{> core_course/placeholder-course }}
-                {{> core_course/placeholder-course }}
-                {{> core_course/placeholder-course }}
-                {{> core_course/placeholder-course }}
-            </div>
+<div id="recentlyaccessedcourses-view-{{uniqid}}" data-region="recentlyaccessedcourses-view">
+    <div data-region="loading-placeholder">
+        <div class="card-deck dashboard-card-deck one-row overflow-hidden" style="height: 13.05rem">
+            {{> core_course/placeholder-course }}
+            {{> core_course/placeholder-course }}
+            {{> core_course/placeholder-course }}
+            {{> core_course/placeholder-course }}
         </div>
     </div>
-</div>
\ No newline at end of file
+    <div class="hidden" data-region="view-content">
+        {{#pagingbar}}
+            <div class="d-flex paging-bar-container mb-3" data-region="paging-bar-container">
+                {{> core/paged_content_paging_bar }}
+            </div>
+        {{/pagingbar}}
+        {{< core_course/coursecards }}
+            {{$classes}}one-row fixed-width-cards justify-content-center overflow-hidden{{/classes}}
+        {{/ core_course/coursecards }}
+    </div>
+    <div class="hidden text-xs-center text-center m-t-3" data-region="empty-message">
+        <img class="empty-placeholder-image-lg m-t-1"
+            src="{{nocoursesimgurl}}"
+            alt="{{#str}} nocourses, block_recentlyaccessedcourses {{/str}}"
+            role="presentation">
+        <p class="text-muted mt-3">{{#str}} nocourses, block_recentlyaccessedcourses {{/str}}</p>
+    </div>
+</div>
index 9f0411f..0dc8b87 100644 (file)
@@ -1,15 +1,18 @@
 <div class="searchform">
-    <form action="{{actionurl}}" style="display: inline;">
-        <fieldset class="invisiblefieldset">
-            <legend class="accesshide">{{#str}}search{{/str}}</legend>
-            <input type="hidden" name="id" value="{{courseid}}">
-            <label class="accesshide" for="searchform_search">{{#str}}search{{/str}}</label>
-            <input id="searchform_search" name="search" type="text" size="16">
-            <button id="searchform_button" type="submit" title={{#quote}}{{#str}}search{{/str}}{{/quote}}>{{#str}}go{{/str}}</button><br>
-            <a href="{{advancedsearchurl}}">{{#str}}advancedsearch, block_search_forums{{/str}}</a>
-            {{#helpicon}}
-                {{>core/help_icon}}
-            {{/helpicon}}
-        </fieldset>
+    <form action="{{actionurl}}" class="form-inline">
+        <input type="hidden" name="id" value="{{courseid}}">
+        <div class="input-group w-100">
+            <label class="sr-only" for="searchform_search">{{#str}}search{{/str}}</label>
+            <input id="searchform_search" name="search" type="text" class="form-control" size="10">
+            <div class="input-group-append">
+                <button class="btn btn-secondary" id="searchform_button" type="submit">{{#str}}go{{/str}}</button>
+            </div>
+        </div>
     </form>
+    <div class="mt-3">
+        <a href="{{advancedsearchurl}}">{{#str}}advancedsearch, block_search_forums{{/str}}</a>
+        {{#helpicon}}
+            {{>core/help_icon}}
+        {{/helpicon}}
+    </div>
 </div>
index 34e99ef..bceefd8 100644 (file)
@@ -143,14 +143,12 @@ class block_settings_renderer extends plugin_renderer_base {
     }
 
     public function search_form(moodle_url $formtarget, $searchvalue) {
-        $content = html_writer::start_tag('form', array('class'=>'adminsearchform', 'method'=>'get', 'action'=>$formtarget, 'role' => 'search'));
-        $content .= html_writer::start_tag('div');
-        $content .= html_writer::tag('label', s(get_string('searchinsettings', 'admin')), array('for'=>'adminsearchquery', 'class'=>'accesshide'));
-        $content .= html_writer::empty_tag('input', array('id'=>'adminsearchquery', 'type'=>'text', 'name'=>'query', 'value'=>s($searchvalue)));
-        $content .= html_writer::empty_tag('input', array('type'=>'submit', 'value'=>s(get_string('search'))));
-        $content .= html_writer::end_tag('div');
-        $content .= html_writer::end_tag('form');
-        return $content;
+        $data = [
+                'action' => $formtarget->out(false),
+                'label' => get_string('searchinsettings', 'admin'),
+                'searchvalue' => $searchvalue
+        ];
+        return $this->render_from_template('block_settings/search_form', $data);
     }
 
 }
index b05d84c..5f55ca2 100644 (file)
@@ -46,15 +46,13 @@ class behat_blocks extends behat_base {
      * @param string $blockname
      */
     public function i_add_the_block($blockname) {
-        $this->execute('behat_forms::i_set_the_field_to',
-            array("bui_addblock", $this->escape($blockname))
-        );
+        $addblock = get_string('addblock');
+        $this->execute('behat_navigation::i_select_from_flat_navigation_drawer', $addblock);
 
-        // If we are running without javascript we need to submit the form.
         if (!$this->running_javascript()) {
-            $this->execute('behat_general::i_click_on_in_the',
-                array(get_string('go'), "button", "#add_block", "css_element")
-            );
+            $this->execute('behat_general::i_click_on_in_the', [$blockname, 'link_exact', '#region-main', 'css_element']);
+        } else {
+            $this->execute('behat_general::i_click_on_in_the', [$blockname, 'link_exact', $addblock, 'dialogue']);
         }
     }
 
@@ -108,7 +106,7 @@ class behat_blocks extends behat_base {
         }
 
         $this->execute('behat_general::i_click_on_in_the',
-            array(get_string('actions'), "link", $this->escape($blockname), "block")
+                array("a[data-toggle='dropdown']", "css_element", $this->escape($blockname), "block")
         );
     }
 
@@ -137,7 +135,17 @@ class behat_blocks extends behat_base {
      * @param string $blockname
      */
     public function the_add_block_selector_should_contain_block($blockname) {
-        $this->execute('behat_forms::the_select_box_should_contain', [get_string('addblock'), $blockname]);
+        $addblock = get_string('addblock');
+        $this->execute('behat_navigation::i_select_from_flat_navigation_drawer', $addblock);
+
+        $cancelstr = get_string('cancel');
+        if (!$this->running_javascript()) {
+            $this->execute('behat_general::should_exist_in_the', [$blockname, 'link_exact', '#region-main', 'css_element']);
+            $this->execute('behat_general::i_click_on_in_the', [$cancelstr, 'link_exact', '#region-main', 'css_element']);
+        } else {
+            $this->execute('behat_general::should_exist_in_the', [$blockname, 'link_exact', $addblock, 'dialogue']);
+            $this->execute('behat_general::i_click_on_in_the', [$cancelstr, 'button', $addblock, 'dialogue']);
+        }
     }
 
     /**
@@ -147,6 +155,16 @@ class behat_blocks extends behat_base {
      * @param string $blockname
      */
     public function the_add_block_selector_should_not_contain_block($blockname) {
-        $this->execute('behat_forms::the_select_box_should_not_contain', [get_string('addblock'), $blockname]);
+        $addblock = get_string('addblock');
+        $this->execute('behat_navigation::i_select_from_flat_navigation_drawer', $addblock);
+
+        $cancelstr = get_string('cancel');
+        if (!$this->running_javascript()) {
+            $this->execute('behat_general::should_not_exist_in_the', [$blockname, 'link_exact', '#region-main', 'css_element']);
+            $this->execute('behat_general::i_click_on_in_the', [$cancelstr, 'link_exact', '#region-main', 'css_element']);
+        } else {
+            $this->execute('behat_general::should_not_exist_in_the', [$blockname, 'link_exact', $addblock, 'dialogue']);
+            $this->execute('behat_general::i_click_on_in_the', [$cancelstr, 'button', $addblock, 'dialogue']);
+        }
     }
 }
index d6c91af..072345d 100644 (file)
         "day": "Today",
         "url": "http://example.com/",
         "title": "Monday 2nd January",
-        "content": "<img class='icon smallicon' alt='icon' src='../../../pix/i/siteevent.svg'>Test site event"
+        "content": "<img class='icon smallicon' src='../../../pix/i/siteevent.svg'>Test site event"
     }
 }}
-{{< core/hover_tooltip }}
-    {{$anchor}}
-        <a href="{{url}}">{{$day}}{{day}}{{/day}}</a>
-    {{/anchor}}
-    {{$tooltip}}
-        <b>{{$title}}{{title}}{{/title}}</b>
-        {{$content}}{{{content}}}{{/content}}
-    {{/tooltip}}
-{{/ core/hover_tooltip }}
+<a {{!
+    }} id="calendar-day-popover-link-{{courseid}}-{{year}}-{{yday}}-{{uniqid}}"{{!
+    }} href="{{$url}}{{url}}{{/url}}"{{!
+    }} data-container="body"{{!
+    }} data-toggle="popover"{{!
+    }} data-html="true"{{!
+    }} data-trigger="hover"{{!
+    }} data-placement="top"{{!
+    }} data-title="{{$title}}{{title}}{{/title}}"{{!
+    }} data-alternate="{{$nocontent}}{{/nocontent}}"{{!
+}}>{{$day}}{{day}}{{/day}}</a>
+<div class="hidden">
+    {{$content}}{{/content}}
+</div>
+{{#js}}
+require(['jquery'], function($) {
+    require(['theme_boost/popover'], function() {
+        var target = $("#calendar-day-popover-link-{{courseid}}-{{year}}-{{yday}}-{{uniqid}}");
+        target.popover({
+            content: function() {
+                var source = target.next().find("> *:not('.hidden')");
+                var content = $('<div>');
+
+                if (source.length) {
+                    content.html(source.clone(false));
+                } else {
+                    content.html(target.data('alternate'));
+                }
+
+                return content.html();
+            }
+        });
+    });
+});
+{{/js}}
index 40bef75..d2e7092 100644 (file)
@@ -139,13 +139,15 @@ class core_course_management_renderer extends plugin_renderer_base {
         $listing = core_course_category::get(0)->get_children();
 
         $attributes = array(
-            'class' => 'ml',
-            'role' => 'tree',
-            'aria-labelledby' => 'category-listing-title'
+                'class' => 'ml-1 list-unstyled',
+                'role' => 'tree',
+                'aria-labelledby' => 'category-listing-title'
         );
 
-        $html  = html_writer::start_div('category-listing');
-        $html .= html_writer::tag('h3', get_string('categories'), array('id' => 'category-listing-title'));
+        $html  = html_writer::start_div('category-listing card w-100');
+        $html .= html_writer::tag('h3', get_string('categories'),
+                array('class' => 'card-header', 'id' => 'category-listing-title'));
+        $html .= html_writer::start_div('card-body');
         $html .= $this->category_listing_actions($category);
         $html .= html_writer::start_tag('ul', $attributes);
         foreach ($listing as $listitem) {
@@ -155,16 +157,17 @@ class core_course_management_renderer extends plugin_renderer_base {
                 $subcategories = $listitem->get_children();
             }
             $html .= $this->category_listitem(
-                $listitem,
-                $subcategories,
-                $listitem->get_children_count(),
-                $selectedcategory,
-                $selectedparents
+                    $listitem,
+                    $subcategories,
+                    $listitem->get_children_count(),
+                    $selectedcategory,
+                    $selectedparents
             );
         }
         $html .= html_writer::end_tag('ul');
         $html .= $this->category_bulk_actions($category);
         $html .= html_writer::end_div();
+        $html .= html_writer::end_div();
         return $html;
     }
 
@@ -181,20 +184,20 @@ class core_course_management_renderer extends plugin_renderer_base {
      * @return string
      */
     public function category_listitem(core_course_category $category, array $subcategories, $totalsubcategories,
-                                      $selectedcategory = null, $selectedcategories = array()) {
+            $selectedcategory = null, $selectedcategories = array()) {
 
         $isexpandable = ($totalsubcategories > 0);
         $isexpanded = (!empty($subcategories));
         $activecategory = ($selectedcategory === $category->id);
         $attributes = array(
-            'class' => 'listitem listitem-category',
-            'data-id' => $category->id,
-            'data-expandable' => $isexpandable ? '1' : '0',
-            'data-expanded' => $isexpanded ? '1' : '0',
-            'data-selected' => $activecategory ? '1' : '0',
-            'data-visible' => $category->visible ? '1' : '0',
-            'role' => 'treeitem',
-            'aria-expanded' => $isexpanded ? 'true' : 'false'
+                'class' => 'listitem listitem-category list-group-item list-group-item-action',
+                'data-id' => $category->id,
+                'data-expandable' => $isexpandable ? '1' : '0',
+                'data-expanded' => $isexpanded ? '1' : '0',
+                'data-selected' => $activecategory ? '1' : '0',
+                'data-visible' => $category->visible ? '1' : '0',
+                'role' => 'treeitem',
+                'aria-expanded' => $isexpanded ? 'true' : 'false'
         );
         $text = $category->get_formatted_name();
         if ($category->parent) {
@@ -205,12 +208,12 @@ class core_course_management_renderer extends plugin_renderer_base {
         }
         $courseicon = $this->output->pix_icon('i/course', get_string('courses'));
         $bcatinput = array(
-            'type' => 'checkbox',
-            'name' => 'bcat[]',
-            'value' => $category->id,
-            'class' => 'bulk-action-checkbox',
-            'aria-label' => get_string('bulkactionselect', 'moodle', $text),
-            'data-action' => 'select'
+                'type' => 'checkbox',
+                'name' => 'bcat[]',
+                'value' => $category->id,
+                'class' => 'bulk-action-checkbox',
+                'aria-label' => get_string('bulkactionselect', 'moodle', $text),
+                'data-action' => 'select'
         );
 
         if (!$category->can_resort_subcategories() && !$category->has_manage_capability()) {
@@ -220,34 +223,36 @@ class core_course_management_renderer extends plugin_renderer_base {
 
         $viewcaturl = new moodle_url('/course/management.php', array('categoryid' => $category->id));
         if ($isexpanded) {
-            $icon = $this->output->pix_icon('t/switch_minus', get_string('collapse'), 'moodle', array('class' => 'tree-icon', 'title' => ''));
+            $icon = $this->output->pix_icon('t/switch_minus', get_string('collapse'),
+                    'moodle', array('class' => 'tree-icon', 'title' => ''));
             $icon = html_writer::link(
-                $viewcaturl,
-                $icon,
-                array(
-                    'class' => 'float-left',
-                    'data-action' => 'collapse',
-                    'title' => get_string('collapsecategory', 'moodle', $text),
-                    'aria-controls' => 'subcategoryof'.$category->id
-                )
+                    $viewcaturl,
+                    $icon,
+                    array(
+                            'class' => 'float-left',
+                            'data-action' => 'collapse',
+                            'title' => get_string('collapsecategory', 'moodle', $text),
+                            'aria-controls' => 'subcategoryof'.$category->id
+                    )
             );
         } else if ($isexpandable) {
-            $icon = $this->output->pix_icon('t/switch_plus', get_string('expand'), 'moodle', array('class' => 'tree-icon', 'title' => ''));
+            $icon = $this->output->pix_icon('t/switch_plus', get_string('expand'),
+                    'moodle', array('class' => 'tree-icon', 'title' => ''));
             $icon = html_writer::link(
-                $viewcaturl,
-                $icon,
-                array(
-                    'class' => 'float-left',
-                    'data-action' => 'expand',
-                    'title' => get_string('expandcategory', 'moodle', $text)
-                )
+                    $viewcaturl,
+                    $icon,
+                    array(
+                            'class' => 'float-left',
+                            'data-action' => 'expand',
+                            'title' => get_string('expandcategory', 'moodle', $text)
+                    )
             );
         } else {
             $icon = $this->output->pix_icon(
-                'i/empty',
-                '',
-                'moodle',
-                array('class' => 'tree-icon'));
+                    'i/empty',
+                    '',
+                    'moodle',
+                    array('class' => 'tree-icon'));
             $icon = html_writer::span($icon, 'float-left');
         }
         $actions = \core_course\management\helper::get_category_listitem_actions($category);
@@ -268,7 +273,7 @@ class core_course_management_renderer extends plugin_renderer_base {
             $textattributes['aria-label'] = $textlabel;
         }
         $html .= html_writer::link($viewcaturl, $text, $textattributes);
-        $html .= html_writer::start_div('float-right');
+        $html .= html_writer::start_div('float-right d-flex');
         if ($category->idnumber) {
             $html .= html_writer::tag('span', s($category->idnumber), array('class' => 'dimmed idnumber'));
         }
@@ -277,28 +282,28 @@ class core_course_management_renderer extends plugin_renderer_base {
         }
         $countid = 'course-count-'.$category->id;
         $html .= html_writer::span(
-            html_writer::span($category->get_courses_count()) .
-            html_writer::span(get_string('courses'), 'accesshide', array('id' => $countid)) .
-            $courseicon,
-            'course-count dimmed',
-            array('aria-labelledby' => $countid)
+                html_writer::span($category->get_courses_count()) .
+                html_writer::span(get_string('courses'), 'accesshide', array('id' => $countid)) .
+                $courseicon,
+                'course-count dimmed',
+                array('aria-labelledby' => $countid)
         );
         $html .= html_writer::end_div();
         $html .= html_writer::end_div();
         if ($isexpanded) {
             $html .= html_writer::start_tag('ul',
-                array('class' => 'ml', 'role' => 'group', 'id' => 'subcategoryof'.$category->id));
+                    array('class' => 'ml', 'role' => 'group', 'id' => 'subcategoryof'.$category->id));
             $catatlevel = \core_course\management\helper::get_expanded_categories($category->path);
             $catatlevel[] = array_shift($selectedcategories);
             $catatlevel = array_unique($catatlevel);
             foreach ($subcategories as $listitem) {
                 $childcategories = (in_array($listitem->id, $catatlevel)) ? $listitem->get_children() : array();
                 $html .= $this->category_listitem(
-                    $listitem,
-                    $childcategories,
-                    $listitem->get_children_count(),
-                    $selectedcategory,
-                    $selectedcategories
+                        $listitem,
+                        $childcategories,
+                        $listitem->get_children_count(),
+                        $selectedcategory,
+                        $selectedcategories
                 );
             }
             $html .= html_writer::end_tag('ul');
@@ -327,7 +332,7 @@ class core_course_management_renderer extends plugin_renderer_base {
 
         if ($cancreatecategory) {
             $url = new moodle_url('/course/editcategory.php', array('parent' => $category->id));
-            $actions[] = html_writer::link($url, get_string('createnewcategory'));
+            $actions[] = html_writer::link($url, get_string('createnewcategory'), array('class' => 'btn btn-default'));
         }
         if (core_course_category::can_approve_course_requests()) {
             $actions[] = html_writer::link(new moodle_url('/course/pending.php'), get_string('coursespending'));
@@ -335,7 +340,7 @@ class core_course_management_renderer extends plugin_renderer_base {
         if (count($actions) === 0) {
             return '';
         }
-        return html_writer::div(join(' | ', $actions), 'listing-actions category-listing-actions');
+        return html_writer::div(join(' ', $actions), 'listing-actions category-listing-actions mb-3');
     }
 
     /**
@@ -480,20 +485,19 @@ class core_course_management_renderer extends plugin_renderer_base {
      * Renders a course listing.
      *
      * @param core_course_category $category The currently selected category. This is what the listing is focused on.
-     * @param core_course_list_element  $course The currently selected course.
+     * @param core_course_list_element $course The currently selected course.
      * @param int $page The page being displayed.
      * @param int $perpage The number of courses to display per page.
      * @param string|null $viewmode The view mode the page is in, one out of 'default', 'combined', 'courses' or 'categories'.
      * @return string
      */
     public function course_listing(core_course_category $category = null, core_course_list_element $course = null,
-                                   $page = 0, $perpage = 20,
-        $viewmode = 'default') {
+            $page = 0, $perpage = 20, $viewmode = 'default') {
 
         if ($category === null) {
             $html = html_writer::start_div('select-a-category');
             $html .= html_writer::tag('h3', get_string('courses'),
-                array('id' => 'course-listing-title', 'tabindex' => '0'));
+                    array('id' => 'course-listing-title', 'tabindex' => '0'));
             $html .= $this->output->notification(get_string('selectacategory'), 'notifymessage');
             $html .= html_writer::end_div();
             return $html;
@@ -507,8 +511,8 @@ class core_course_management_renderer extends plugin_renderer_base {
             $page = $totalpages - 1;
         }
         $options = array(
-            'offset' => $page * $perpage,
-            'limit' => $perpage
+                'offset' => $page * $perpage,
+                'limit' => $perpage
         );
         $courseid = isset($course) ? $course->id : null;
         $class = '';
@@ -519,15 +523,16 @@ class core_course_management_renderer extends plugin_renderer_base {
             $class .= ' lastpage';
         }
 
-        $html  = html_writer::start_div('course-listing'.$class, array(
-            'data-category' => $category->id,
-            'data-page' => $page,
-            'data-totalpages' => $totalpages,
-            'data-totalcourses' => $totalcourses,
-            'data-canmoveoutof' => $category->can_move_courses_out_of() && $category->can_move_courses_into()
+        $html  = html_writer::start_div('card course-listing w-100'.$class, array(
+                'data-category' => $category->id,
+                'data-page' => $page,
+                'data-totalpages' => $totalpages,
+                'data-totalcourses' => $totalcourses,
+                'data-canmoveoutof' => $category->can_move_courses_out_of() && $category->can_move_courses_into()
         ));
         $html .= html_writer::tag('h3', $category->get_formatted_name(),
-            array('id' => 'course-listing-title', 'tabindex' => '0'));
+                array('id' => 'course-listing-title', 'tabindex' => '0', 'class' => 'card-header'));
+        $html .= html_writer::start_div('card-body');
         $html .= $this->course_listing_actions($category, $course, $perpage);
         $html .= $this->listing_pagination($category, $page, $perpage, false, $viewmode);
         $html .= html_writer::start_tag('ul', array('class' => 'ml course-list', 'role' => 'group'));
@@ -538,6 +543,7 @@ class core_course_management_renderer extends plugin_renderer_base {
         $html .= $this->listing_pagination($category, $page, $perpage, true, $viewmode);
         $html .= $this->course_bulk_actions($category);
         $html .= html_writer::end_div();
+        $html .= html_writer::end_div();
         return $html;
     }
 
@@ -588,7 +594,7 @@ class core_course_management_renderer extends plugin_renderer_base {
      * This function will be called for every course being displayed by course_listing.
      *
      * @param core_course_category $category The currently selected category and the category the course belongs to.
-     * @param core_course_list_element  $course The course to produce HTML for.
+     * @param core_course_list_element $course The course to produce HTML for.
      * @param int $selectedcourse The id of the currently selected course.
      * @return string
      */
@@ -596,19 +602,19 @@ class core_course_management_renderer extends plugin_renderer_base {
 
         $text = $course->get_formatted_name();
         $attributes = array(
-            'class' => 'listitem listitem-course',
-            'data-id' => $course->id,
-            'data-selected' => ($selectedcourse == $course->id) ? '1' : '0',
-            'data-visible' => $course->visible ? '1' : '0'
+                'class' => 'listitem listitem-course list-group-item list-group-item-action',
+                'data-id' => $course->id,
+                'data-selected' => ($selectedcourse == $course->id) ? '1' : '0',
+                'data-visible' => $course->visible ? '1' : '0'
         );
 
         $bulkcourseinput = array(
-            'type' => 'checkbox',
-            'name' => 'bc[]',
-            'value' => $course->id,
-            'class' => 'bulk-action-checkbox',
-            'aria-label' => get_string('bulkactionselect', 'moodle', $text),
-            'data-action' => 'select'
+                'type' => 'checkbox',
+                'name' => 'bc[]',
+                'value' => $course->id,
+                'class' => 'bulk-action-checkbox',
+                'aria-label' => get_string('bulkactionselect', 'moodle', $text),
+                'data-action' => 'select'
         );
         if (!$category->has_manage_capability()) {
             // Very very hardcoded here.
@@ -654,7 +660,7 @@ class core_course_management_renderer extends plugin_renderer_base {
         $actions = array();
         if ($category->can_create_course()) {
             $url = new moodle_url('/course/edit.php', array('category' => $category->id, 'returnto' => 'catmanage'));
-            $actions[] = html_writer::link($url, get_string('createnewcourse'));
+            $actions[] = html_writer::link($url, get_string('createnewcourse'), array('class' => 'btn btn-default'));
         }
         if ($category->can_request_course()) {
             // Request a new course.
@@ -675,42 +681,42 @@ class core_course_management_renderer extends plugin_renderer_base {
             $timecreatedurl = new moodle_url($baseurl, array('resort' => 'timecreated'));
             $timecreateddescurl = new moodle_url($baseurl, array('resort' => 'timecreateddesc'));
             $menu = new action_menu(array(
-                new action_menu_link_secondary($fullnameurl,
-                                               null,
-                                               get_string('sortbyx', 'moodle', get_string('fullnamecourse'))),
-                new action_menu_link_secondary($fullnameurldesc,
-                                               null,
-                                               get_string('sortbyxreverse', 'moodle', get_string('fullnamecourse'))),
-                new action_menu_link_secondary($shortnameurl,
-                                               null,
-                                               get_string('sortbyx', 'moodle', get_string('shortnamecourse'))),
-                new action_menu_link_secondary($shortnameurldesc,
-                                               null,
-                                               get_string('sortbyxreverse', 'moodle', get_string('shortnamecourse'))),
-                new action_menu_link_secondary($idnumberurl,
-                                               null,
-                                               get_string('sortbyx', 'moodle', get_string('idnumbercourse'))),
-                new action_menu_link_secondary($idnumberdescurl,
-                                               null,
-                                               get_string('sortbyxreverse', 'moodle', get_string('idnumbercourse'))),
-                new action_menu_link_secondary($timecreatedurl,
-                                               null,
-                                               get_string('sortbyx', 'moodle', get_string('timecreatedcourse'))),
-                new action_menu_link_secondary($timecreateddescurl,
-                                               null,
-                                               get_string('sortbyxreverse', 'moodle', get_string('timecreatedcourse')))
+                    new action_menu_link_secondary($fullnameurl,
+                            null,
+                            get_string('sortbyx', 'moodle', get_string('fullnamecourse'))),
+                    new action_menu_link_secondary($fullnameurldesc,
+                            null,
+                            get_string('sortbyxreverse', 'moodle', get_string('fullnamecourse'))),
+                    new action_menu_link_secondary($shortnameurl,
+                            null,
+                            get_string('sortbyx', 'moodle', get_string('shortnamecourse'))),
+                    new action_menu_link_secondary($shortnameurldesc,
+                            null,
+                            get_string('sortbyxreverse', 'moodle', get_string('shortnamecourse'))),
+                    new action_menu_link_secondary($idnumberurl,
+                            null,
+                            get_string('sortbyx', 'moodle', get_string('idnumbercourse'))),
+                    new action_menu_link_secondary($idnumberdescurl,
+                            null,
+                            get_string('sortbyxreverse', 'moodle', get_string('idnumbercourse'))),
+                    new action_menu_link_secondary($timecreatedurl,
+                            null,
+                            get_string('sortbyx', 'moodle', get_string('timecreatedcourse'))),
+                    new action_menu_link_secondary($timecreateddescurl,
+                            null,
+                            get_string('sortbyxreverse', 'moodle', get_string('timecreatedcourse')))
             ));
             $menu->set_menu_trigger(get_string('resortcourses'));
             $actions[] = $this->render($menu);
         }
         $strall = get_string('all');
         $menu = new action_menu(array(
-            new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 5)), null, 5),
-            new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 10)), null, 10),
-            new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 20)), null, 20),
-            new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 50)), null, 50),
-            new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 100)), null, 100),
-            new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 999)), null, $strall),
+                new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 5)), null, 5),
+                new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 10)), null, 10),
+                new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 20)), null, 20),
+                new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 50)), null, 50),
+                new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 100)), null, 100),
+                new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 999)), null, $strall),
         ));
         if ((int)$perpage === 999) {
             $perpage = $strall;
@@ -718,7 +724,7 @@ class core_course_management_renderer extends plugin_renderer_base {
         $menu->attributes['class'] .= ' courses-per-page';
         $menu->set_menu_trigger(get_string('perpagea', 'moodle', $perpage));
         $actions[] = $this->render($menu);
-        return html_writer::div(join(' ', $actions), 'listing-actions course-listing-actions');
+        return html_writer::div(join(' ', $actions), 'listing-actions course-listing-actions');
     }
 
     /**
@@ -800,20 +806,25 @@ class core_course_management_renderer extends plugin_renderer_base {
     /**
      * Renderers detailed course information.
      *
-     * @param core_course_list_element  $course The course to display details for.
+     * @param core_course_list_element $course The course to display details for.
      * @return string
      */
     public function course_detail(core_course_list_element $course) {
         $details = \core_course\management\helper::get_course_detail_array($course);
         $fullname = $details['fullname']['value'];
 
-        $html  = html_writer::start_div('course-detail');
-        $html .= html_writer::tag('h3', $fullname, array('id' => 'course-detail-title', 'tabindex' => '0'));
+        $html = html_writer::start_div('course-detail card');
+        $html .= html_writer::start_div('card-header');
+        $html .= html_writer::tag('h3', $fullname, array('id' => 'course-detail-title',
+                'class' => 'card-title', 'tabindex' => '0'));
+        $html .= html_writer::end_div();
+        $html .= html_writer::start_div('card-body');
         $html .= $this->course_detail_actions($course);
         foreach ($details as $class => $data) {
             $html .= $this->detail_pair($data['key'], $data['value'], $class);
         }
         $html .= html_writer::end_div();
+        $html .= html_writer::end_div();
         return $html;
     }
 
@@ -827,8 +838,8 @@ class core_course_management_renderer extends plugin_renderer_base {
      */
     protected function detail_pair($key, $value, $class ='') {
         $html = html_writer::start_div('detail-pair row yui3-g '.preg_replace('#[^a-zA-Z0-9_\-]#', '-', $class));
-        $html .= html_writer::div(html_writer::span($key), 'pair-key span3 col-md-3 yui3-u-1-4');
-        $html .= html_writer::div(html_writer::span($value), 'pair-value span9 col-md-9 m-b-1 yui3-u-3-4 form-inline');
+        $html .= html_writer::div(html_writer::span($key), 'pair-key col-md-3 yui3-u-1-4 font-weight-bold');
+        $html .= html_writer::div(html_writer::span($value), 'pair-value col-md-8 yui3-u-3-4');
         $html .= html_writer::end_div();
         return $html;
     }
@@ -836,7 +847,7 @@ class core_course_management_renderer extends plugin_renderer_base {
     /**
      * A collection of actions for a course.
      *
-     * @param core_course_list_element  $course The course to display actions for.
+     * @param core_course_list_element $course The course to display actions for.
      * @return string
      */
     public function course_detail_actions(core_course_list_element $course) {
@@ -846,9 +857,10 @@ class core_course_management_renderer extends plugin_renderer_base {
         }
         $options = array();
         foreach ($actions as $action) {
-            $options[] = $this->action_link($action['url'], $action['string']);
+            $options[] = $this->action_link($action['url'], $action['string'], null,
+                    array('class' => 'btn btn-sm btn-secondary mr-1 mb-3'));
         }
-        return html_writer::div(join(' | ', $options), 'listing-actions course-detail-listing-actions');
+        return html_writer::div(join('', $options), 'listing-actions course-detail-listing-actions');
     }
 
     /**
@@ -893,7 +905,7 @@ class core_course_management_renderer extends plugin_renderer_base {
      * @return string
      */
     public function grid_start($id = null, $class = null) {
-        $gridclass = 'grid-row-r row-fluid';
+        $gridclass = 'grid-start grid-row-r d-flex flex-wrap row';
         if (is_null($class)) {
             $class = $gridclass;
         } else {
@@ -925,30 +937,15 @@ class core_course_management_renderer extends plugin_renderer_base {
      */
     public function grid_column_start($size, $id = null, $class = null) {
 
-        // Calculate Bootstrap grid sizing.
-        $bootstrapclass = 'span'.$size.' col-md-'.$size;
-
-        // Calculate YUI grid sizing.
-        if ($size === 12) {
-            $maxsize = 1;
-            $size = 1;
-        } else {
-            $maxsize = 12;
-            $divisors = array(8, 6, 5, 4, 3, 2);
-            foreach ($divisors as $divisor) {
-                if (($maxsize % $divisor === 0) && ($size % $divisor === 0)) {
-                    $maxsize = $maxsize / $divisor;
-                    $size = $size / $divisor;
-                    break;
-                }
-            }
-        }
-        if ($maxsize > 1) {
-            $yuigridclass =  "grid-col-{$size}-{$maxsize} grid-col";
+        if ($id == 'course-detail') {
+            $size = 12;
+            $bootstrapclass = 'col-md-'.$size;
         } else {
-            $yuigridclass =  "grid-col-1 grid-col";
+            $bootstrapclass = 'd-flex flex-wrap px-3 mb-3';
         }
 
+        $yuigridclass = "col-sm";
+
         if (is_null($class)) {
             $class = $yuigridclass . ' ' . $bootstrapclass;
         } else {
@@ -958,7 +955,7 @@ class core_course_management_renderer extends plugin_renderer_base {
         if (!is_null($id)) {
             $attributes['id'] = $id;
         }
-        return html_writer::start_div($class, $attributes);
+        return html_writer::start_div($class . " grid_column_start", $attributes);
     }
 
     /**
@@ -1058,14 +1055,14 @@ class core_course_management_renderer extends plugin_renderer_base {
      *
      * @param array $courses The courses to display.
      * @param int $totalcourses The total number of courses to display.
-     * @param core_course_list_element  $course The currently selected course if there is one.
+     * @param core_course_list_element $course The currently selected course if there is one.
      * @param int $page The current page, starting at 0.
      * @param int $perpage The number of courses to display per page.
      * @param string $search The string we are searching for.
      * @return string
      */
     public function search_listing(array $courses, $totalcourses, core_course_list_element $course = null, $page = 0, $perpage = 20,
-        $search = '') {
+            $search = '') {
         $page = max($page, 0);
         $perpage = max($perpage, 2);
         $totalpages = ceil($totalcourses / $perpage);
@@ -1077,11 +1074,11 @@ class core_course_management_renderer extends plugin_renderer_base {
         $last = false;
         $i = $page * $perpage;
 
-        $html  = html_writer::start_div('course-listing', array(
-            'data-category' => 'search',
-            'data-page' => $page,
-            'data-totalpages' => $totalpages,
-            'data-totalcourses' => $totalcourses
+        $html  = html_writer::start_div('course-listing w-100', array(
+                'data-category' => 'search',
+                'data-page' => $page,
+                'data-totalpages' => $totalpages,
+                'data-totalcourses' => $totalcourses
         ));
         $html .= html_writer::tag('h3', get_string('courses'));
         $html .= $this->search_pagination($totalcourses, $page, $perpage);
@@ -1172,7 +1169,7 @@ class core_course_management_renderer extends plugin_renderer_base {
      *
      * This function will be called for every course being displayed by course_listing.
      *
-     * @param core_course_list_element  $course The course to produce HTML for.
+     * @param core_course_list_element $course The course to produce HTML for.
      * @param int $selectedcourse The id of the currently selected course.
      * @return string
      */
@@ -1180,20 +1177,20 @@ class core_course_management_renderer extends plugin_renderer_base {
 
         $text = $course->get_formatted_name();
         $attributes = array(
-            'class' => 'listitem listitem-course',
-            'data-id' => $course->id,
-            'data-selected' => ($selectedcourse == $course->id) ? '1' : '0',
-            'data-visible' => $course->visible ? '1' : '0'
+                'class' => 'listitem listitem-course list-group-item list-group-item-action',
+                'data-id' => $course->id,
+                'data-selected' => ($selectedcourse == $course->id) ? '1' : '0',
+                'data-visible' => $course->visible ? '1' : '0'
         );
         $bulkcourseinput = '';
         if (core_course_category::get($course->category)->can_move_courses_out_of()) {
             $bulkcourseinput = array(
-                'type' => 'checkbox',
-                'name' => 'bc[]',
-                'value' => $course->id,
-                'class' => 'bulk-action-checkbox',
-                'aria-label' => get_string('bulkactionselect', 'moodle', $text),
-                'data-action' => 'select'
+                    'type' => 'checkbox',
+                    'name' => 'bc[]',
+                    'value' => $course->id,
+                    'class' => 'bulk-action-checkbox',
+                    'aria-label' => get_string('bulkactionselect', 'moodle', $text),
+                    'data-action' => 'select'
             );
         }
         $viewcourseurl = new moodle_url($this->page->url, array('courseid' => $course->id));
@@ -1302,16 +1299,26 @@ class core_course_management_renderer extends plugin_renderer_base {
         $strsearchcourses = get_string("searchcourses");
         $searchurl = new moodle_url('/course/management.php');
 
-        $output = html_writer::start_tag('form', array('id' => $formid, 'action' => $searchurl, 'method' => 'get',
-            'class' => 'form-inline'));
-        $output .= html_writer::start_tag('fieldset', array('class' => 'coursesearchbox invisiblefieldset m-y-1'));
-        $output .= html_writer::tag('label', $strsearchcourses, array('for' => $inputid));
-        $output .= html_writer::empty_tag('input', array('type' => 'text', 'id' => $inputid, 'size' => $inputsize,
-            'name' => 'search', 'value' => s($value), 'class' => 'form-control m-x-1'));
-        $output .= html_writer::empty_tag('input', array('type' => 'submit', 'value' => get_string('go'),
-            'class' => 'btn btn-secondary'));
+        $output = html_writer::start_div('row');
+        $output .= html_writer::start_div('col-md-12');
+        $output .= html_writer::start_tag('form', array('class' => 'card', 'id' => $formid,
+                'action' => $searchurl, 'method' => 'get'));
+        $output .= html_writer::start_tag('fieldset', array('class' => 'coursesearchbox invisiblefieldset'));
+        $output .= html_writer::tag('div', $this->output->heading($strsearchcourses.': ', 2, 'm-0'),
+                array('class' => 'card-header'));
+        $output .= html_writer::start_div('card-body');
+        $output .= html_writer::start_div('input-group col-sm-6 col-lg-4 m-auto');
+        $output .= html_writer::empty_tag('input', array('class' => 'form-control', 'type' => 'text', 'id' => $inputid,
+                'size' => $inputsize, 'name' => 'search', 'value' => s($value)));
+        $output .= html_writer::start_tag('span', array('class' => 'input-group-btn'));
+        $output .= html_writer::tag('button', get_string('go'), array('class' => 'btn btn-primary', 'type' => 'submit'));
+        $output .= html_writer::end_tag('span');
+        $output .= html_writer::end_div();
+        $output .= html_writer::end_div();
         $output .= html_writer::end_tag('fieldset');
         $output .= html_writer::end_tag('form');
+        $output .= html_writer::end_div();
+        $output .= html_writer::end_div();
 
         return $output;
     }
index 3bba431..b7a01bd 100644 (file)
@@ -344,13 +344,13 @@ class core_course_renderer extends plugin_renderer_base {
     }
 
     /**
-     * Renders html to display a course search form
+     * Renders html to display a course search form.
      *
      * @param string $value default value to populate the search field
      * @param string $format display format - 'plain' (default), 'short' or 'navbar'
      * @return string
      */
-    function course_search_form($value = '', $format = 'plain') {
+    public function course_search_form($value = '', $format = 'plain') {
         static $count = 0;
         $formid = 'coursesearch';
         if ((++$count) > 1) {
@@ -372,23 +372,19 @@ class core_course_renderer extends plugin_renderer_base {
                 $inputsize = 30;
         }
 
-        $strsearchcourses= get_string("searchcourses");
-        $searchurl = new moodle_url('/course/search.php');
-
-        $output = html_writer::start_tag('form', array('id' => $formid, 'action' => $searchurl, 'method' => 'get'));
-        $output .= html_writer::start_tag('fieldset', array('class' => 'coursesearchbox invisiblefieldset'));
-        $output .= html_writer::tag('label', $strsearchcourses.': ', array('for' => $inputid));
-        $output .= html_writer::empty_tag('input', array('type' => 'text', 'id' => $inputid,
-            'size' => $inputsize, 'name' => 'search', 'value' => s($value)));
-        $output .= html_writer::empty_tag('input', array('type' => 'submit',
-            'value' => get_string('go')));
-        $output .= html_writer::end_tag('fieldset');
+        $data = (object) [
+                'searchurl' => (new moodle_url('/course/search.php'))->out(false),
+                'id' => $formid,
+                'inputid' => $inputid,
+                'inputsize' => $inputsize,
+                'value' => $value
+        ];
         if ($format != 'navbar') {
-            $output .= $this->output->help_icon("coursesearch", "core");
+            $helpicon = new \help_icon('coursesearch', 'core');
+            $data->helpicon = $helpicon->export_for_template($this);
         }
-        $output .= html_writer::end_tag('form');
 
-        return $output;
+        return $this->render_from_template('core_course/course_search_form', $data);
     }
 
     /**
diff --git a/course/templates/coursecard.mustache b/course/templates/coursecard.mustache
new file mode 100644 (file)
index 0000000..0ea7e8f
--- /dev/null
@@ -0,0 +1,69 @@
+{{!
+    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/>.
+}}
+{{!
+    @template course_course/coursecard
+
+    This template renders the a card for the course cards.
+
+    Example context (json):
+    {
+        "courses": [
+            {
+                "name": "Assignment due 1",
+                "viewurl": "https://moodlesite/course/view.php?id=2",
+                "courseimage": "https://moodlesite/pluginfile/123/course/overviewfiles/123.jpg",
+                "fullname": "course 3",
+                "hasprogress": true,
+                "progress": 10
+            }
+        ]
+    }
+}}
+<div class="card dashboard-card" role="listitem"
+    data-region="course-content"
+    data-course-id="{{{id}}}">
+    <a href="{{viewurl}}" tabindex="-1">
+        <div class="card-img dashboard-card-img" style='background-image: url("{{{courseimage}}}");'>
+            <span class="sr-only">{{#str}}aria:courseimage, core_course{{/str}}</span>
+        </div>
+    </a>
+    <div class="card-body pr-1 course-info-container" id="course-info-container-{{id}}-{{uniqid}}">
+        <div class="d-flex align-items-start">
+            <a href="{{viewurl}}" class="coursename mr-2 text-truncate">
+                <div class="text-muted muted d-flex w-100 mb-1 text-truncate" style="flex-flow:wrap;">
+                    {{$coursecategory}}{{/coursecategory}}
+                    {{#showshortname}}
+                    {{$divider}}{{/divider}}
+                    <span class="sr-only">
+                        {{#str}}aria:courseshortname, core_course{{/str}}
+                    </span>
+                    <div>
+                        {{{shortname}}}
+                    </div>
+                    {{/showshortname}}
+                </div>
+                {{> core_course/favouriteicon }}
+                <span class="sr-only">
+                    {{#str}}aria:coursename, core_course{{/str}}
+                </span>
+                {{$coursename}}{{/coursename}}
+            </a>
+            {{$menu}}{{/menu}}
+        </div>
+    </div>
+    {{$progress}}{{/progress}}
+</div>
index c2c09a0..88ae6be 100644 (file)
     }
 }}
 
-<div class="card-deck dashboard-card-deck {{$classes}}{{/classes}}" role="list">
+<div class="card-deck dashboard-card-deck {{$classes}}{{/classes}}" data-region="card-deck" role="list">
 {{#courses}}
-    <div class="card dashboard-card" role="listitem"
-        data-region="course-content"
-        data-course-id="{{{id}}}">
-        <a href="{{viewurl}}" tabindex="-1">
-            <div class="card-img dashboard-card-img" style='background-image: url("{{{courseimage}}}");'>
-                <span class="sr-only">{{#str}}aria:courseimage, core_course{{/str}}</span>
-            </div>
-        </a>
-        <div class="card-body pr-1 course-info-container" id="course-info-container-{{id}}-{{uniqid}}">
-            <div class="d-flex align-items-start">
-                <a href="{{viewurl}}" class="coursename mr-2 text-truncate">
-                    <div class="text-muted muted d-flex w-100 mb-1 text-truncate" style="flex-flow:wrap;">
-                        {{$coursecategory}}{{/coursecategory}}
-                        {{#showshortname}}
-                        {{$divider}}{{/divider}}
-                        <span class="sr-only">
-                            {{#str}}aria:courseshortname, core_course{{/str}}
-                        </span>
-                        <div>
-                            {{{shortname}}}
-                        </div>
-                        {{/showshortname}}
-                    </div>
-                    {{> core_course/favouriteicon }}
-                    <span class="sr-only">
-                        {{#str}}aria:coursename, core_course{{/str}}
-                    </span>
-                    {{$coursename}}{{/coursename}}
-                </a>
-                {{$menu}}{{/menu}}
-            </div>
-        </div>
-        {{$progress}}{{/progress}}
-    </div>
+    {{> core_course/coursecard }}
 {{/courses}}
 </div>
index 93b8860..ee06c76 100644 (file)
@@ -186,10 +186,11 @@ class behat_course extends behat_base {
             // We are on the frontpage.
             if ($section) {
                 // Section 1 represents the contents on the frontpage.
-                $sectionxpath = "//body[@id='page-site-index']/descendant::div[contains(concat(' ',normalize-space(@class),' '),' sitetopic ')]";
+                $sectionxpath = "//body[@id='page-site-index']" .
+                        "/descendant::div[contains(concat(' ',normalize-space(@class),' '),' sitetopic ')]";
             } else {
                 // Section 0 represents "Site main menu" block.
-                $sectionxpath = "//div[contains(concat(' ',normalize-space(@class),' '),' block_site_main_menu ')]";
+                $sectionxpath = "//*[contains(concat(' ',normalize-space(@class),' '),' block_site_main_menu ')]";
             }
         } else {
             // We are inside the course.
@@ -201,15 +202,16 @@ class behat_course extends behat_base {
         if ($this->running_javascript()) {
 
             // Clicks add activity or resource section link.
-            $sectionxpath = $sectionxpath . "/descendant::div[@class='section-modchooser']/span/a";
+            $sectionxpath = $sectionxpath . "/descendant::div" .
+                    "[contains(concat(' ', normalize-space(@class) , ' '), ' section-modchooser ')]/span/a";
             $sectionnode = $this->find('xpath', $sectionxpath);
             $sectionnode->click();
 
             // Clicks the selected activity if it exists.
             $activityxpath = "//div[@id='chooseform']/descendant::label" .
-                "/descendant::span[contains(concat(' ', normalize-space(@class), ' '), ' typename ')]" .
-                "[normalize-space(.)=$activityliteral]" .
-                "/parent::label/child::input";
+                    "/descendant::span[contains(concat(' ', normalize-space(@class), ' '), ' typename ')]" .
+                    "[normalize-space(.)=$activityliteral]" .
+                    "/parent::label/child::input";
             $activitynode = $this->find('xpath', $activityxpath);
             $activitynode->doubleClick();
 
@@ -217,8 +219,9 @@ class behat_course extends behat_base {
             // Without Javascript.
 
             // Selecting the option from the select box which contains the option.
-            $selectxpath = $sectionxpath . "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' section_add_menus ')]" .
-                "/descendant::select[option[normalize-space(.)=$activityliteral]]";
+            $selectxpath = $sectionxpath . "/descendant::div" .
+                    "[contains(concat(' ', normalize-space(@class), ' '), ' section_add_menus ')]" .
+                    "/descendant::select[option[normalize-space(.)=$activityliteral]]";
             $selectnode = $this->find('xpath', $selectxpath);
             $selectnode->selectOption($activity);
 
@@ -230,7 +233,6 @@ class behat_course extends behat_base {
 
     }
 
-
     /**
      * Opens a section edit menu if it is not already opened.
      *
@@ -248,7 +250,7 @@ class behat_course extends behat_base {
 
         // If it is already opened we do nothing.
         $xpath = $this->section_exists($sectionnumber);
-        $xpath .= "/descendant::div[contains(@class, 'section-actions')]/descendant::a[contains(@class, 'textmenu')]";
+        $xpath .= "/descendant::div[contains(@class, 'section-actions')]/descendant::a[contains(@data-toggle, 'dropdown')]";
 
         $exception = new ExpectationException('Section "' . $sectionnumber . '" was not found', $this->getSession());
         $menu = $this->find('xpath', $xpath, $exception);
@@ -550,8 +552,8 @@ class behat_course extends behat_base {
         // Edit menu should be visible.
         if ($this->is_course_editor()) {
             $xpath = $sectionxpath .
-                     "/descendant::div[contains(@class, 'section-actions')]" .
-                     "/descendant::a[contains(@class, 'textmenu')]";
+                    "/descendant::div[contains(@class, 'section-actions')]" .
+                    "/descendant::a[contains(@data-toggle, 'dropdown')]";
             if (!$this->getSession()->getPage()->find('xpath', $xpath)) {
                 throw new ExpectationException('The section edit menu is not available', $this->getSession());
             }
@@ -843,15 +845,23 @@ class behat_course extends behat_base {
 
         // If it is already opened we do nothing.
         $activitynode = $this->get_activity_node($activityname);
-        $classes = array_flip(explode(' ', $activitynode->getAttribute('class')));
-        if (!empty($classes['action-menu-shown'])) {
+
+        // Find the menu.
+        $menunode = $activitynode->find('css', 'a[data-toggle=dropdown]');
+        if (!$menunode) {
+            throw new ExpectationException(sprintf('Could not find actions menu for the activity "%s"', $activityname),
+                    $this->getSession());
+        }
+        $expanded = $menunode->getAttribute('aria-expanded');
+        if ($expanded == 'true') {
             return;
         }
 
         $this->execute('behat_course::i_click_on_in_the_activity',
-            array("a[role='menuitem']", "css_element", $this->escape($activityname))
+                array("a[data-toggle='dropdown']", "css_element", $this->escape($activityname))
         );
 
+        $this->actions_menu_should_be_open($activityname);
     }
 
     /**
@@ -869,13 +879,19 @@ class behat_course extends behat_base {
 
         // If it is already closed we do nothing.
         $activitynode = $this->get_activity_node($activityname);
-        $classes = array_flip(explode(' ', $activitynode->getAttribute('class')));
-        if (empty($classes['action-menu-shown'])) {
+        // Find the menu.
+        $menunode = $activitynode->find('css', 'a[data-toggle=dropdown]');
+        if (!$menunode) {
+            throw new ExpectationException(sprintf('Could not find actions menu for the activity "%s"', $activityname),
+                    $this->getSession());
+        }
+        $expanded = $menunode->getAttribute('aria-expanded');
+        if ($expanded != 'true') {
             return;
         }
 
         $this->execute('behat_course::i_click_on_in_the_activity',
-            array("a[role='menuitem']", "css_element", $this->escape($activityname))
+                array("a[data-toggle='dropdown']", "css_element", $this->escape($activityname))
         );
     }
 
@@ -892,10 +908,15 @@ class behat_course extends behat_base {
             throw new DriverException('Activities actions menu not available when Javascript is disabled');
         }
 
-        // If it is already closed we do nothing.
         $activitynode = $this->get_activity_node($activityname);
-        $classes = array_flip(explode(' ', $activitynode->getAttribute('class')));
-        if (empty($classes['action-menu-shown'])) {
+        // Find the menu.
+        $menunode = $activitynode->find('css', 'a[data-toggle=dropdown]');
+        if (!$menunode) {
+            throw new ExpectationException(sprintf('Could not find actions menu for the activity "%s"', $activityname),
+                    $this->getSession());
+        }
+        $expanded = $menunode->getAttribute('aria-expanded');
+        if ($expanded != 'true') {
             throw new ExpectationException(sprintf("The action menu for '%s' is not open", $activityname), $this->getSession());
         }
     }
@@ -1039,18 +1060,18 @@ class behat_course extends behat_base {
 
         // Determine the future new activity xpath from the former one.
         $duplicatedxpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')]" .
-            "[contains(., $activityliteral)]/following-sibling::li";
-        $duplicatedactionsmenuxpath = $duplicatedxpath . "/descendant::a[@role='menuitem']";
+                "[contains(., $activityliteral)]/following-sibling::li";
+        $duplicatedactionsmenuxpath = $duplicatedxpath . "/descendant::a[@data-toggle='dropdown']";
 
         if ($this->running_javascript()) {
             // We wait until the AJAX request finishes and the section is visible again.
             $hiddenlightboxxpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')]" .
-                "[contains(., $activityliteral)]" .
-                "/ancestor::li[contains(concat(' ', normalize-space(@class), ' '), ' section ')]" .
-                "/descendant::div[contains(concat(' ', @class, ' '), ' lightbox ')][contains(@style, 'display: none')]";
+                    "[contains(., $activityliteral)]" .
+                    "/ancestor::li[contains(concat(' ', normalize-space(@class), ' '), ' section ')]" .
+                    "/descendant::div[contains(concat(' ', @class, ' '), ' lightbox ')][contains(@style, 'display: none')]";
 
             $this->execute("behat_general::wait_until_exists",
-                array($this->escape($hiddenlightboxxpath), "xpath_element")
+                    array($this->escape($hiddenlightboxxpath), "xpath_element")
             );
 
             // Close the original activity actions menu.
@@ -1059,13 +1080,13 @@ class behat_course extends behat_base {
             // The next sibling of the former activity will be the duplicated one, so we click on it from it's xpath as, at
             // this point, it don't even exists in the DOM (the steps are executed when we return them).
             $this->execute('behat_general::i_click_on',
-                array($this->escape($duplicatedactionsmenuxpath), "xpath_element")
+                    array($this->escape($duplicatedactionsmenuxpath), "xpath_element")
             );
         }
 
         // We force the xpath as otherwise mink tries to interact with the former one.
         $this->execute('behat_general::i_click_on_in_the',
-            array(get_string('editsettings'), "link", $this->escape($duplicatedxpath), "xpath_element")
+                array(get_string('editsettings'), "link", $this->escape($duplicatedxpath), "xpath_element")
         );
 
         $this->execute("behat_forms::i_set_the_following_fields_to_these_values", $data);
@@ -1281,8 +1302,8 @@ class behat_course extends behat_base {
     protected function is_course_editor() {
 
         // We don't need to behat_base::spin() here as all is already loaded.
-        if (!$this->getSession()->getPage()->findButton(get_string('turneditingoff')) &&
-                !$this->getSession()->getPage()->findButton(get_string('turneditingon'))) {
+        if (!$this->getSession()->getPage()->findLink(get_string('turneditingoff')) &&
+                !$this->getSession()->getPage()->findLink(get_string('turneditingon'))) {
             return false;
         }
 
@@ -1842,7 +1863,8 @@ class behat_course extends behat_base {
      * @throws Behat\Mink\Exception\ExpectationException
      */
     protected function user_clicks_on_management_listing_action($listingtype, $listingnode, $action) {
-        $actionsnode = $listingnode->find('xpath', "//*[contains(concat(' ', normalize-space(@class), ' '), '{$listingtype}-item-actions')]");
+        $actionsnode = $listingnode->find('xpath', "//*" .
+                "[contains(concat(' ', normalize-space(@class), ' '), '{$listingtype}-item-actions')]");
         if (!$actionsnode) {
             throw new ExpectationException("Could not find the actions for $listingtype", $this->getSession());
         }
@@ -1851,7 +1873,7 @@ class behat_course extends behat_base {
             throw new ExpectationException("Expected action was not available or not found ($action)", $this->getSession());
         }
         if ($this->running_javascript() && !$actionnode->isVisible()) {
-            $actionsnode->find('css', 'a.toggle-display')->click();
+            $actionsnode->find('css', 'a[data-toggle=dropdown]')->click();
             $actionnode = $actionsnode->find('css', '.action-'.$action);
         }
         $actionnode->click();
@@ -1874,10 +1896,7 @@ class behat_course extends behat_base {
      * @Given /^I navigate to course participants$/
      */
     public function i_navigate_to_course_participants() {
-        $coursestr = behat_context_helper::escape(get_string('courses'));
-        $mycoursestr = behat_context_helper::escape(get_string('mycourses'));
-        $xpath = "//div[contains(@class,'block')]//li[p/*[string(.)=$coursestr or string(.)=$mycoursestr]]";
-        $this->execute('behat_general::i_click_on_in_the', [get_string('participants'), 'link', $xpath, 'xpath_element']);
+        $this->execute('behat_navigation::i_select_from_flat_navigation_drawer', get_string('participants'));
     }
 
     /**
index 1cf5509..0a6fcff 100644 (file)
@@ -602,8 +602,8 @@ class enrol_self_testcase extends advanced_testcase {
         $selfplugin->enrol_user($instance1, $user2->id, $editingteacherrole->id);
 
         $this->setUser($guest);
-        $noaccesshtml = get_string('noguestaccess', 'enrol') . $OUTPUT->continue_button(get_login_url());
-        $this->assertSame($noaccesshtml, $selfplugin->can_self_enrol($instance1, true));
+        $this->assertContains(get_string('noguestaccess', 'enrol'),
+                $selfplugin->can_self_enrol($instance1, true));
 
         $this->setUser($user1);
         $this->assertTrue($selfplugin->can_self_enrol($instance1, true));
index 1cf7e30..5484ff5 100644 (file)
@@ -134,124 +134,16 @@ class core_files_renderer extends plugin_renderer_base {
     /**
      * Returns html for displaying one file manager
      *
-     * The main element in HTML must have id="filemanager-{$client_id}" and
-     * class="filemanager fm-loading";
-     * After all necessary code on the page (both html and javascript) is loaded,
-     * the class fm-loading will be removed and added class fm-loaded;
-     * The main element (class=filemanager) will be assigned the following classes:
-     * 'fm-maxfiles' - when filemanager has maximum allowed number of files;
-     * 'fm-nofiles' - when filemanager has no files at all (although there might be folders);
-     * 'fm-noitems' - when current view (folder) has no items - neither files nor folders;
-     * 'fm-updating' - when current view is being updated (usually means that loading icon is to be displayed);
-     * 'fm-nomkdir' - when 'Make folder' action is unavailable (empty($fm->options->subdirs) == true)
-     *
-     * Element with class 'filemanager-container' will be holding evens for dnd upload (dragover, etc.).
-     * It will have class:
-     * 'dndupload-ready' - when a file is being dragged over the browser
-     * 'dndupload-over' - when file is being dragged over this filepicker (additional to 'dndupload-ready')
-     * 'dndupload-uploading' - during the upload process (note that after dnd upload process is
-     * over, the file manager will refresh the files list and therefore will have for a while class
-     * fm-updating. Both waiting processes should look similar so the images don't jump for user)
-     *
-     * If browser supports Drag-and-drop, the body element will have class 'dndsupported',
-     * otherwise - 'dndnotsupported';
-     *
-     * Element with class 'fp-content' will be populated with files list;
-     * Element with class 'fp-btn-add' will hold onclick event for adding a file (opening filepicker);
-     * Element with class 'fp-btn-mkdir' will hold onclick event for adding new folder;
-     * Element with class 'fp-btn-download' will hold onclick event for download action;
-     *
-     * Element with class 'fp-path-folder' is a template for one folder in path toolbar.
-     * It will hold mouse click event and will be assigned classes first/last/even/odd respectfully.
-     * Parent element will receive class 'empty' when there are no folders to be displayed;
-     * The content of subelement with class 'fp-path-folder-name' will be substituted with folder name;
-     *
-     * Element with class 'fp-viewbar' will have the class 'enabled' or 'disabled' when view mode
-     * can be changed or not;
-     * Inside element with class 'fp-viewbar' there are expected elements with classes
-     * 'fp-vb-icons', 'fp-vb-tree' and 'fp-vb-details'. They will handle onclick events to switch
-     * between the view modes, the last clicked element will have the class 'checked';
-     *
      * @param form_filemanager $fm
      * @return string
      */
     protected function fm_print_generallayout($fm) {
-        global $OUTPUT;
-        $options = $fm->options;
-        $client_id = $options->client_id;
-        $straddfile  = get_string('addfile', 'repository');
-        $strmakedir  = get_string('makeafolder', 'moodle');
-        $strdownload = get_string('downloadfolder', 'repository');
-        $strloading  = get_string('loading', 'repository');
-        $strdroptoupload = get_string('droptoupload', 'moodle');
-        $icon_progress = $OUTPUT->pix_icon('i/loading_small', $strloading).'';
-        $restrictions = $this->fm_print_restrictions($fm);
-        $strdndnotsupported = get_string('dndnotsupported_insentence', 'moodle').$OUTPUT->help_icon('dndnotsupported');
-        $strdndenabledinbox = get_string('dndenabled_inbox', 'moodle');
-        $loading = get_string('loading', 'repository');
-        $straddfiletext = get_string('addfiletext', 'repository');
-        $strcreatefolder = get_string('createfolder', 'repository');
-        $strdownloadallfiles = get_string('downloadallfiles', 'repository');
-
-        $html = '
-<div id="filemanager-'.$client_id.'" class="filemanager fm-loading">
-    <div class="fp-restrictions">
-        '.$restrictions.'
-        <span class="dnduploadnotsupported-message"> - '.$strdndnotsupported.' </span>
-    </div>
-    <div class="fp-navbar">
-        <div class="filemanager-toolbar">
-            <div class="fp-toolbar">
-                <div class="fp-btn-add">
-                    <a role="button" title="' . $straddfile . '" href="#">
-                        ' . $this->pix_icon('a/add_file', $straddfiletext) . '
-                    </a>
-                </div>
-                <div class="fp-btn-mkdir">
-                    <a role="button" title="' . $strmakedir . '" href="#">
-                        ' . $this->pix_icon('a/create_folder', $strcreatefolder) . '
-                    </a>
-                </div>
-                <div class="fp-btn-download">
-                    <a role="button" title="' . $strdownload . '" href="#">
-                        ' . $this->pix_icon('a/download_all', $strdownloadallfiles) . '
-                    </a>
-                </div>
-                <span class="fp-img-downloading">
-                    ' . $this->pix_icon('i/loading_small', '') . '
-                </span>
-            </div>
-            <div class="fp-viewbar">
-                <a title="'. get_string('displayicons', 'repository') .'" class="fp-vb-icons" href="#">
-                    ' . $this->pix_icon('fp/view_icon_active', get_string('displayasicons', 'repository'), 'theme') . '
-                </a>
-                <a title="'. get_string('displaydetails', 'repository') .'" class="fp-vb-details" href="#">
-                    ' . $this->pix_icon('fp/view_list_active', get_string('displayasdetails', 'repository'), 'theme') . '
-                </a>
-                <a title="'. get_string('displaytree', 'repository') .'" class="fp-vb-tree" href="#">
-                    ' . $this->pix_icon('fp/view_tree_active', get_string('displayastree', 'repository'), 'theme') . '
-                </a>
-            </div>
-        </div>
-        <div class="fp-pathbar">
-            <span class="fp-path-folder"><a class="fp-path-folder-name" href="#"></a></span>
-        </div>
-    </div>
-    <div class="filemanager-loading mdl-align">'.$icon_progress.'</div>
-    <div class="filemanager-container" >
-        <div class="fm-content-wrapper">
-            <div class="fp-content"></div>
-            <div class="fm-empty-container">
-                <div class="dndupload-message">'.$strdndenabledinbox.'<br/><div class="dndupload-arrow"></div></div>
-            </div>
-            <div class="dndupload-target">'.$strdroptoupload.'<br/><div class="dndupload-arrow"></div></div>
-            <div class="dndupload-progressbars"></div>
-            <div class="dndupload-uploadinprogress">'.$icon_progress.'</div>
-        </div>
-        <div class="filemanager-updating">'.$icon_progress.'</div>
-    </div>
-</div>';
-        return $html;
+        $context = [
+                'client_id' => $fm->options->client_id,
+                'helpicon' => $this->help_icon('setmainfile', 'repository'),
+                'restrictions' => $this->fm_print_restrictions($fm)
+        ];
+        return $this->render_from_template('core/filemanager_page_generallayout', $context);
     }
 
     /**
@@ -350,140 +242,21 @@ class core_files_renderer extends plugin_renderer_base {
     /**
      * FileManager JS template for window with file information/actions.
      *
-     * All content must be enclosed in one element, CSS for this class must define width and
-     * height of the window;
-     *
-     * Thumbnail image will be added as content to the element with class 'fp-thumbnail';
-     *
-     * Inside the window the elements with the following classnames must be present:
-     * 'fp-saveas', 'fp-author', 'fp-license', 'fp-path'. Inside each of them must be
-     * one input element (or select in case of fp-license and fp-path). They may also have labels.
-     * The elements will be assign with class 'uneditable' and input/select element will become
-     * disabled if they are not applicable for the particular file;
-     *
-     * There may be present elements with classes 'fp-original', 'fp-datemodified', 'fp-datecreated',
-     * 'fp-size', 'fp-dimensions', 'fp-reflist'. They will receive additional class 'fp-unknown' if
-     * information is unavailable. If there is information available, the content of embedded
-     * element with class 'fp-value' will be substituted with the value;
-     *
-     * The value of Original ('fp-original') is loaded in separate request. When it is applicable
-     * but not yet loaded the 'fp-original' element receives additional class 'fp-loading';
-     *
-     * The value of 'Aliases/Shortcuts' ('fp-reflist') is also loaded in separate request. When it
-     * is applicable but not yet loaded the 'fp-original' element receives additional class
-     * 'fp-loading'. The string explaining that XX references exist will replace content of element
-     * 'fp-refcount'. Inside '.fp-reflist .fp-value' each reference will be enclosed in <li>;
-     *
-     * Elements with classes 'fp-file-update', 'fp-file-download', 'fp-file-delete', 'fp-file-zip',
-     * 'fp-file-unzip', 'fp-file-setmain' and 'fp-file-cancel' will hold corresponding onclick
-     * events (there may be several elements with class 'fp-file-cancel');
-     *
-     * When confirm button is pressed and file is being selected, the top element receives
-     * additional class 'loading'. It is removed when response from server is received.
-     *
-     * When any of the input fields is changed, the top element receives class 'fp-changed';
-     * When current file can be set as main - top element receives class 'fp-cansetmain';
-     * When current file is folder/zip/file - top element receives respectfully class
-     * 'fp-folder'/'fp-zip'/'fp-file';
-     *
-     * @return string
      */
     protected function fm_js_template_fileselectlayout() {
-        global $OUTPUT;
-        $strloading  = get_string('loading', 'repository');
-        $iconprogress = $this->pix_icon('i/loading_small', $strloading).'';
-        $rv = '
-<div class="filemanager fp-select">
-    <div class="fp-select-loading">
-        ' . $this->pix_icon('i/loading_small', '') . '
-    </div>
-    <form class="form-horizontal">
-        <button class="fp-file-download">'.get_string('download').'</button>
-        <button class="fp-file-delete">'.get_string('delete').'</button>
-        <button class="fp-file-setmain">'.get_string('setmainfile', 'repository').'</button>
-        <span class="fp-file-setmain-help">'.$OUTPUT->help_icon('setmainfile', 'repository').'</span>
-        <button class="fp-file-zip">'.get_string('zip', 'editor').'</button>
-        <button class="fp-file-unzip">'.get_string('unzip').'</button>
-        <div class="fp-hr"></div>
-
-        <div class="fp-forminset">
-                <div class="fp-saveas control-group clearfix">
-                    <label class="control-label">'.get_string('name', 'repository').'</label>
-                    <div class="controls">
-                        <input type="text"/>
-                    </div>
-                </div>
-                <div class="fp-author control-group clearfix">
-                    <label class="control-label">'.get_string('author', 'repository').'</label>
-                    <div class="controls">
-                        <input type="text"/>
-                    </div>
-                </div>
-                <div class="fp-license control-group clearfix">
-                    <label class="control-label">'.get_string('chooselicense', 'repository').'</label>
-                    <div class="controls">
-                        <select></select>
-                    </div>
-                </div>
-                <div class="fp-path control-group clearfix">
-                    <label class="control-label">'.get_string('path', 'repository').'</label>
-                    <div class="controls">
-                        <select></select>
-                    </div>
-                </div>
-                <div class="fp-original control-group clearfix">
-                    <label class="control-label">'.get_string('original', 'repository').'</label>
-                    <div class="controls">
-                        <span class="fp-originloading">'.$iconprogress.' '.$strloading.'</span><span class="fp-value"></span>
-                    </div>
-                </div>
-                <div class="fp-reflist control-group clearfix">
-                    <label class="control-label">'.get_string('referenceslist', 'repository').'</label>
-                    <div class="controls">
-                        <p class="fp-refcount"></p>
-                        <span class="fp-reflistloading">'.$iconprogress.' '.$strloading.'</span>
-                        <ul class="fp-value"></ul>
-                    </div>
-                </div>
-        </div>
-        <div class="fp-select-buttons">
-            <button class="fp-file-update btn-primary btn">'.get_string('update', 'moodle').'</button>
-            <button class="fp-file-cancel btn-cancel btn">'.get_string('cancel').'</button>
-        </div>
-    </form>
-    <div class="fp-info clearfix">
-        <div class="fp-hr"></div>
-        <p class="fp-thumbnail"></p>
-        <div class="fp-fileinfo">
-            <div class="fp-datemodified">'.get_string('lastmodified', 'repository').' <span class="fp-value"></span></div>
-            <div class="fp-datecreated">'.get_string('datecreated', 'repository').' <span class="fp-value"></span></div>
-            <div class="fp-size">'.get_string('size', 'repository').' <span class="fp-value"></span></div>
-            <div class="fp-dimensions">'.get_string('dimensions', 'repository').' <span class="fp-value"></span></div>
-        </div>
-    </div>
-</div>';
-        return $rv;
+        $context = [
+                'helpicon' => $this->help_icon('setmainfile', 'repository')
+        ];
+        return $this->render_from_template('core/filemanager_fileselect', $context);
     }
 
     /**
      * FileManager JS template for popup confirm dialogue window.
      *
-     * Must have one top element, CSS for this element must define width and height of the window;
-     *
-     * content of element with class 'fp-dlg-text' will be replaced with dialog text;
-     * elements with classes 'fp-dlg-butconfirm' and 'fp-dlg-butcancel' will
-     * hold onclick events;
-     *
      * @return string
      */
     protected function fm_js_template_confirmdialog() {
-        $rv = '
-<div class="filemanager fp-dlg">
-    <div class="fp-dlg-text"></div>
-    <button class="fp-dlg-butconfirm btn-primary btn">'.get_string('ok').'</button>
-    <button class="fp-dlg-butcancel btn-cancel btn">'.get_string('cancel').'</button>
-</div>';
-        return $rv;
+        return $this->render_from_template('core/filemanager_confirmdialog', []);
     }
 
     /**
@@ -529,112 +302,11 @@ class core_files_renderer extends plugin_renderer_base {
     /**
      * Template for FilePicker with general layout (not QuickUpload).
      *
-     * Must have one top element containing everything else (recommended <div class="file-picker">),
-     * CSS for this element must define width and height of the filepicker window. Or CSS must
-     * define min-width, max-width, min-height and max-height and in this case the filepicker
-     * window will be resizeable;
-     *
-     * Element with class 'fp-viewbar' will have the class 'enabled' or 'disabled' when view mode
-     * can be changed or not;
-     * Inside element with class 'fp-viewbar' there are expected elements with classes
-     * 'fp-vb-icons', 'fp-vb-tree' and 'fp-vb-details'. They will handle onclick events to switch
-     * between the view modes, the last clicked element will have the class 'checked';
-     *
-     * Element with class 'fp-repo' is a template for displaying one repository. Other repositories
-     * will be attached as siblings (classes first/last/even/odd will be added respectfully).
-     * The currently selected repostory will have class 'active'. Contents of element with class
-     * 'fp-repo-name' will be replaced with repository name, source of image with class
-     * 'fp-repo-icon' will be replaced with repository icon;
-     *
-     * Element with class 'fp-content' is obligatory and will hold the current contents;
-     *
-     * Element with class 'fp-paging' will contain page navigation (will be deprecated soon);
-     *
-     * Element with class 'fp-path-folder' is a template for one folder in path toolbar.
-     * It will hold mouse click event and will be assigned classes first/last/even/odd respectfully.
-     * Parent element will receive class 'empty' when there are no folders to be displayed;
-     * The content of subelement with class 'fp-path-folder-name' will be substituted with folder name;
-     *
-     * Element with class 'fp-toolbar' will have class 'empty' if all 'Back', 'Search', 'Refresh',
-     * 'Logout', 'Manage' and 'Help' are unavailable for this repo;
-     *
-     * Inside fp-toolbar there are expected elements with classes fp-tb-back, fp-tb-search,
-     * fp-tb-refresh, fp-tb-logout, fp-tb-manage and fp-tb-help. Each of them will have
-     * class 'enabled' or 'disabled' if particular repository has this functionality.
-     * Element with class 'fp-tb-search' must contain empty form inside, it's contents will
-     * be substituted with the search form returned by repository (in the most cases it
-     * is generated with template core_repository_renderer::repository_default_searchform);
-     * Other elements must have either <a> or <button> element inside, it will hold onclick
-     * event for corresponding action; labels for fp-tb-back and fp-tb-logout may be
-     * replaced with those specified by repository;
      *
      * @return string
      */
     protected function fp_js_template_generallayout() {
-        $rv = '
-<div tabindex="0" class="file-picker fp-generallayout" role="dialog" aria-live="assertive">
-    <div class="fp-repo-area">
-        <ul class="fp-list" role="tablist">
-            <li class="fp-repo" role="tab" aria-selected="false" tabindex="-1">
-                <a href="#" tabindex="-1"><img class="fp-repo-icon" alt=" " width="16" height="16" />&nbsp;
-                    <span class="fp-repo-name"></span>
-                </a>
-            </li>
-        </ul>
-    </div>
-    <div class="fp-repo-items" tabindex="0">
-        <div class="fp-navbar">
-            <div>
-                <div class="fp-toolbar">
-                    <div class="fp-tb-back">
-                        <a href="#">'.get_string('back', 'repository').'</a>
-                    </div>
-                    <div class="fp-tb-search">
-                        <form></form>
-                    </div>
-                    <div class="fp-tb-refresh">
-                        <a title="'. get_string('refresh', 'repository') .'" href="#">
-                            ' . $this->pix_icon('a/refresh', '') . '
-                        </a>
-                    </div>
-                    <div class="fp-tb-logout">
-                        <a title="'. get_string('logout', 'repository') .'" href="#">
-                            ' . $this->pix_icon('a/logout', '') . '
-                        </a>
-                    </div>
-                    <div class="fp-tb-manage">
-                        <a title="'. get_string('manageurl', 'repository') .'" href="#">
-                            ' . $this->pix_icon('a/setting', '') . '
-                        </a>
-                    </div>
-                    <div class="fp-tb-help">
-                        <a title="'. get_string('help', 'repository') .'" href="#">
-                            ' . $this->pix_icon('a/help', '') . '
-                        </a>
-                    </div>
-                    <div class="fp-tb-message"></div>
-                </div>
-                <div class="fp-viewbar">
-                    <a role="button" title="'. get_string('displayicons', 'repository') .'" class="fp-vb-icons" href="#">
-                        ' . $this->pix_icon('fp/view_icon_active', '', 'theme') . '
-                    </a>
-                    <a role="button" title="'. get_string('displaydetails', 'repository') .'" class="fp-vb-details" href="#">
-                        ' . $this->pix_icon('fp/view_list_active', '', 'theme') . '
-                    </a>
-                    <a role="button" title="'. get_string('displaytree', 'repository') .'" class="fp-vb-tree" href="#">
-                        ' . $this->pix_icon('fp/view_tree_active', '', 'theme') . '
-                    </a>
-                </div>
-                <div class="fp-clear-left"></div>
-            </div>
-            <div class="fp-pathbar">
-                 <span class="fp-path-folder"><a class="fp-path-folder-name" href="#"></a></span>
-            </div>
-        </div>
-        <div class="fp-content"></div>
-    </div>
-</div>';
-        return $rv;
+        return $this->render_from_template('core/filemanager_modal_generallayout', []);
     }
 
     /**
@@ -723,159 +395,19 @@ class core_files_renderer extends plugin_renderer_base {
     /**
      * FilePicker JS template for window appearing to select a file.
      *
-     * All content must be enclosed in one element, CSS for this class must define width and
-     * height of the window;
-     *
-     * Thumbnail image will be added as content to the element with class 'fp-thumbnail';
-     *
-     * Inside the window the elements with the following classnames must be present:
-     * 'fp-saveas', 'fp-linktype-2', 'fp-linktype-1', 'fp-linktype-4', 'fp-setauthor',
-     * 'fp-setlicense'. Inside each of them must have one input element (or select in case of
-     * fp-setlicense). They may also have labels.
-     * The elements will be assign with class 'uneditable' and input/select element will become
-     * disabled if they are not applicable for the particular file;
-     *
-     * There may be present elements with classes 'fp-datemodified', 'fp-datecreated', 'fp-size',
-     * 'fp-license', 'fp-author', 'fp-dimensions'. They will receive additional class 'fp-unknown'
-     * if information is unavailable. If there is information available, the content of embedded
-     * element with class 'fp-value' will be substituted with the value;
-     *
-     * Elements with classes 'fp-select-confirm' and 'fp-select-cancel' will hold corresponding
-     * onclick events;
-     *
-     * When confirm button is pressed and file is being selected, the top element receives
-     * additional class 'loading'. It is removed when response from server is received.
-     *
      * @return string
      */
     protected function fp_js_template_selectlayout() {
-        $rv = '
-<div class="file-picker fp-select">
-    <div class="fp-select-loading">
-        ' . $this->pix_icon('i/loading_small', '') . '
-    </div>
-    <form class="form-horizontal">
-        <div class="fp-forminset">
-                <div class="fp-linktype-2 control-group control-radio clearfix">
-                    <label class="control-label control-radio">'.get_string('makefileinternal', 'repository').'</label>
-                    <div class="controls control-radio">
-                        <input type="radio"/>
-                    </div>
-                </div>
-                <div class="fp-linktype-1 control-group control-radio clearfix">
-                    <label class="control-label control-radio">'.get_string('makefilelink', 'repository').'</label>
-                    <div class="controls control-radio">
-                        <input type="radio"/>
-                    </div>
-                </div>
-                <div class="fp-linktype-4 control-group control-radio clearfix">
-                    <label class="control-label control-radio">'.get_string('makefilereference', 'repository').'</label>
-                    <div class="controls control-radio">
-                        <input type="radio"/>
-                    </div>
-                </div>
-                <div class="fp-linktype-8 control-group control-radio clearfix">
-                    <label class="control-label control-radio">'.get_string('makefilecontrolledlink', 'repository').'</label>
-                    <div class="controls control-radio">
-                        <input type="radio"/>
-                    </div>
-                </div>
-                <div class="fp-saveas control-group clearfix">
-                    <label class="control-label">'.get_string('saveas', 'repository').'</label>
-                    <div class="controls">
-                        <input type="text"/>
-                    </div>
-                </div>
-                <div class="fp-setauthor control-group clearfix">
-                    <label class="control-label">'.get_string('author', 'repository').'</label>
-                    <div class="controls">
-                        <input type="text"/>
-                    </div>
-                </div>
-                <div class="fp-setlicense control-group clearfix">
-                    <label class="control-label">'.get_string('chooselicense', 'repository').'</label>
-                    <div class="controls">
-                        <select></select>
-                    </div>
-                </div>
-        </div>
-       <div class="fp-select-buttons">
-            <button class="fp-select-confirm btn-primary btn">'.get_string('getfile', 'repository').'</button>
-            <button class="fp-select-cancel btn-cancel btn">'.get_string('cancel').'</button>
-        </div>
-    </form>
-    <div class="fp-info clearfix">
-        <div class="fp-hr"></div>
-        <p class="fp-thumbnail"></p>
-        <div class="fp-fileinfo">
-            <div class="fp-datemodified">'.get_string('lastmodified', 'repository').'<span class="fp-value"></span></div>
-            <div class="fp-datecreated">'.get_string('datecreated', 'repository').'<span class="fp-value"></span></div>
-            <div class="fp-size">'.get_string('size', 'repository').'<span class="fp-value"></span></div>
-            <div class="fp-license">'.get_string('license', 'repository').'<span class="fp-value"></span></div>
-            <div class="fp-author">'.get_string('author', 'repository').'<span class="fp-value"></span></div>
-            <div class="fp-dimensions">'.get_string('dimensions', 'repository').'<span class="fp-value"></span></div>
-        </div>
-    </div>
-</div>';
-        return $rv;
+        return $this->render_from_template('core/filemanager_selectlayout', []);
     }
 
     /**
      * FilePicker JS template for 'Upload file' repository
      *
-     * Content to display when user chooses 'Upload file' repository (will be nested inside
-     * element with class 'fp-content').
-     *
-     * Must contain form (enctype="multipart/form-data" method="POST")
-     *
-     * The elements with the following classnames must be present:
-     * 'fp-file', 'fp-saveas', 'fp-setauthor', 'fp-setlicense'. Inside each of them must have
-     * one input element (or select in case of fp-setlicense). They may also have labels.
-     *
-     * Element with class 'fp-upload-btn' will hold onclick event for uploading the file;
-     *
-     * Please note that some fields may be hidden using CSS if this is part of quickupload form
-     *
      * @return string
      */
     protected function fp_js_template_uploadform() {
-        $rv = '
-<div class="fp-upload-form">
-    <div class="fp-content-center">
-        <form enctype="multipart/form-data" method="POST" class="form-horizontal">
-            <div class="fp-formset">
-                <div class="fp-file control-group clearfix">
-                    <label class="control-label">'.get_string('attachment', 'repository').'</label>
-                    <div class="controls">
-                        <input type="file"/>
-                    </div>
-                </div>
-                <div class="fp-saveas control-group clearfix">
-                    <label class="control-label">'.get_string('saveas', 'repository').'</label>
-                    <div class="controls">
-                        <input type="text"/>
-                    </div>
-                </div>
-                <div class="fp-setauthor control-group clearfix">
-                    <label class="control-label">'.get_string('author', 'repository').'</label>
-                    <div class="controls">
-                        <input type="text"/>
-                    </div>
-                </div>
-                <div class="fp-setlicense control-group clearfix">
-                    <label class="control-label">'.get_string('chooselicense', 'repository').'</label>
-                    <div class="controls">
-                        <select ></select>
-                    </div>
-                </div>
-            </div>
-        </form>
-        <div class="mdl-align">
-            <button class="fp-upload-btn btn-primary btn">'.get_string('upload', 'repository').'</button>
-        </div>
-    </div>
-</div> ';
-        return $rv;
+        return $this->render_from_template('core/filemanager_uploadform', []);
     }
 
     /**
@@ -932,115 +464,29 @@ class core_files_renderer extends plugin_renderer_base {
     /**
      * FilePicker JS template for popup dialogue window asking for action when file with the same name already exists.
      *
-     * Must have one top element, CSS for this element must define width and height of the window;
-     *
-     * content of element with class 'fp-dlg-text' will be replaced with dialog text;
-     * elements with classes 'fp-dlg-butoverwrite', 'fp-dlg-butrename',
-     * 'fp-dlg-butoverwriteall', 'fp-dlg-butrenameall' and 'fp-dlg-butcancel' will
-     * hold onclick events;
-     *
-     * content of element with class 'fp-dlg-butrename' will be substituted with appropriate string
-     * (Note that it may have long text)
-     *
      * @return string
      */
     protected function fp_js_template_processexistingfile() {
-        $rv = '
-<div class="file-picker fp-dlg">
-    <p class="fp-dlg-text"></p>
-    <div class="fp-dlg-buttons">
-        <button class="fp-dlg-butoverwrite btn">'.get_string('overwrite', 'repository').'</button>
-        <button class="fp-dlg-butrename btn"></button>
-        <button class="fp-dlg-butcancel btn btn-cancel">'.get_string('cancel').'</button>
-    </div>
-</div>';
-        return $rv;
+        return $this->render_from_template('core/filemanager_processexistingfile', []);
     }
 
     /**
-     * FilePicker JS template for popup dialogue window asking for action when file with the same name already exists (multiple-file version).
-     *
-     * Must have one top element, CSS for this element must define width and height of the window;
-     *
-     * content of element with class 'fp-dlg-text' will be replaced with dialog text;
-     * elements with classes 'fp-dlg-butoverwrite', 'fp-dlg-butrename' and 'fp-dlg-butcancel' will
-     * hold onclick events;
-     *
-     * content of element with class 'fp-dlg-butrename' will be substituted with appropriate string
-     * (Note that it may have long text)
+     * FilePicker JS template for popup dialogue window asking for action when file with the same name already exists
+     * (multiple-file version).
      *
      * @return string
      */
     protected function fp_js_template_processexistingfilemultiple() {
-        $rv = '
-<div class="file-picker fp-dlg">
-    <p class="fp-dlg-text"></p>
-    <a class="fp-dlg-butoverwrite fp-panel-button" href="#">'.get_string('overwrite', 'repository').'</a>
-    <a class="fp-dlg-butcancel fp-panel-button" href="#">'.get_string('cancel').'</a>
-    <a class="fp-dlg-butrename fp-panel-button" href="#"></a>
-    <br/>
-    <a class="fp-dlg-butoverwriteall fp-panel-button" href="#">'.get_string('overwriteall', 'repository').'</a>
-    <a class="fp-dlg-butrenameall fp-panel-button" href="#">'.get_string('renameall', 'repository').'</a>
-</div>';
-        return $rv;
+        return $this->render_from_template('core/filemanager_processexistingfilemultiple', []);
     }
 
     /**
      * FilePicker JS template for repository login form including templates for each element type
      *
-     * Must contain one <form> element with templates for different input types inside:
-     * Elements with classes 'fp-login-popup', 'fp-login-textarea', 'fp-login-select' and
-     * 'fp-login-input' are templates for displaying respective login form elements. Inside
-     * there must be exactly one element with type <button>, <textarea>, <select> or <input>
-     * (i.e. fp-login-popup should have <button>, fp-login-textarea should have <textarea>, etc.);
-     * They may also contain the <label> element and it's content will be substituted with
-     * label;
-     *
-     * You can also define elements with classes 'fp-login-checkbox', 'fp-login-text'
-     * but if they are not found, 'fp-login-input' will be used;
-     *
-     * Element with class 'fp-login-radiogroup' will be used for group of radio inputs. Inside
-     * it should hava a template for one radio input (with class 'fp-login-radio');
-     *
-     * Element with class 'fp-login-submit' will hold on click mouse event (form submission). It
-     * will be removed if at least one popup element is present;
-     *
      * @return string
      */
     protected function fp_js_template_loginform() {
-        $rv = '
-<div class="fp-login-form">
-    <div class="fp-content-center">
-        <form class="form-horizontal">
-            <div class="fp-formset">
-                <div class="fp-login-popup control-group clearfix">
-                    <div class="controls fp-popup">
-                        <button class="fp-login-popup-but btn-primary btn">'.get_string('login', 'repository').'</button>
-                    </div>
-                </div>
-                <div class="fp-login-textarea control-group clearfix">
-                    <div class="controls"><textarea></textarea></div>
-                </div>
-                <div class="fp-login-select control-group clearfix">
-                    <label class="control-label"></label>
-
-                    <div class="controls"><select></select></div>
-                </div>';
-        $rv .= '
-                <div class="fp-login-input control-group clearfix">
-                    <label class="control-label"></label>
-                    <div class="controls"><input/></div>
-                </div>
-                <div class="fp-login-radiogroup control-group clearfix">
-                    <label class="control-label"></label>
-                    <div class="controls fp-login-radio"><input /> <label></label></div>
-                </div>
-            </div>
-            <p><button class="fp-login-submit btn-primary btn">'.get_string('submit', 'repository').'</button></p>
-        </form>
-    </div>
-</div>';
-        return $rv;
+        return $this->render_from_template('core/filemanager_loginform', []);
     }
 
     /**
@@ -1066,13 +512,7 @@ class core_files_renderer extends plugin_renderer_base {
      * Default contents is one text input field with name="s"
      */
     public function repository_default_searchform() {
-        $searchinput = html_writer::label(get_string('searchrepo', 'repository'),
-            'reposearch', false, array('class' => 'accesshide'));
-        $searchinput .= html_writer::empty_tag('input', array('type' => 'text',
-            'id' => 'reposearch', 'name' => 's', 'value' => get_string('search', 'repository')));
-        $str = html_writer::tag('div', $searchinput, array('class' => "fp-def-search"));
-
-        return $str;
+        return $this->render_from_template('core/filemanager_default_searchform', []);
     }
 }
 
index 883121c..1199a1e 100644 (file)
     }
 }}
 <div class="gradingform_guide_comment_chooser" id="comment_chooser">
-    <ul role="list">
+    <div class="list-group">
         {{#comments}}
-            <li role="listitem">
-                <button id="comment-option-{{criterionId}}-{{id}}" class="btn btn-link" tabindex="0">
-                    {{description}}
-                </button>
-            </li>
+            <button class="list-group-item list-group-item-action" id="comment-option-{{criterionId}}-{{id}}" tabindex="0">
+                {{description}}
+            </button>
         {{/comments}}
-    </ul>
+    </div>
 </div>
index 384822a..f7ca951 100644 (file)
@@ -46,50 +46,8 @@ class renderer extends \plugin_renderer_base {
      * @return string HTML to display
      */
     protected function render_user_button(user_button $button) {
-        $attributes = array('type'     => 'button',
-                            'class'    => 'selectortrigger',
-                            'value'    => $button->label,
-                            'disabled' => $button->disabled ? 'disabled' : null,
-                            'title'    => $button->tooltip);
-
-        if ($button->actions) {
-            $id = \html_writer::random_id('single_button');
-            $attributes['id'] = $id;
-            foreach ($button->actions as $action) {
-                $this->add_action_handler($action, $id);
-            }
-        }
-        // First the input element.
-        $output = \html_writer::empty_tag('input', $attributes);
-
-        // Then hidden fields.
-        $params = $button->url->params();
-        if ($button->method === 'post') {
-            $params['sesskey'] = sesskey();
-        }
-        foreach ($params as $var => $val) {
-            $output .= \html_writer::empty_tag('input', array('type' => 'hidden', 'name' => $var, 'value' => $val));
-        }
-
-        // Then div wrapper for xhtml strictness.
-        $output = \html_writer::tag('div', $output);
-
-        // Now the form itself around it.
-        if ($button->method === 'get') {
-            $url = $button->url->out_omit_querystring(true); // Url without params, the anchor part allowed.
-        } else {
-            $url = $button->url->out_omit_querystring();     // Url without params, the anchor part not allowed.
-        }
-        if ($url === '') {
-            $url = '#'; // There has to be always some action.
-        }
-        $attributes = array('method' => $button->method,
-                            'action' => $url,
-                            'id'     => $button->formid);
-        $output = \html_writer::tag('div', $output, $attributes);
-
-        // Finally one more wrapper with class.
-        return \html_writer::tag('div', $output, array('class' => $button->class));
+        $data = $button->export_for_template($this);
+        return $this->render_from_template('gradereport_history/user_button', $data);
     }
 
     /**
index ad4b657..abe592a 100644 (file)
     <input type="checkbox" name="{{applyname}}" value="1" id="{{applyname}}">
     <label for="{{applyname}}">{{applylabel}}</label>
 </div>
-<fieldset>
+<fieldset class="form-inline">
     <legend class="accesshide">{{label}}</legend>
     <label for="{{menuname}}">{{menulabel}}</label>
-    <select name="{{menuname}}" id="{{menuname}}">
+    <select name="{{menuname}}" id="{{menuname}}" class="form-control">
         {{#menuoptions}}
             <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
         {{/menuoptions}}
index a922e89..43fdbcf 100644 (file)
@@ -17,4 +17,4 @@
 {{!
     Button.
 }}
-<input type="{{type}}" value={{#quote}}{{value}}{{/quote}}>
+<input type="{{type}}" value={{#quote}}{{value}}{{/quote}} class="btn btn-secondary">
index e6388ca..9f3be1a 100644 (file)
@@ -17,7 +17,7 @@
 {{!
     Dropdown attribute.
 }}
-<select id="{{name}}" name="{{name}}" tabindex="1" {{#disabled}}disabled{{/disabled}}>
+<select id="{{name}}" name="{{name}}" class="custom-select" tabindex="1" {{#disabled}}disabled{{/disabled}}>
     {{#options}}
         <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
     {{/options}}
index 3b93780..ffd06e9 100644 (file)
@@ -18,5 +18,5 @@
     Text attribute.
 }}
 <label for="{{name}}" class="accesshide">{{label}}</label>
-<input id="{{name}}" name="{{name}}" type="text" value="{{value}}" {{#tabindex}}tabindex="{{.}}"{{/tabindex}} {{#disabled}}disabled{{/disabled}}>
+<input id="{{name}}" name="{{name}}" type="text" value="{{value}}" class="form-control" {{#tabindex}}tabindex="{{.}}"{{/tabindex}} {{#disabled}}disabled{{/disabled}}>
 <input type="hidden" name="old{{name}}" value="{{value}}">
index 12e6789..b17c4cd 100644 (file)
@@ -76,15 +76,18 @@ class behat_grade extends behat_base {
         $gradeitem = behat_context_helper::escape($gradeitem);
 
         if ($this->running_javascript()) {
-            $xpath = "//tr[contains(.,$gradeitem)]//*[contains(@class,'moodle-actionmenu')]//a[contains(@class,'toggle-display')]";
+            $xpath = "//tr[contains(.,$gradeitem)]//*[contains(@class,'moodle-actionmenu')]";
             if ($this->getSession()->getPage()->findAll('xpath', $xpath)) {
-                $this->execute("behat_general::i_click_on", array($this->escape($xpath), "xpath_element"));
+                $this->execute("behat_action_menu::i_open_the_action_menu_in",
+                        array("//tr[contains(.,$gradeitem)]",
+                                "xpath_element"));
             }
         }
 
         $savechanges = get_string('savechanges', 'grades');
         $edit = behat_context_helper::escape(get_string('edit') . '  ');
-        $linkxpath = "//a[./img[starts-with(@title,$edit) and contains(@title,$gradeitem)]]";
+        $linkxpath = "//a[./*[contains(concat(' ', normalize-space(@class), ' '), ' icon ') " .
+                "and starts-with(@title,$edit) and contains(@title,$gradeitem)]]";
 
         $this->execute("behat_general::i_click_on", array($this->escape($linkxpath), "xpath_element"));
         $this->execute("behat_forms::i_set_the_following_fields_to_these_values", $data);
@@ -128,16 +131,19 @@ class behat_grade extends behat_base {
         $gradeitem = behat_context_helper::escape($gradeitem);
 
         if ($this->running_javascript()) {
-            $xpath = "//tr[contains(.,$gradeitem)]//*[contains(@class,'moodle-actionmenu')]//a[contains(@class,'toggle-display')]";
+            $xpath = "//tr[contains(.,$gradeitem)]//*[contains(@class,'moodle-actionmenu')]";
             if ($this->getSession()->getPage()->findAll('xpath', $xpath)) {
-                $this->execute("behat_general::i_click_on", array($this->escape($xpath), "xpath_element"));
+                $this->execute("behat_action_menu::i_open_the_action_menu_in",
+                        array("//tr[contains(.,$gradeitem)]",
+                                "xpath_element"));
             }
         }
 
         // Going to edit calculation.
         $savechanges = get_string('savechanges', 'grades');
         $edit = behat_context_helper::escape(get_string('editcalculation', 'grades'));
-        $linkxpath = "//a[./img[starts-with(@title,$edit) and contains(@title,$gradeitem)]]";
+        $linkxpath = "//a[./*[contains(concat(' ', normalize-space(@class), ' '), ' icon ') " .
+                "and starts-with(@title,$edit) and contains(@title,$gradeitem)]]";
         $this->execute("behat_general::i_click_on", array($this->escape($linkxpath), "xpath_element"));
 
         // Mapping names to idnumbers.
@@ -145,11 +151,12 @@ class behat_grade extends behat_base {
         foreach ($datahash as $gradeitem => $idnumber) {
             // This xpath looks for course, categories and items with the provided name.
             // Grrr, we can't equal in categoryitem and courseitem because there is a line jump...
-            $inputxpath ="//input[@class='idnumber'][" .
-                "parent::li[@class='item'][text()='" . $gradeitem . "']" .
-                " or " .
-                "parent::li[@class='categoryitem' or @class='courseitem']/parent::ul/parent::li[starts-with(text(),'" . $gradeitem . "')]" .
-            "]";
+            $inputxpath = "//input[@class='idnumber'][" .
+                    "parent::li[@class='item'][text()='" . $gradeitem . "']" .
+                    " or " .
+                    "parent::li[@class='categoryitem' or @class='courseitem']" .
+                    "/parent::ul/parent::li[starts-with(text(),'" . $gradeitem . "')]" .
+                    "]";
             $this->execute('behat_forms::i_set_the_field_with_xpath_to', array($inputxpath, $idnumber));
         }
 
@@ -174,17 +181,18 @@ class behat_grade extends behat_base {
         $gradeitem = behat_context_helper::escape($gradeitem);
 
         if ($this->running_javascript()) {
-            $xpath = "//tr[contains(.,$gradecategorytotal)]//*[contains(@class,'moodle-actionmenu')]" .
-                "//a[contains(@class,'toggle-display')]";
+            $xpath = "//tr[contains(.,$gradecategorytotal)]//*[contains(@class,'moodle-actionmenu')]";
             if ($this->getSession()->getPage()->findAll('xpath', $xpath)) {
-                $this->execute("behat_general::i_click_on", array($this->escape($xpath), "xpath_element"));
+                $xpath = "//tr[contains(.,$gradecategorytotal)]";
+                $this->execute("behat_action_menu::i_open_the_action_menu_in", array($xpath, "xpath_element"));
             }
         }
 
         // Going to edit calculation.
         $savechanges = get_string('savechanges', 'grades');
         $edit = behat_context_helper::escape(get_string('editcalculation', 'grades'));
-        $linkxpath = "//a[./img[starts-with(@title,$edit) and contains(@title,$gradeitem)]]";
+        $linkxpath = "//a[./*[contains(concat(' ', normalize-space(@class), ' '), ' icon ') " .
+                "and starts-with(@title,$edit) and contains(@title,$gradeitem)]]";
         $this->execute("behat_general::i_click_on", array($this->escape($linkxpath), "xpath_element"));
 
         // Mapping names to idnumbers.
@@ -193,11 +201,11 @@ class behat_grade extends behat_base {
             // This xpath looks for course, categories and items with the provided name.
             // Grrr, we can't equal in categoryitem and courseitem because there is a line jump...
             $inputxpath = "//input[@class='idnumber'][" .
-                "parent::li[@class='item'][text()='" . $gradeitem . "']" .
-                " | " .
-                "parent::li[@class='categoryitem' | @class='courseitem']" .
-                "/parent::ul/parent::li[starts-with(text(),'" . $gradeitem . "')]" .
-            "]";
+                    "parent::li[@class='item'][text()='" . $gradeitem . "']" .
+                    " | " .
+                    "parent::li[@class='categoryitem' | @class='courseitem']" .
+                    "/parent::ul/parent::li[starts-with(text(),'" . $gradeitem . "')]" .
+                    "]";
             $this->execute('behat_forms::i_set_the_field_with_xpath_to', array($inputxpath, $idnumber));
         }
 
@@ -221,9 +229,10 @@ class behat_grade extends behat_base {
 
         if ($this->running_javascript()) {
             $gradeitemliteral = behat_context_helper::escape($gradeitem);
-            $xpath = "//tr[contains(.,$gradeitemliteral)]//*[contains(@class,'moodle-actionmenu')]//a[contains(@class,'toggle-display')]";
+            $xpath = "//tr[contains(.,$gradeitemliteral)]//*[contains(@class,'moodle-actionmenu')]";
             if ($this->getSession()->getPage()->findAll('xpath', $xpath)) {
-                $this->execute("behat_general::i_click_on", array($this->escape($xpath), "xpath_element"));
+                $xpath = "//tr[contains(.,$gradeitemliteral)]";
+                $this->execute("behat_action_menu::i_open_the_action_menu_in", array($xpath, "xpath_element"));
             }
         }
 
@@ -290,11 +299,10 @@ class behat_grade extends behat_base {
      * @param string $gradepath
      */
     public function i_navigate_to_in_the_course_gradebook($gradepath) {
-        // If we are not on one of the gradebook pages already, follow "Grades" link in the navigation block.
+        // If we are not on one of the gradebook pages already, follow "Grades" link in the navigation drawer.
         $xpath = '//div[contains(@class,\'grade-navigation\')]';
         if (!$this->getSession()->getPage()->findAll('xpath', $xpath)) {
-            $this->execute("behat_general::i_click_on_in_the", array(get_string('grades'), 'link',
-                get_string('pluginname', 'block_navigation'), 'block'));
+            $this->execute('behat_navigation::i_select_from_flat_navigation_drawer', get_string('grades'));
         }
 
         $this->select_in_gradebook_tabs($gradepath);
index 234e6a4..8e4ac6a 100644 (file)
@@ -194,15 +194,11 @@ class core_grade_import_lib_test extends advanced_testcase {
             'itemid' => $gradeitem->id
         ));
 
-        $url = $CFG->wwwroot . '/grade/index.php';
-        $expectedresponse = "++ Grade import success ++
-<div class=\"continuebutton\"><form method=\"get\" action=\"$url\"><div><input type=\"submit\" value=\"Continue\" /><input type=\"hidden\" name=\"id\" value=\"$course->id\" /></div></form></div>";
-
         ob_start();
         $status = grade_import_commit($course->id, $importcode);
         $output = ob_get_contents();
         ob_end_clean();
         $this->assertTrue($status);
-        $this->assertEquals($expectedresponse, $output);
+        $this->assertContains("++ Grade import success ++", $output);
     }
 }
index 0cedfb0..257381f 100644 (file)
@@ -1103,6 +1103,7 @@ $string['sessioncookiedomain'] = 'Cookie domain';
 $string['sessioncookiepath'] = 'Cookie path';
 $string['sessionhandling'] = 'Session handling';
 $string['sessiontimeout'] = 'Timeout';
+$string['settingdependenton'] = 'This setting may be hidden, based on the value of <strong>{$a}</strong>';
 $string['settingfileuploads'] = 'File uploading is required for normal operation, please enable it in PHP configuration.';
 $string['settingmemorylimit'] = 'Insufficient memory detected, please set higher memory limit in PHP settings.';
 $string['settingsafemode'] = 'Moodle is not fully compatible with safe mode, please ask server administrator to turn it off. Running Moodle under safe mode is not supported, please expect various problems if you do so.';
index 8772235..edf5998 100644 (file)
@@ -1317,6 +1317,84 @@ class admin_externalpage implements part_of_admin_tree {
     }
 }
 
+/**
+ * Used to store details of the dependency between two settings elements.
+ *
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2017 Davo Smith, Synergy Learning
+ */
+class admin_settingdependency {
+    /** @var string the name of the setting to be shown/hidden */
+    public $settingname;
+    /** @var string the setting this is dependent on */
+    public $dependenton;
+    /** @var string the condition to show/hide the element */
+    public $condition;
+    /** @var string the value to compare against */
+    public $value;
+
+    /** @var string[] list of valid conditions */
+    private static $validconditions = ['checked', 'notchecked', 'noitemselected', 'eq', 'neq', 'in'];
+
+    /**
+     * admin_settingdependency constructor.
+     * @param string $settingname
+     * @param string $dependenton
+     * @param string $condition
+     * @param string $value
+     * @throws \coding_exception
+     */
+    public function __construct($settingname, $dependenton, $condition, $value) {
+        $this->settingname = $this->parse_name($settingname);
+        $this->dependenton = $this->parse_name($dependenton);
+        $this->condition = $condition;
+        $this->value = $value;
+
+        if (!in_array($this->condition, self::$validconditions)) {
+            throw new coding_exception("Invalid condition '$condition'");
+        }
+    }
+
+    /**
+     * Convert the setting name into the form field name.
+     * @param string $name
+     * @return string
+     */
+    private function parse_name($name) {
+        $bits = explode('/', $name);
+        $name = array_pop($bits);
+        $plugin = '';
+        if ($bits) {
+            $plugin = array_pop($bits);
+            if ($plugin === 'moodle') {
+                $plugin = '';
+            }
+        }
+        return 's_'.$plugin.'_'.$name;
+    }
+
+    /**
+     * Gather together all the dependencies in a format suitable for initialising javascript
+     * @param admin_settingdependency[] $dependencies
+     * @return array
+     */
+    public static function prepare_for_javascript($dependencies) {
+        $result = [];
+        foreach ($dependencies as $d) {
+            if (!isset($result[$d->dependenton])) {
+                $result[$d->dependenton] = [];
+            }
+            if (!isset($result[$d->dependenton][$d->condition])) {
+                $result[$d->dependenton][$d->condition] = [];
+            }
+            if (!isset($result[$d->dependenton][$d->condition][$d->value])) {
+                $result[$d->dependenton][$d->condition][$d->value] = [];
+            }
+            $result[$d->dependenton][$d->condition][$d->value][] = $d->settingname;
+        }
+        return $result;
+    }
+}
 
 /**
  * Used to group a number of admin_setting objects into a page and add them to the admin tree.
@@ -1334,6 +1412,9 @@ class admin_settingpage implements part_of_admin_tree {
     /** @var mixed An array of admin_setting objects that are part of this setting page. */
     public $settings;
 
+    /** @var admin_settingdependency[] list of settings to hide when certain conditions are met */
+    protected $dependencies = [];
+
     /** @var string The role capability/permission a user must have to access this external page. */
     public $req_capability;
 
@@ -1463,6 +1544,29 @@ class admin_settingpage implements part_of_admin_tree {
         return true;
     }
 
+    /**
+     * Hide the named setting if the specified condition is matched.
+     *
+     * @param string $settingname
+     * @param string $dependenton
+     * @param string $condition
+     * @param string $value
+     */
+    public function hide_if($settingname, $dependenton, $condition = 'notchecked', $value = '1') {
+        $this->dependencies[] = new admin_settingdependency($settingname, $dependenton, $condition, $value);
+
+        // Reformat the dependency name to the plugin | name format used in the display.
+        $dependenton = str_replace('/', ' | ', $dependenton);
+
+        // Let the setting know, so it can be displayed underneath.
+        $findname = str_replace('/', '', $settingname);
+        foreach ($this->settings as $name => $setting) {
+            if ($name === $findname) {
+                $setting->add_dependent_on($dependenton);
+            }
+        }
+    }
+
     /**
      * see admin_externalpage
      *
@@ -1521,6 +1625,25 @@ class admin_settingpage implements part_of_admin_tree {
         }
         return false;
     }
+
+    /**
+     * Should any of the settings on this page be shown / hidden based on conditions?
+     * @return bool
+     */
+    public function has_dependencies() {
+        return (bool)$this->dependencies;
+    }
+
+    /**
+     * Format the setting show/hide conditions ready to initialise the page javascript
+     * @return array
+     */
+    public function get_dependencies_for_javascript() {
+        if (!$this->has_dependencies()) {
+            return [];
+        }
+        return admin_settingdependency::prepare_for_javascript($this->dependencies);
+    }
 }
 
 
@@ -1551,6 +1674,8 @@ abstract class admin_setting {
     private $flags = array();
     /** @var bool Whether this field must be forced LTR. */
     private $forceltr = null;
+    /** @var array list of other settings that may cause this setting to be hidden */
+    private $dependenton = [];
 
     /**
      * Constructor
@@ -1920,6 +2045,22 @@ abstract class admin_setting {
     public function set_force_ltr($value) {
         $this->forceltr = $value;
     }
+
+    /**
+     * Add a setting to the list of those that could cause this one to be hidden
+     * @param string $dependenton
+     */
+    public function add_dependent_on($dependenton) {
+        $this->dependenton[] = $dependenton;
+    }
+
+    /**
+     * Get a list of the settings that could cause this one to be hidden.
+     * @return array
+     */
+    public function get_dependent_on() {
+        return $this->dependenton;
+    }
 }
 
 /**
@@ -8571,6 +8712,10 @@ function format_admin_setting($setting, $title='', $form='', $description='', $l
         $context->error = $adminroot->errors[$context->fullname]->error;
     }
 
+    if ($dependenton = $setting->get_dependent_on()) {
+        $context->dependenton = get_string('settingdependenton', 'admin', implode(', ', $dependenton));
+    }
+
     $context->id = 'admin-' . $setting->name;
     $context->title = highlightfast($query, $title);
     $context->name = highlightfast($query, $context->name);
index 0c5dc70..99ef99d 100644 (file)
Binary files a/lib/amd/build/paged_content_paging_bar.min.js and b/lib/amd/build/paged_content_paging_bar.min.js differ
diff --git a/lib/amd/build/showhidesettings.min.js b/lib/amd/build/showhidesettings.min.js
new file mode 100644 (file)
index 0000000..c3803c7
Binary files /dev/null and b/lib/amd/build/showhidesettings.min.js differ
index 456ee39..4b693b7 100644 (file)
@@ -550,6 +550,10 @@ define(
 
     return {
         init: init,
+        disableNextControlButtons: disableNextControlButtons,
+        enableNextControlButtons: enableNextControlButtons,
+        disablePreviousControlButtons: disablePreviousControlButtons,
+        enablePreviousControlButtons: enablePreviousControlButtons,
         showPage: showPage,
         rootSelector: SELECTORS.ROOT,
     };
diff --git a/lib/amd/src/showhidesettings.js b/lib/amd/src/showhidesettings.js
new file mode 100644 (file)
index 0000000..b260438
--- /dev/null
@@ -0,0 +1,345 @@
+/**
+ * Show/hide admin settings based on other settings selected
+ *
+ * @package core
+ * @copyright 2018 Davo Smith, Synergy Learning
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery'], function($) {
+    var dependencies;
+
+    // -------------------------------------------------
+    // Support functions, used by dependency functions.
+    // -------------------------------------------------
+
+    /**
+     * Check to see if the given element is the hidden element that makes sure checkbox
+     * elements always submit a value.
+     * @param {jQuery} $el
+     * @returns {boolean}
+     */
+    function isCheckboxHiddenElement($el) {
+        return ($el.is('input[type=hidden]') && $el.siblings('input[type=checkbox][name="' + $el.attr('name') + '"]').length);
+    }
+
+    /**
+     * Check to see if this is a radio button with the wrong value (i.e. a radio button from
+     * the group we are interested in, but not the specific one we wanted).
+     * @param {jQuery} $el
+     * @param {string} value
+     * @returns {boolean}
+     */
+    function isWrongRadioButton($el, value) {
+        return ($el.is('input[type=radio]') && $el.attr('value') !== value);
+    }
+
+    /**
+     * Is this element relevant when we're looking for checked / not checked status?
+     * @param {jQuery} $el
+     * @param {string} value
+     * @returns {boolean}
+     */
+    function isCheckedRelevant($el, value) {
+        return (!isCheckboxHiddenElement($el) && !isWrongRadioButton($el, value));
+    }
+
+    /**
+     * Is this an unchecked radio button? (If it is, we want to skip it, as
+     * we're only interested in the value of the radio button that is checked)
+     * @param {jQuery} $el
+     * @returns {boolean}
+     */
+    function isUncheckedRadioButton($el) {
+        return ($el.is('input[type=radio]') && !$el.prop('checked'));
+    }
+
+    /**
+     * Is this an unchecked checkbox?
+     * @param {jQuery} $el
+     * @returns {boolean}
+     */
+    function isUncheckedCheckbox($el) {
+        return ($el.is('input[type=checkbox]') && !$el.prop('checked'));
+    }
+
+    /**
+     * Is this a multi-select select element?
+     * @param {jQuery} $el
+     * @returns {boolean}
+     */
+    function isMultiSelect($el) {
+        return ($el.is('select') && $el.prop('multiple'));
+    }
+
+    /**
+     * Does the multi-select exactly match the list of values provided?
+     * @param {jQuery} $el
+     * @param {array} values
+     * @returns {boolean}
+     */
+    function multiSelectMatches($el, values) {
+        var selected = $el.val() || [];
+        if (!values.length) {
+            // No values - nothing to match against.
+            return false;
+        }
+        if (selected.length !== values.length) {
+            // Different number of expected and actual values - cannot possibly be a match.
+            return false;
+        }
+        for (var i in selected) {
+            if (selected.hasOwnProperty(i)) {
+                if (values.indexOf(selected[i]) === -1) {
+                    return false; // Found a non-matching value - give up immediately.
+                }
+            }
+        }
+        // Didn't find a non-matching value, so we have a match.
+        return true;
+    }
+
+    // -------------------------------
+    // Specific dependency functions.
+    // -------------------------------
+
+    var depFns = {
+        notchecked: function($dependon, value) {
+            var hide = false;
+            value = String(value);
+            $dependon.each(function(idx, el) {
+                var $el = $(el);
+                if (isCheckedRelevant($el, value)) {
+                    hide = hide || !$el.prop('checked');
+                }
+            });
+            return hide;
+        },
+
+        checked: function($dependon, value) {
+            var hide = false;
+            value = String(value);
+            $dependon.each(function(idx, el) {
+                var $el = $(el);
+                if (isCheckedRelevant($el, value)) {
+                    hide = hide || $el.prop('checked');
+                }
+            });
+            return hide;
+        },
+
+        noitemselected: function($dependon) {
+            var hide = false;
+            $dependon.each(function(idx, el) {
+                var $el = $(el);
+                hide = hide || ($el.prop('selectedIndex') === -1);
+            });
+            return hide;
+        },
+
+        eq: function($dependon, value) {
+            var hide = false;
+            var hiddenVal = false;
+            value = String(value);
+            $dependon.each(function(idx, el) {
+                var $el = $(el);
+                if (isUncheckedRadioButton($el)) {
+                    // For radio buttons, we're only interested in the one that is checked.
+                    return;
+                }
+                if (isCheckboxHiddenElement($el)) {
+                    // This is the hidden input that is part of the checkbox setting.
+                    // We will use this value, if the associated checkbox is unchecked.
+                    hiddenVal = ($el.val() === value);
+                    return;
+                }
+                if (isUncheckedCheckbox($el)) {
+                    // Checkbox is not checked - hide depends on the 'unchecked' value stored in
+                    // the associated hidden element, which we have already found, above.
+                    hide = hide || hiddenVal;
+                    return;
+                }
+                if (isMultiSelect($el)) {
+                    // Expect a list of values to match, separated by '|' - all of them must
+                    // match the values selected.
+                    var values = value.split('|');
+                    hide = multiSelectMatches($el, values);
+                    return;
+                }
+                // All other element types - just compare the value directly.
+                hide = hide || ($el.val() === value);
+            });
+            return hide;
+        },
+
+        'in': function($dependon, value) {
+            var hide = false;
+            var hiddenVal = false;
+            var values = value.split('|');
+            $dependon.each(function(idx, el) {
+                var $el = $(el);
+                if (isUncheckedRadioButton($el)) {
+                    // For radio buttons, we're only interested in the one that is checked.
+                    return;
+                }
+                if (isCheckboxHiddenElement($el)) {
+                    // This is the hidden input that is part of the checkbox setting.
+                    // We will use this value, if the associated checkbox is unchecked.
+                    hiddenVal = (values.indexOf($el.val()) > -1);
+                    return;
+                }
+                if (isUncheckedCheckbox($el)) {
+                    // Checkbox is not checked - hide depends on the 'unchecked' value stored in
+                    // the associated hidden element, which we have already found, above.
+                    hide = hide || hiddenVal;
+                    return;
+                }
+                if (isMultiSelect($el)) {
+                    // For multiselect, we check to see if the list of values provided matches the list selected.
+                    hide = multiSelectMatches($el, values);
+                    return;
+                }
+                // All other element types - check to see if the value is in the list.
+                hide = hide || (values.indexOf($el.val()) > -1);
+            });
+            return hide;
+        },
+
+        defaultCondition: function($dependon, value) { // Not equal.
+            var hide = false;
+            var hiddenVal = false;
+            value = String(value);
+            $dependon.each(function(idx, el) {
+                var $el = $(el);
+                if (isUncheckedRadioButton($el)) {
+                    // For radio buttons, we're only interested in the one that is checked.
+                    return;
+                }
+                if (isCheckboxHiddenElement($el)) {
+                    // This is the hidden input that is part of the checkbox setting.
+                    // We will use this value, if the associated checkbox is unchecked.
+                    hiddenVal = ($el.val() !== value);
+                    return;
+                }
+                if (isUncheckedCheckbox($el)) {
+                    // Checkbox is not checked - hide depends on the 'unchecked' value stored in
+                    // the associated hidden element, which we have already found, above.
+                    hide = hide || hiddenVal;
+                    return;
+                }
+                if (isMultiSelect($el)) {
+                    // Expect a list of values to match, separated by '|'