Merge branch 'MDL-64878-master' of git://github.com/damyon/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 27 Feb 2019 17:58:26 +0000 (18:58 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 27 Feb 2019 17:58:26 +0000 (18:58 +0100)
509 files changed:
.gitignore
admin/cli/install_database.php
admin/settings.php
admin/settings/subsystems.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/analytics/amd/build/model.min.js
admin/tool/analytics/amd/src/model.js
admin/tool/analytics/classes/output/form/edit_model.php
admin/tool/analytics/classes/output/form/import_model.php [new file with mode: 0644]
admin/tool/analytics/classes/output/helper.php
admin/tool/analytics/classes/output/invalid_analysables.php
admin/tool/analytics/classes/output/models_list.php
admin/tool/analytics/createmodel.php [new file with mode: 0644]
admin/tool/analytics/importmodel.php [new file with mode: 0644]
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/analytics/model.php
admin/tool/analytics/templates/invalid_analysables.mustache
admin/tool/analytics/templates/models_list.mustache
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/langimport/classes/locale.php [new file with mode: 0644]
admin/tool/langimport/index.php
admin/tool/langimport/lang/en/tool_langimport.php
admin/tool/langimport/tests/locale_test.php [new file with mode: 0644]
admin/tool/lp/classes/external.php
admin/tool/lp/classes/external/user_competency_summary_in_course_exporter.php
admin/tool/lp/classes/output/course_competencies_page.php
admin/tool/lp/templates/course_competencies_page.mustache
admin/tool/lp/templates/progress_bar.mustache
admin/tool/lp/templates/user_competency_summary_in_course.mustache
admin/tool/lp/tests/behat/plan_crud.feature
admin/tool/mobile/classes/api.php
admin/tool/mobile/db/upgrade.php [new file with mode: 0644]
admin/tool/mobile/version.php
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
admin/tool/xmldb/actions/add_persistent_mandatory/add_persistent_mandatory.class.php [new file with mode: 0644]
admin/tool/xmldb/actions/edit_table/edit_table.class.php
admin/tool/xmldb/lang/en/tool_xmldb.php
analytics/classes/calculable.php
analytics/classes/manager.php
analytics/classes/model.php
analytics/classes/model_config.php [new file with mode: 0644]
analytics/classes/packable.php [new file with mode: 0644]
analytics/tests/model_test.php
analytics/tests/prediction_test.php
blocks/admin_bookmarks/tests/behat/bookmark_admin_pages.feature
blocks/login/block_login.php
blocks/myoverview/amd/build/view.min.js
blocks/myoverview/amd/src/view.js
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/templates/nav-grouping-selector.mustache
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
cache/classes/loaders.php
cache/tests/cache_test.php
cache/tests/fixtures/lib.php
calendar/templates/minicalendar_day_link.mustache
competency/classes/api.php
competency/tests/api_test.php
completion/classes/progress.php
course/classes/management_renderer.php
course/modlib.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/locallib.php
enrol/renderer.php
enrol/self/tests/self_test.php
enrol/tests/course_enrolment_manager_test.php
enrol/upgrade.txt
enrol/yui/otherusersmanager/otherusersmanager.js
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]
install/lang/eu/admin.php
install/lang/eu/install.php
lang/en/admin.php
lang/en/analytics.php
lang/en/competency.php
lang/en/enrol.php
lang/en/message.php
lib/adminlib.php
lib/amd/build/checkbox-toggleall.min.js [new file with mode: 0644]
lib/amd/build/paged_content_paging_bar.min.js
lib/amd/build/showhidesettings.min.js [new file with mode: 0644]
lib/amd/src/checkbox-toggleall.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/behat/form_field/behat_form_field.php
lib/classes/component.php
lib/classes/message/manager.php
lib/db/messages.php
lib/db/upgrade.php
lib/editor/atto/plugins/media/tests/behat/media.feature
lib/form/amd/build/showadvanced.min.js [new file with mode: 0644]
lib/form/amd/src/showadvanced.js [new file with mode: 0644]
lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-debug.js [deleted file]
lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-min.js [deleted file]
lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced.js [deleted file]
lib/form/yui/src/showadvanced/build.json [deleted file]
lib/form/yui/src/showadvanced/js/showadvanced.js [deleted file]
lib/form/yui/src/showadvanced/meta/showadvanced.json [deleted file]
lib/formslib.php
lib/mlbackend/php/classes/processor.php
lib/mlbackend/python/classes/processor.php
lib/moodlelib.php
lib/navigationlib.php
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 97% 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
login/change_password_form.php
login/forgot_password_form.php
message/amd/build/message_area.min.js [deleted file]
message/amd/build/message_area_actions.min.js [deleted file]
message/amd/build/message_area_contacts.min.js [deleted file]
message/amd/build/message_area_events.min.js [deleted file]
message/amd/build/message_area_messages.min.js [deleted file]
message/amd/build/message_area_profile.min.js [deleted file]
message/amd/build/message_area_search.min.js [deleted file]
message/amd/build/message_area_tabs.min.js [deleted file]
message/amd/build/message_drawer.min.js
message/amd/build/message_drawer_router.min.js
message/amd/build/message_drawer_view_contact.min.js
message/amd/build/message_drawer_view_contacts.min.js
message/amd/build/message_drawer_view_contacts_section_contacts.min.js
message/amd/build/message_drawer_view_conversation.min.js
message/amd/build/message_drawer_view_group_info.min.js
message/amd/build/message_drawer_view_overview.min.js
message/amd/build/message_drawer_view_overview_section.min.js
message/amd/build/message_drawer_view_search.min.js
message/amd/build/message_drawer_view_settings.min.js
message/amd/build/message_preferences.min.js [deleted file]
message/amd/build/message_user_button.min.js [new file with mode: 0644]
message/amd/build/notification_processor_settings.min.js
message/amd/build/preferences_notifications_list_controller.min.js
message/amd/build/preferences_processor_form.min.js
message/amd/src/message_area.js [deleted file]
message/amd/src/message_area_actions.js [deleted file]
message/amd/src/message_area_contacts.js [deleted file]
message/amd/src/message_area_events.js [deleted file]
message/amd/src/message_area_messages.js [deleted file]
message/amd/src/message_area_profile.js [deleted file]
message/amd/src/message_area_search.js [deleted file]
message/amd/src/message_area_tabs.js [deleted file]
message/amd/src/message_drawer.js
message/amd/src/message_drawer_router.js
message/amd/src/message_drawer_view_contact.js
message/amd/src/message_drawer_view_contacts.js
message/amd/src/message_drawer_view_contacts_section_contacts.js
message/amd/src/message_drawer_view_conversation.js
message/amd/src/message_drawer_view_group_info.js
message/amd/src/message_drawer_view_overview.js
message/amd/src/message_drawer_view_overview_section.js
message/amd/src/message_drawer_view_search.js
message/amd/src/message_drawer_view_settings.js
message/amd/src/message_preferences.js [deleted file]
message/amd/src/message_user_button.js [new file with mode: 0644]
message/amd/src/notification_processor_settings.js
message/amd/src/preferences_notifications_list_controller.js
message/amd/src/preferences_processor_form.js
message/classes/api.php
message/classes/helper.php
message/classes/output/messagearea/contact.php
message/classes/output/messagearea/contacts.php
message/classes/output/messagearea/message.php
message/classes/output/messagearea/message_area.php
message/classes/output/messagearea/message_search_results.php [deleted file]
message/classes/output/messagearea/messages.php
message/classes/output/messagearea/profile.php
message/classes/output/messagearea/user_search_results.php
message/externallib.php
message/index.php
message/lib.php
message/templates/message_area.mustache [deleted file]
message/templates/message_area_contact.mustache [deleted file]
message/templates/message_area_contacts.mustache [deleted file]
message/templates/message_area_contacts_area.mustache [deleted file]
message/templates/message_area_message.mustache [deleted file]
message/templates/message_area_message_search_results.mustache [deleted file]
message/templates/message_area_messages_area.mustache [deleted file]
message/templates/message_area_profile.mustache [deleted file]
message/templates/message_area_response.mustache [deleted file]
message/templates/message_area_user_search_results.mustache [deleted file]
message/templates/message_drawer.mustache
message/templates/message_drawer_view_conversation_body_message.mustache
message/templates/message_drawer_view_conversation_footer_content.mustache
message/templates/message_drawer_view_overview_body.mustache
message/templates/message_drawer_view_overview_footer.mustache [new file with mode: 0644]
message/templates/message_drawer_view_overview_section.mustache
message/templates/message_index.mustache [new file with mode: 0644]
message/templates/message_preferences.mustache [deleted file]
message/templates/message_preferences_component.mustache [deleted file]
message/templates/message_preferences_notification_processor.mustache [deleted file]
message/templates/preferences_processor.mustache
message/tests/behat/message_admin_settings.feature [new file with mode: 0644]
message/tests/externallib_test.php
message/tests/privacy_provider_test.php
message/upgrade.txt
mod/assign/classes/event/base.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/externallib.php
mod/assign/lang/en/assign.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/submission/file/locallib.php
mod/assign/submission/onlinetext/locallib.php
mod/assign/templates/grading_actions.mustache
mod/assign/templates/grading_navigation.mustache
mod/assign/templates/grading_navigation_user_selector.mustache
mod/assign/tests/events_test.php
mod/assign/tests/externallib_test.php
mod/assign/version.php
mod/chat/lib.php
mod/chat/tests/lib_test.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/glossary/lib.php
mod/glossary/tests/lib_test.php
mod/glossary/view.php
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 [moved from message/classes/output/renderer.php with 54% similarity]
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/amd/build/qbankmanager.min.js [new file with mode: 0644]
question/amd/src/qbankmanager.js [new file with mode: 0644]
question/classes/bank/checkbox_column.php
question/classes/bank/view.php
question/renderer.php
question/tests/behat/select_questions.feature [new file with mode: 0644]
question/type/essay/backup/moodle2/restore_qtype_essay_plugin.class.php
question/type/essay/tests/restore_test.php [new file with mode: 0644]
question/type/gapselect/renderer.php
question/type/gapselect/tests/walkthrough_test.php
question/type/numerical/renderer.php
question/type/numerical/styles.css
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-debug.js [deleted file]
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-min.js [deleted file]
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager.js [deleted file]
question/yui/src/qbankmanager/build.json [deleted file]
question/yui/src/qbankmanager/js/qbankmanager.js [deleted file]
question/yui/src/qbankmanager/meta/qbankmanager.json [deleted file]
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/layout/columns2.php
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/forms.scss
theme/boost/scss/moodle/message.scss
theme/boost/style/moodle.css
theme/boost/templates/block_search_forums/search_form.mustache [deleted file]
theme/boost/templates/columns1.mustache
theme/boost/templates/columns2.mustache
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/core_form/element-password.mustache
theme/boost/templates/flat_navigation.mustache
theme/boost/templates/footer.mustache
theme/boost/templates/gradereport_singleview/button.mustache [deleted file]
theme/boost/templates/login.mustache
theme/boost/templates/maintenance.mustache
theme/boost/templates/mod_forum/quick_search_form.mustache [deleted file]
theme/boost/templates/navbar-secure.mustache
theme/boost/templates/secure.mustache
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/bs4-compat.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/forms.less
theme/bootstrapbase/less/moodle/message.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/core_message/message_drawer.mustache
theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_body_message.mustache
theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_footer_content.mustache
theme/bootstrapbase/templates/core_message/message_drawer_view_overview_section.mustache
theme/bootstrapbase/templates/core_message/message_index.mustache [new file with mode: 0644]
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 message/templates/message_area_messages.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
user/editadvanced_form.php
user/editlib.php
user/language_form.php
user/lib.php
user/tests/behat/behat_user.php
user/tests/behat/input-purpose.feature [new file with mode: 0644]
version.php

index 1dab0f7..b96359f 100644 (file)
@@ -9,10 +9,18 @@
 #
 # See gitignore(5) man page for more details
 #
+
+# Swap files (vim)
+[._]*.s[a-v][a-z]
+[._]*.sw[a-p]
+[._]s[a-rt-v][a-z]
+[._]ss[a-gi-z]
+[._]sw[a-p]
+# Temporary files including undo
+*~
+#
 /config.php
 /lib/editor/tinymce/extra/tools/temp/
-*~
-*.swp
 /tags
 /TAGS
 /cscope.*
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 a108b42..b559de3 100644 (file)
@@ -21,6 +21,12 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
         0)
     );
 
+    $optionalsubsystems->add(new admin_setting_configcheckbox('messagingdefaultpressenter',
+        new lang_string('messagingdefaultpressenter', 'admin'),
+        new lang_string('configmessagingdefaultpressenter', 'admin'),
+        1)
+    );
+
     $options = array(
         DAYSECS => new lang_string('secondstotime86400'),
         WEEKSECS => new lang_string('secondstotime604800'),
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 e9e2e3c..c66254a 100644 (file)
Binary files a/admin/tool/analytics/amd/build/model.min.js and b/admin/tool/analytics/amd/build/model.min.js differ
index 8913431..2f0b605 100644 (file)
@@ -36,6 +36,15 @@ define(['jquery', 'core/str', 'core/log', 'core/notification', 'core/modal_facto
                 component: 'tool_analytics'
             }
 
+        },
+        'delete': {
+            title: {
+                key: 'delete',
+                component: 'tool_analytics'
+            }, body: {
+                key: 'deletemodelconfirmation',
+                component: 'tool_analytics'
+            }
         }
     };
 
index 91fb876..2c0be86 100644 (file)
@@ -45,13 +45,25 @@ class edit_model extends \moodleform {
 
         $mform = $this->_form;
 
-        if ($this->_customdata['model']->is_trained()) {
+        if ($this->_customdata['trainedmodel']) {
             $message = get_string('edittrainedwarning', 'tool_analytics');
             $mform->addElement('html', $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING));
         }
 
         $mform->addElement('advcheckbox', 'enabled', get_string('enabled', 'tool_analytics'));
 
+        if (!empty($this->_customdata['targets'])) {
+            $targets = array('' => '');
+            foreach ($this->_customdata['targets'] as $classname => $target) {
+                $optionname = \tool_analytics\output\helper::class_to_option($classname);
+                $targets[$optionname] = $target->get_name();
+            }
+
+            $mform->addElement('select', 'target', get_string('target', 'tool_analytics'), $targets);
+            $mform->addHelpButton('target', 'target', 'tool_analytics');
+            $mform->addRule('target', get_string('required'), 'required', null, 'client');
+        }
+
         $indicators = array();
         foreach ($this->_customdata['indicators'] as $classname => $indicator) {
             $optionname = \tool_analytics\output\helper::class_to_option($classname);
@@ -88,11 +100,13 @@ class edit_model extends \moodleform {
             $predictionprocessors);
         $mform->addHelpButton('predictionsprocessor', 'predictionsprocessor', 'analytics');
 
-        $mform->addElement('hidden', 'id', $this->_customdata['id']);
-        $mform->setType('id', PARAM_INT);
+        if (!empty($this->_customdata['id'])) {
+            $mform->addElement('hidden', 'id', $this->_customdata['id']);
+            $mform->setType('id', PARAM_INT);
 
-        $mform->addElement('hidden', 'action', 'edit');
-        $mform->setType('action', PARAM_ALPHANUMEXT);
+            $mform->addElement('hidden', 'action', 'edit');
+            $mform->setType('action', PARAM_ALPHANUMEXT);
+        }
 
         $this->add_action_buttons();
     }
diff --git a/admin/tool/analytics/classes/output/form/import_model.php b/admin/tool/analytics/classes/output/form/import_model.php
new file mode 100644 (file)
index 0000000..1b4c375
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Model upload form.
+ *
+ * @package   tool_analytics
+ * @copyright 2017 onwards Ankit Agarwal
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_analytics\output\form;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Model upload form.
+ *
+ * @package   tool_analytics
+ * @copyright 2017 onwards Ankit Agarwal
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class import_model extends \moodleform {
+
+    /**
+     * Form definition.
+     *
+     * @return null
+     */
+    public function definition () {
+        $mform = $this->_form;
+
+        $mform->addElement('header', 'settingsheader', get_string('importmodel', 'tool_analytics'));
+
+        $mform->addElement('filepicker', 'modelfile', get_string('file'), null, ['accepted_types' => '.zip']);
+        $mform->addRule('modelfile', null, 'required');
+
+        $mform->addElement('advcheckbox', 'ignoreversionmismatches', get_string('ignoreversionmismatches', 'tool_analytics'),
+            get_string('ignoreversionmismatchescheckbox', 'tool_analytics'));
+
+        $this->add_action_buttons(true, get_string('import'));
+    }
+}
index 8d121e2..6543022 100644 (file)
@@ -58,4 +58,38 @@ class helper {
         // Really unlikely but yeah, I'm a bad booyyy.
         return str_replace('2015102400ouuu', '\\', $option);
     }
+
+    /**
+     * Sets an analytics > analytics models > $title breadcrumb.
+     *
+     * @param string $title
+     * @param \moodle_url $url
+     * @param \context|null $context Defaults to context_system
+     * @return null
+     */
+    public static function set_navbar(string $title, \moodle_url $url, ?\context $context = null) {
+        global $PAGE;
+
+        if (!$context) {
+            $context = \context_system::instance();
+        }
+
+        $PAGE->set_context($context);
+        $PAGE->set_url($url);
+
+        if ($siteadmin = $PAGE->settingsnav->find('root', \navigation_node::TYPE_SITE_ADMIN)) {
+            $PAGE->navbar->add($siteadmin->get_content(), $siteadmin->action());
+        }
+        if ($analytics = $PAGE->settingsnav->find('analytics', \navigation_node::TYPE_SETTING)) {
+            $PAGE->navbar->add($analytics->get_content(), $analytics->action());
+        }
+        if ($analyticmodels = $PAGE->settingsnav->find('analyticmodels', \navigation_node::TYPE_SETTING)) {
+            $PAGE->navbar->add($analyticmodels->get_content(), $analyticmodels->action());
+        }
+        $PAGE->navbar->add($title, $url);
+
+        $PAGE->set_pagelayout('report');
+        $PAGE->set_title($title);
+        $PAGE->set_heading($title);
+    }
 }
index a5bda09..0bb3902 100644 (file)
@@ -153,6 +153,12 @@ class invalid_analysables implements \renderable, \templatable {
             $data->analysables[] = $obj;
         }
 
+        if (empty($data->analysables)) {
+            $data->noanalysables = [
+                'message' => get_string('noinvalidanalysables', 'tool_analytics'),
+                'announce' => true,
+            ];
+        }
         return $data;
     }
 }
index cbd0a60..b3a1c43 100644 (file)
@@ -62,6 +62,8 @@ class models_list implements \renderable, \templatable {
         global $PAGE;
 
         $data = new \stdClass();
+        $data->importmodelurl = new \moodle_url('/admin/tool/analytics/importmodel.php');
+        $data->createmodelurl = new \moodle_url('/admin/tool/analytics/createmodel.php');
 
         $onlycli = get_config('analytics', 'onlycli');
         if ($onlycli === false) {
@@ -229,10 +231,19 @@ class models_list implements \renderable, \templatable {
 
             // Export training data.
             if (!$model->is_static() && $model->is_trained()) {
-                $urlparams['action'] = 'export';
+                $urlparams['action'] = 'exportdata';
                 $url = new \moodle_url('model.php', $urlparams);
                 $icon = new \action_menu_link_secondary($url, new \pix_icon('i/export',
-                    get_string('exporttrainingdata', 'tool_analytics')), get_string('export', 'tool_analytics'));
+                    get_string('exporttrainingdata', 'tool_analytics')), get_string('exporttrainingdata', 'tool_analytics'));
+                $actionsmenu->add($icon);
+            }
+
+            // Export model.
+            if (!$model->is_static() && $model->get_indicators() && !empty($modeldata->timesplitting)) {
+                $urlparams['action'] = 'exportmodel';
+                $url = new \moodle_url('model.php', $urlparams);
+                $icon = new \action_menu_link_secondary($url, new \pix_icon('i/backup',
+                    get_string('exportmodel', 'tool_analytics')), get_string('exportmodel', 'tool_analytics'));
                 $actionsmenu->add($icon);
             }
 
@@ -247,7 +258,7 @@ class models_list implements \renderable, \templatable {
             }
 
             // Clear model.
-            if (!empty($predictioncontexts)) {
+            if (!empty($predictioncontexts) || $model->is_trained()) {
                 $actionid = 'clear-' . $model->get_id();
                 $PAGE->requires->js_call_amd('tool_analytics/model', 'confirmAction', [$actionid, 'clear']);
                 $urlparams['action'] = 'clear';
@@ -258,6 +269,15 @@ class models_list implements \renderable, \templatable {
                 $actionsmenu->add($icon);
             }
 
+            $actionid = 'delete-' . $model->get_id();
+            $PAGE->requires->js_call_amd('tool_analytics/model', 'confirmAction', [$actionid, 'delete']);
+            $urlparams['action'] = 'delete';
+            $url = new \moodle_url('model.php', $urlparams);
+            $icon = new \action_menu_link_secondary($url, new \pix_icon('t/delete',
+                get_string('delete', 'tool_analytics')), get_string('delete', 'tool_analytics'),
+                ['data-action-id' => $actionid]);
+            $actionsmenu->add($icon);
+
             $modeldata->actions = $actionsmenu->export_for_template($output);
 
             $data->models[] = $modeldata;
diff --git a/admin/tool/analytics/createmodel.php b/admin/tool/analytics/createmodel.php
new file mode 100644 (file)
index 0000000..bf1701c
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Create model form.
+ *
+ * @package    tool_analytics
+ * @copyright  2019 David Monllao {@link http://www.davidmonllao.com}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../config.php');
+
+require_login();
+\core_analytics\manager::check_can_manage_models();
+
+$returnurl = new \moodle_url('/admin/tool/analytics/index.php');
+$url = new \moodle_url('/admin/tool/analytics/createmodel.php');
+$title = get_string('createmodel', 'tool_analytics');
+
+\tool_analytics\output\helper::set_navbar($title, $url);
+
+// Static targets are not editable, we discard them.
+$targets = array_filter(\core_analytics\manager::get_all_targets(), function($target) {
+    return (!$target->based_on_assumptions());
+});
+
+$customdata = array(
+    'trainedmodel' => false,
+    'targets' => $targets,
+    'indicators' => \core_analytics\manager::get_all_indicators(),
+    'timesplittings' => \core_analytics\manager::get_enabled_time_splitting_methods(),
+    'predictionprocessors' => \core_analytics\manager::get_all_prediction_processors(),
+);
+$mform = new \tool_analytics\output\form\edit_model(null, $customdata);
+
+if ($mform->is_cancelled()) {
+    redirect($returnurl);
+
+} else if ($data = $mform->get_data()) {
+
+    // Converting option names to class names.
+    $targetclass = \tool_analytics\output\helper::option_to_class($data->target);
+    if (empty($targets[$targetclass])) {
+        throw new \moodle_exception('errorinvalidtarget', 'analytics', '', $targetclass);
+    }
+    $target = $targets[$targetclass];
+
+    $indicators = array();
+    foreach ($data->indicators as $indicator) {
+        $indicatorinstance = \core_analytics\manager::get_indicator(
+            \tool_analytics\output\helper::option_to_class($indicator)
+        );
+        $indicators[$indicatorinstance->get_id()] = $indicatorinstance;
+    }
+    $timesplitting = \tool_analytics\output\helper::option_to_class($data->timesplitting);
+    $predictionsprocessor = \tool_analytics\output\helper::option_to_class($data->predictionsprocessor);
+
+    // Insert the model into db.
+    $model = \core_analytics\model::create($target, []);
+
+    // Filter out indicators that can not be used by this target.
+    $invalidindicators = array_diff_key($indicators, $model->get_potential_indicators());
+    if ($invalidindicators) {
+        $indicators = array_diff_key($indicators, $invalidindicators);
+    }
+
+    // Update the model with the valid list of indicators.
+    $model->update($data->enabled, $indicators, $timesplitting, $predictionsprocessor);
+
+    $message = '';
+    $messagetype = \core\output\notification::NOTIFY_SUCCESS;
+    if (!empty($invalidindicators)) {
+        $message = get_string('invalidindicatorsremoved', 'tool_analytics');
+    }
+    redirect($returnurl, $message, 0, $messagetype);
+}
+
+echo $OUTPUT->header();
+$mform->display();
+echo $OUTPUT->footer();
diff --git a/admin/tool/analytics/importmodel.php b/admin/tool/analytics/importmodel.php
new file mode 100644 (file)
index 0000000..eedeaa0
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Import models tool frontend.
+ *
+ * @package tool_analytics
+ * @copyright 2017 onwards Ankit Agarwal
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../config.php');
+
+require_login();
+\core_analytics\manager::check_can_manage_models();
+
+$returnurl = new \moodle_url('/admin/tool/analytics/index.php');
+$url = new \moodle_url('/admin/tool/analytics/importmodel.php');
+$title = get_string('importmodel', 'tool_analytics');
+
+\tool_analytics\output\helper::set_navbar($title, $url);
+
+$form = new \tool_analytics\output\form\import_model();
+if ($form->is_cancelled()) {
+    redirect($returnurl);
+} else if ($data = $form->get_data()) {
+
+    $modelconfig = new \core_analytics\model_config();
+
+    $zipfilepath = $form->save_temp_file('modelfile');
+
+    list ($modeldata, $unused) = $modelconfig->extract_import_contents($zipfilepath);
+
+    if ($error = $modelconfig->check_dependencies($modeldata, $data->ignoreversionmismatches)) {
+        // The file is not available until the form is validated so we need an alternative method to show errors.
+        redirect($url, $error, 0, \core\output\notification::NOTIFY_ERROR);
+    }
+    \core_analytics\model::import_model($zipfilepath);
+
+    redirect($returnurl, get_string('importedsuccessfully', 'tool_analytics'), 0,
+        \core\output\notification::NOTIFY_SUCCESS);
+}
+
+echo $OUTPUT->header();
+$form->display();
+echo $OUTPUT->footer();
\ No newline at end of file
index 8df2cc3..580d7b8 100644 (file)
@@ -29,11 +29,15 @@ $string['analyticmodels'] = 'Analytics models';
 $string['bettercli'] = 'Evaluating models and generating predictions may involve heavy processing. It is recommended to run these actions from the command line.';
 $string['cantguessstartdate'] = 'Can\'t guess the start date';
 $string['cantguessenddate'] = 'Can\'t guess the end date';
+$string['classdoesnotexist'] = 'Class {$a} does not exist';
 $string['clearpredictions'] = 'Clear predictions';
 $string['clearmodelpredictions'] = 'Are you sure you want to clear all "{$a}" predictions?';
 $string['clienablemodel'] = 'You can enable the model by selecting a time-splitting method by its ID. Note that you can also enable it later using the web interface (\'none\' to exit).';
 $string['clievaluationandpredictions'] = 'A scheduled task iterates through enabled models and gets predictions. Models evaluation via the web interface is disabled. You can allow these processes to be executed manually via the web interface by disabling the <a href="{$a}">\'onlycli\'</a> analytics setting.';
 $string['clievaluationandpredictionsnoadmin'] = 'A scheduled task iterates through enabled models and gets predictions. Models evaluation via the web interface is disabled. It may be enabled by a site administrator.';
+$string['createmodel'] = 'Create model';
+$string['delete'] = 'Delete';
+$string['deletemodelconfirmation'] = 'Are you sure you want to delete "{$a}"? These changes can not be reverted.';
 $string['disabled'] = 'Disabled';
 $string['editmodel'] = 'Edit "{$a}" model';
 $string['edittrainedwarning'] = 'This model has already been trained. Note that changing its indicators or its time-splitting method will delete its previous predictions and start generating new predictions.';
@@ -50,7 +54,7 @@ $string['errortrainingdataexport'] = 'The model training data could not be expor
 $string['evaluate'] = 'Evaluate';
 $string['evaluatemodel'] = 'Evaluate model';
 $string['evaluationinbatches'] = 'The site contents are calculated and stored in batches. The evaluation process may be stopped at any time. The next time it is run, it will continue from the point when it was stopped.';
-$string['export'] = 'Export';
+$string['exportmodel'] = 'Export configuration';
 $string['exporttrainingdata'] = 'Export training data';
 $string['getpredictionsresultscli'] = 'Results using {$a->name} (id: {$a->id}) course duration splitting';
 $string['getpredictionsresults'] = 'Results using {$a->name} course duration splitting';
@@ -58,15 +62,21 @@ $string['extrainfo'] = 'Info';
 $string['generalerror'] = 'Evaluation error. Status code {$a}';
 $string['getpredictions'] = 'Get predictions';
 $string['goodmodel'] = 'This is a good model for using to obtain predictions. Enable it to start obtaining predictions.';
+$string['importmodel'] = 'Import model';
 $string['indicators'] = 'Indicators';
 $string['info'] = 'Info';
+$string['ignoreversionmismatches'] = 'Ignore version mismatches';
+$string['ignoreversionmismatchescheckbox'] = 'Ignore the differences between this site version and the original site version.';
+$string['importedsuccessfully'] = 'The model has been successfully imported.';
 $string['insights'] = 'Insights';
 $string['invalidanalysables'] = 'Invalid site elements';
 $string['invalidanalysablesinfo'] = 'This pages lists this site analysable elements that can not be used by this prediction model. The listed elements can not be used neither to train the prediction model nor the prediction model can get predictions for them.';
 $string['invalidanalysablestable'] = 'Invalid site analysable elements table';
+$string['invalidindicatorsremoved'] = 'A new model has been added. Indicators that do not work with the selected target have been automatically removed.';
 $string['invalidprediction'] = 'Invalid to get predictions';
 $string['invalidtraining'] = 'Invalid to train the model';
 $string['loginfo'] = 'Log extra info';
+$string['missingmoodleversion'] = 'Imported file does not define a moodle version number';
 $string['modelid'] = 'Model ID';
 $string['modelinvalidanalysables'] = 'Invalid analysable elements for "{$a}" model';
 $string['modelresults'] = '{$a} results';
@@ -75,6 +85,7 @@ $string['nextpage'] = 'Next page';
 $string['nodatatoevaluate'] = 'There is no data to evaluate the model';
 $string['nodatatopredict'] = 'No new elements to get predictions for';
 $string['nodatatotrain'] = 'There is no new data that can be used for training';
+$string['noinvalidanalysables'] = 'This site does not contain any invalid analysable element.';
 $string['notdefined'] = 'Not yet defined';
 $string['pluginname'] = 'Analytic models';
 $string['predictionresults'] = 'Prediction results';
@@ -85,12 +96,14 @@ $string['previouspage'] = 'Previous page';
 $string['samestartdate'] = 'Current start date is good';
 $string['sameenddate'] = 'Current end date is good';
 $string['target'] = 'Target';
+$string['target_help'] = 'The target is what the model will predict.';
 $string['timesplittingnotdefined'] = 'Time splitting is not defined.';
 $string['timesplittingnotdefined_help'] = 'You need to select a time-splitting method before enabling the model.';
 $string['trainandpredictmodel'] = 'Training model and calculating predictions';
 $string['trainingprocessfinished'] = 'Training process finished';
 $string['trainingresults'] = 'Training results';
 $string['trainmodels'] = 'Train models';
+$string['versionnotsame'] = 'Imported file was from a different moodle version ({$a->importedversion}) than the current one ({$a->version})';
 $string['viewlog'] = 'Log';
 $string['weeksenddateautomaticallyset'] = 'End date automatically set based on start date and the number of sections';
 $string['weeksenddatedefault'] = 'End date automatically calculated from the course start date.';
index b70fc24..23d8454 100644 (file)
  */
 
 require_once(__DIR__ . '/../../../config.php');
+require_once($CFG->libdir . '/filelib.php');
 
 $id = required_param('id', PARAM_INT);
 $action = required_param('action', PARAM_ALPHANUMEXT);
 
-$context = context_system::instance();
-
 require_login();
 
 $model = new \core_analytics\model($id);
 \core_analytics\manager::check_can_manage_models();
 
+$returnurl = new \moodle_url('/admin/tool/analytics/index.php');
 $params = array('id' => $id, 'action' => $action);
 $url = new \moodle_url('/admin/tool/analytics/model.php', $params);
 
@@ -57,8 +57,14 @@ switch ($action) {
     case 'disable':
         $title = get_string('disable');
         break;
-    case 'export':
-        $title = get_string('export', 'tool_analytics');
+    case 'delete':
+        $title = get_string('delete');
+        break;
+    case 'exportdata':
+        $title = get_string('exporttrainingdata', 'tool_analytics');
+        break;
+    case 'exportmodel':
+        $title = get_string('exportmodel', 'tool_analytics');
         break;
     case 'clear':
         $title = get_string('clearpredictions', 'tool_analytics');
@@ -70,11 +76,7 @@ switch ($action) {
         throw new moodle_exception('errorunknownaction', 'analytics');
 }
 
-$PAGE->set_context($context);
-$PAGE->set_url($url);
-$PAGE->set_pagelayout('report');
-$PAGE->set_title($title);
-$PAGE->set_heading($title);
+\tool_analytics\output\helper::set_navbar($title, $url);
 
 $onlycli = get_config('analytics', 'onlycli');
 if ($onlycli === false) {
@@ -88,14 +90,21 @@ switch ($action) {
         confirm_sesskey();
 
         $model->enable();
-        redirect(new \moodle_url('/admin/tool/analytics/index.php'));
+        redirect($returnurl);
         break;
 
     case 'disable':
         confirm_sesskey();
 
         $model->update(0, false, false);
-        redirect(new \moodle_url('/admin/tool/analytics/index.php'));
+        redirect($returnurl);
+        break;
+
+    case 'delete':
+        confirm_sesskey();
+
+        $model->delete();
+        redirect($returnurl);
         break;
 
     case 'edit':
@@ -108,7 +117,7 @@ switch ($action) {
 
         $customdata = array(
             'id' => $model->get_id(),
-            'model' => $model,
+            'trainedmodel' => $model->is_trained(),
             'indicators' => $model->get_potential_indicators(),
             'timesplittings' => \core_analytics\manager::get_enabled_time_splitting_methods(),
             'predictionprocessors' => \core_analytics\manager::get_all_prediction_processors()
@@ -116,7 +125,7 @@ switch ($action) {
         $mform = new \tool_analytics\output\form\edit_model(null, $customdata);
 
         if ($mform->is_cancelled()) {
-            redirect(new \moodle_url('/admin/tool/analytics/index.php'));
+            redirect($returnurl);
 
         } else if ($data = $mform->get_data()) {
 
@@ -129,7 +138,7 @@ switch ($action) {
             $timesplitting = \tool_analytics\output\helper::option_to_class($data->timesplitting);
             $predictionsprocessor = \tool_analytics\output\helper::option_to_class($data->predictionsprocessor);
             $model->update($data->enabled, $indicators, $timesplitting, $predictionsprocessor);
-            redirect(new \moodle_url('/admin/tool/analytics/index.php'));
+            redirect($returnurl);
         }
 
         echo $OUTPUT->header();
@@ -203,7 +212,7 @@ switch ($action) {
         echo $renderer->render_table($modellogstable);
         break;
 
-    case 'export':
+    case 'exportdata':
 
         if ($model->is_static() || !$model->is_trained()) {
             throw new moodle_exception('errornoexport', 'tool_analytics');
@@ -211,7 +220,7 @@ switch ($action) {
 
         $file = $model->get_training_data();
         if (!$file) {
-            redirect(new \moodle_url('/admin/tool/analytics/index.php'), get_string('errortrainingdataexport', 'tool_analytics'),
+            redirect($returnurl, get_string('errortrainingdataexport', 'tool_analytics'),
                 null, \core\output\notification::NOTIFY_ERROR);
         }
 
@@ -219,11 +228,17 @@ switch ($action) {
         send_file($file, $filename, null, 0, false, true);
         break;
 
+    case 'exportmodel':
+        $zipfilename = 'model-' . $model->get_unique_id() . '-' . microtime(false) . '.zip';
+        $zipfilepath = $model->export_model($zipfilename);
+        send_temp_file($zipfilepath, $zipfilename);
+        break;
+
     case 'clear':
         confirm_sesskey();
 
         $model->clear();
-        redirect(new \moodle_url('/admin/tool/analytics/index.php'));
+        redirect($returnurl);
         break;
 
     case 'invalidanalysables':
index c97dd6b..2b3ddd6 100644 (file)
 <div class="box">
     <h3>{{#str}}modelinvalidanalysables, tool_analytics, {{modelname}}{{/str}}</h3>
     <div>{{#str}}invalidanalysablesinfo, tool_analytics{{/str}}</div>
-    <div class="m-t-2 m-b-1">
-        <span>{{#prev}}{{> core/single_button}}{{/prev}}</span>
-        <span>{{#next}}{{> core/single_button}}{{/next}}</span>
-    </div>
-    <table class="generaltable fullwidth">
-        <caption class="accesshide">{{#str}}invalidanalysablestable, tool_analytics{{/str}}</caption>
-        <thead>
-            <tr>
-                <th scope="col">{{#str}}name{{/str}}</th>
-                <th scope="col">{{#str}}invalidtraining, tool_analytics{{/str}}</th>
-                <th scope="col">{{#str}}invalidprediction, tool_analytics{{/str}}</th>
-            </tr>
-        </thead>
-        <tbody>
-        {{#analysables}}
-            <tr>
-                <td>{{{url}}}</td>
-                <td>{{validtraining}}</td>
-                <td>{{validprediction}}</td>
-            </tr>
-        {{/analysables}}
-        </tbody>
-    </table>
-    <div class="m-t-1 m-b-2">
-        <span>{{#prev}}{{> core/single_button}}{{/prev}}</span>
-        <span>{{#next}}{{> core/single_button}}{{/next}}</span>
-    </div>
+    {{#noanalysables}}
+        <div class="m-t-2 m-b-1">
+            {{> core/notification_info}}
+        </div>
+    {{/noanalysables}}
+    {{^noanalysables}}
+        <div class="m-t-2 m-b-1">
+            <span>{{#prev}}{{> core/single_button}}{{/prev}}</span>
+            <span>{{#next}}{{> core/single_button}}{{/next}}</span>
+        </div>
+        <table class="generaltable fullwidth">
+            <caption class="accesshide">{{#str}}invalidanalysablestable, tool_analytics{{/str}}</caption>
+            <thead>
+                <tr>
+                    <th scope="col">{{#str}}name{{/str}}</th>
+                    <th scope="col">{{#str}}invalidtraining, tool_analytics{{/str}}</th>
+                    <th scope="col">{{#str}}invalidprediction, tool_analytics{{/str}}</th>
+                </tr>
+            </thead>
+            <tbody>
+            {{#analysables}}
+                <tr>
+                    <td>{{{url}}}</td>
+                    <td>{{validtraining}}</td>
+                    <td>{{validprediction}}</td>
+                </tr>
+            {{/analysables}}
+            </tbody>
+        </table>
+        <div class="m-t-1 m-b-2">
+            <span>{{#prev}}{{> core/single_button}}{{/prev}}</span>
+            <span>{{#next}}{{> core/single_button}}{{/next}}</span>
+        </div>
+    {{/noanalysables}}
 </div>
index efdc4ca..bd11e0b 100644 (file)
 {{/infos}}
 
 <div class="box">
+    <div class="top-nav d-flex">
+        <a href="{{createmodelurl}}" class="btn btn-default mr-2">{{#str}}createmodel, tool_analytics{{/str}}</a>
+        <a href="{{importmodelurl}}" class="btn btn-default">{{#str}}importmodel, tool_analytics{{/str}}</a>
+    </div>
     <table class="generaltable fullwidth">
         <caption>{{#str}}analyticmodels, tool_analytics{{/str}}</caption>
         <thead>
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
diff --git a/admin/tool/langimport/classes/locale.php b/admin/tool/langimport/classes/locale.php
new file mode 100644 (file)
index 0000000..69c43b6
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Helper class for the language import tool.
+ *
+ * @package    tool_langimport
+ * @copyright  2018 Université Rennes 2 {@link https://www.univ-rennes2.fr}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_langimport;
+
+use coding_exception;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Helper class for the language import tool.
+ *
+ * @copyright  2018 Université Rennes 2 {@link https://www.univ-rennes2.fr}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class locale {
+    /**
+     * Checks availability of locale on current operating system.
+     *
+     * @param string $langpackcode E.g.: en, es, fr, de.
+     * @return bool TRUE if the locale is available on OS.
+     * @throws coding_exception when $langpackcode parameter is a non-empty string.
+     */
+    public function check_locale_availability(string $langpackcode) : bool {
+        global $CFG;
+
+        if (empty($langpackcode)) {
+            throw new coding_exception('Invalid language pack code in \\'.__METHOD__.'() call, only non-empty string is allowed');
+        }
+
+        // Fetch the correct locale based on ostype.
+        if ($CFG->ostype === 'WINDOWS') {
+            $stringtofetch = 'localewin';
+        } else {
+            $stringtofetch = 'locale';
+        }
+
+        // Store current locale.
+        $currentlocale = $this->set_locale(LC_ALL, 0);
+
+        $locale = get_string_manager()->get_string($stringtofetch, 'langconfig', $a = null, $langpackcode);
+
+        // Try to set new locale.
+        $return = $this->set_locale(LC_ALL, $locale);
+
+        // Restore current locale.
+        $this->set_locale(LC_ALL, $currentlocale);
+
+        // If $return is not equal to false, it means that setlocale() succeed to change locale.
+        return $return !== false;
+    }
+
+    /**
+     * Wrap for the native PHP function setlocale().
+     *
+     * @param int $category Specifying the category of the functions affected by the locale setting.
+     * @param string $locale E.g.: en_AU.utf8, en_GB.utf8, es_ES.utf8, fr_FR.utf8, de_DE.utf8.
+     * @return string|false Returns the new current locale, or FALSE on error.
+     */
+    protected function set_locale(int $category = LC_ALL, string $locale = '0') {
+        return setlocale($category, $locale);
+    }
+}
index 92a09da..1180abc 100644 (file)
@@ -109,9 +109,16 @@ echo $OUTPUT->header();
 echo $OUTPUT->heading(get_string('langimport', 'tool_langimport'));
 
 $installedlangs = get_string_manager()->get_list_of_translations(true);
+$locale = new \tool_langimport\locale();
 
+$missinglocales = '';
 $missingparents = array();
-foreach ($installedlangs as $installedlang => $unused) {
+foreach ($installedlangs as $installedlang => $langpackname) {
+    // Check locale availability.
+    if (!$locale->check_locale_availability($installedlang)) {
+        $missinglocales .= '<li>'.$langpackname.'</li>';
+    }
+
     $parent = get_parent_language($installedlang);
     if (empty($parent)) {
         continue;
@@ -121,6 +128,14 @@ foreach ($installedlangs as $installedlang => $unused) {
     }
 }
 
+if (!empty($missinglocales)) {
+    // There is at least one missing locale.
+    $a = new stdClass();
+    $a->globallocale = moodle_getlocale();
+    $a->missinglocales = $missinglocales;
+    $controller->errors[] = get_string('langunsupported', 'tool_langimport', $a);
+}
+
 if ($availablelangs = $controller->availablelangs) {
     $remote = true;
 } else {
index faeb02c..27739ed 100644 (file)
@@ -37,6 +37,7 @@ $string['langpackupdateskipped'] = 'Update of \'{$a}\' language pack skipped';
 $string['langpackuptodate'] = 'Language pack \'{$a}\' is up-to-date';
 $string['langpackupdated'] = 'Language pack \'{$a}\' was successfully updated';
 $string['langpackupdatedevent'] = 'Language pack updated';
+$string['langunsupported'] = '<p>Your server does not seem to fully support the following languages:</p><ul>{$a->missinglocales}</ul><p>Instead, the global locale ({$a->globallocale}) will be used to format certain strings such as dates or numbers.</p>';
 $string['langupdatecomplete'] = 'Language pack update completed';
 $string['missingcfglangotherroot'] = 'Missing configuration value $CFG->langotherroot';
 $string['missinglangparent'] = 'Missing parent language <em>{$a->parent}</em> of <em>{$a->lang}</em>.';
diff --git a/admin/tool/langimport/tests/locale_test.php b/admin/tool/langimport/tests/locale_test.php
new file mode 100644 (file)
index 0000000..4d1ffef
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Tests for \tool_langimport\locale class.
+ *
+ * @package    tool_langimport
+ * @copyright  2018 Université Rennes 2 {@link https://www.univ-rennes2.fr}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tests for \tool_langimport\locale class.
+ *
+ * @copyright  2018 Université Rennes 2 {@link https://www.univ-rennes2.fr}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class locale_testcase extends \advanced_testcase {
+    /**
+     * Test that \tool_langimport\locale::check_locale_availability() works as expected.
+     *
+     * @return void
+     */
+    public function test_check_locale_availability() {
+        // Create a mock of set_locale() method to simulate :
+        // - first setlocale() call which backup current locale
+        // - second setlocale() call which try to set new 'es' locale
+        // - third setlocale() call which restore locale.
+        $mock = $this->getMockBuilder(\tool_langimport\locale::class)
+            ->setMethods(['set_locale'])
+            ->getMock();
+        $mock->method('set_locale')->will($this->onConsecutiveCalls('en', 'es', 'en'));
+
+        // Test what happen when locale is available on system.
+        $result = $mock->check_locale_availability('en');
+        $this->assertTrue($result);
+
+        // Create a mock of set_locale() method to simulate :
+        // - first setlocale() call which backup current locale
+        // - second setlocale() call which fail to set new locale
+        // - third setlocale() call which restore locale.
+        $mock = $this->getMockBuilder(\tool_langimport\locale::class)
+            ->setMethods(['set_locale'])
+            ->getMock();
+        $mock->method('set_locale')->will($this->onConsecutiveCalls('en', false, 'en'));
+
+        // Test what happen when locale is not available on system.
+        $result = $mock->check_locale_availability('en');
+        $this->assertFalse($result);
+
+        // Test an invalid parameter.
+        $locale = new \tool_langimport\locale();
+        $this->expectException(coding_exception::class);
+        $locale->check_locale_availability('');
+    }
+}
index 77cf893..f798e7d 100644 (file)
@@ -420,6 +420,9 @@ class external extends external_api {
                     ))
                 ),
                 'comppath' => competency_path_exporter::get_read_structure(),
+                'plans' => new external_multiple_structure(
+                    plan_exporter::get_read_structure()
+                ),
             ))),
             'manageurl' => new external_value(PARAM_LOCALURL, 'Url to the manage competencies page.'),
         ));
index d1b7b7f..78ea24a 100644 (file)
@@ -26,11 +26,13 @@ defined('MOODLE_INTERNAL') || die();
 
 use core_competency\api;
 use core_competency\user_competency;
+use core_competency\external\plan_exporter;
 use core_course\external\course_module_summary_exporter;
 use core_course\external\course_summary_exporter;
 use context_course;
 use renderer_base;
 use stdClass;
+use moodle_url;
 
 /**
  * Class for exporting user competency data with additional related data in a plan.
@@ -62,7 +64,14 @@ class user_competency_summary_in_course_exporter extends \core\external\exporter
             'coursemodules' => array(
                 'type' => course_module_summary_exporter::read_properties_definition(),
                 'multiple' => true
-            )
+            ),
+            'plans' => array(
+                'type' => plan_exporter::read_properties_definition(),
+                'multiple' => true
+            ),
+            'pluginbaseurl' => [
+                'type' => PARAM_URL
+            ],
         );
     }
 
@@ -95,6 +104,16 @@ class user_competency_summary_in_course_exporter extends \core\external\exporter
         }
         $result->coursemodules = $exportedmodules;
 
+        // User learning plans.
+        $plans = api::list_plans_with_competency($this->related['user']->id, $this->related['competency']);
+        $exportedplans = array();
+        foreach ($plans as $plan) {
+            $planexporter = new plan_exporter($plan, array('template' => $plan->get_template()));
+            $exportedplans[] = $planexporter->export($output);
+        }
+        $result->plans = $exportedplans;
+        $result->pluginbaseurl = (new moodle_url('/admin/tool/lp'))->out(true);
+
         return (array) $result;
     }
 }
index eaf7d34..218d8a5 100644 (file)
@@ -41,6 +41,7 @@ use core_competency\external\course_competency_exporter;
 use core_competency\external\course_competency_settings_exporter;
 use core_competency\external\user_competency_course_exporter;
 use core_competency\external\user_competency_exporter;
+use core_competency\external\plan_exporter;
 use tool_lp\external\competency_path_exporter;
 use tool_lp\external\course_competency_statistics_exporter;
 use core_course\external\course_module_summary_exporter;
@@ -113,6 +114,7 @@ class course_competencies_page implements renderable, templatable {
         $data->courseid = $this->courseid;
         $data->pagecontextid = $this->context->id;
         $data->competencies = array();
+        $data->pluginbaseurl = (new moodle_url('/admin/tool/lp'))->out(true);
 
         $gradable = is_enrolled($this->context, $USER, 'moodle/competency:coursecompetencygradable');
         if ($gradable) {
@@ -154,12 +156,21 @@ class course_competencies_page implements renderable, templatable {
                 'context' => $context
             ]);
 
+            // User learning plans.
+            $plans = api::list_plans_with_competency($USER->id, $competency);
+            $exportedplans = array();
+            foreach ($plans as $plan) {
+                $planexporter = new plan_exporter($plan, array('template' => $plan->get_template()));
+                $exportedplans[] = $planexporter->export($output);
+            }
+
             $onerow = array(
                 'competency' => $compexporter->export($output),
                 'coursecompetency' => $ccexporter->export($output),
                 'ruleoutcomeoptions' => $ccoutcomeoptions,
                 'coursemodules' => $exportedmodules,
-                'comppath' => $pathexporter->export($output)
+                'comppath' => $pathexporter->export($output),
+                'plans' => $exportedplans
             );
             if ($gradable) {
                 $foundusercompetencycourse = false;
index eaa0f43..6d89b20 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template tool_lp/course_competencies_page
+
     Course competencies template.
 
     For a full list of the context for this template see the course_competencies_page renderable.
+
+    This template includes ajax functionality, so it cannot be shown in the template library.
 }}
 <div data-region="coursecompetenciespage">
     <div data-region="actions" class="clearfix">
@@ -66,7 +70,7 @@
         <div class="clearfix"></div>
         {{/canmanagecoursecompetencies}}
         {{#competency}}
-            <a href="{{pluginbaseurl}}user_competency_in_course.php?courseid={{courseid}}&competencyid={{competency.id}}&userid={{gradableuserid}}"
+            <a href="{{pluginbaseurl}}/user_competency_in_course.php?courseid={{courseid}}&competencyid={{competency.id}}&userid={{gradableuserid}}"
                    id="competency-info-link-{{competency.id}}"
                    title="{{#str}}viewdetails, tool_lp{{/str}}">
                 <p><strong>{{{competency.shortname}}} <em>{{competency.idnumber}}</em></strong></p>
         {{/canmanagecoursecompetencies}}
         <div data-region="coursecompetencyactivities">
         <p>
-        <ul class="inline list-inline">
+        <strong>{{#str}}activities{{/str}}</strong>
+        <ul class="inline list-inline p-2">
         {{#coursemodules}}
             <li class="list-inline-item"><a href="{{url}}"><img src="{{iconurl}}"> {{name}} </a></li>
         {{/coursemodules}}
         {{^coursemodules}}
-            <li class="list-inline-item"><span class="alert">{{#str}}noactivities, tool_lp{{/str}}</span></li>
+            <li class="list-inline-item">{{#str}}noactivities, tool_lp{{/str}}</li>
         {{/coursemodules}}
         </ul>
         </p>
         </div>
+        <div data-region="learningplans">
+        <p>
+        <strong>{{#str}}userplans, core_competency{{/str}}</strong>
+        <ul class="inline list-inline p-2">
+        {{#plans}}
+            <li class="list-inline-item"><a href="{{pluginbaseurl}}/plan.php?id={{id}}">{{{name}}}</a></li>
+        {{/plans}}
+        {{^plans}}
+            <li class="list-inline-item">{{#str}}nouserplanswithcompetency, core_competency{{/str}}</li>
+        {{/plans}}
+        </ul>
+        </p>
+        </div>
     </td>
     </tr>
 {{/competencies}}
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 22cf96d..ce80bbb 100644 (file)
         </dd>
         {{/user}}
         {{/displayuser}}
+        <dt>{{#str}}userplans, competency{{/str}}</dt>
+        <dd>
+        <p>
+        <ul class="inline list-inline">
+        {{#plans}}
+            <li class="list-inline-item"><a href="{{pluginbaseurl}}/plan.php?id={{id}}">{{{name}}}</a></li>
+        {{/plans}}
+        {{^plans}}
+            <li>{{#str}}nouserplanswithcompetency, competency{{/str}}</li>
+        {{/plans}}
+        </ul>
+        </p>
+        </dd>
         {{#usercompetencycourse}}
         <dt>{{#str}}proficient, tool_lp{{/str}}</dt>
         <dd>
index d2ca869..0fbbfd1 100644 (file)
@@ -130,3 +130,35 @@ Feature: Manage plearning plan
     When I click on "Delete" "button" in the "Confirm" "dialogue"
     And I wait until the page is ready
     Then I should not see "Science plan Year-4"
+
+  Scenario: See a learning plan from a course
+    Given the following lp "plans" exist:
+      | name | user | description |
+      | Science plan Year-manage | admin | science plan description |
+    And the following lp "frameworks" exist:
+      | shortname | idnumber |
+      | Framework 1 | sc-y-2 |
+    And the following lp "competencies" exist:
+      | shortname | framework |
+      | comp1 | sc-y-2 |
+      | comp2 | sc-y-2 |
+    And I follow "Learning plans"
+    And I should see "Science plan Year-manage"
+    And I follow "Science plan Year-manage"
+    And I should see "Add competency"
+    And I press "Add competency"
+    And "Competency picker" "dialogue" should be visible
+    And I select "comp1" of the competency tree
+    When I click on "Add" "button" in the "Competency picker" "dialogue"
+    Then "comp1" "table_row" should exist
+    And I create a course with:
+      | Course full name | New course fullname |
+      | Course short name | New course shortname |
+    And I follow "New course fullname"
+    And I follow "Competencies"
+    And I press "Add competencies to course"
+    And "Competency picker" "dialogue" should be visible
+    And I select "comp1" of the competency tree
+    And I click on "Add" "button" in the "Competency picker" "dialogue"
+    And I should see "Learning plans"
+    And I should see "Science plan Year-manage"
index 71cf2bb..bd49699 100644 (file)
@@ -356,7 +356,7 @@ class api {
         $mobileplugins = self::get_plugins_supporting_mobile();
         foreach ($mobileplugins as $plugin) {
             $displayname = core_plugin_manager::instance()->plugin_name($plugin['component']) . " - " . $plugin['addon'];
-            $remoteaddonslist['remoteAddOn_' . $plugin['component'] . '_' . $plugin['addon']] = $displayname;
+            $remoteaddonslist['sitePlugin_' . $plugin['component'] . '_' . $plugin['addon']] = $displayname;
 
         }
 
diff --git a/admin/tool/mobile/db/upgrade.php b/admin/tool/mobile/db/upgrade.php
new file mode 100644 (file)
index 0000000..73748aa
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Mobile app support.
+ *
+ * @package    tool_mobile
+ * @copyright  2019 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/lib/upgradelib.php');
+
+/**
+ * Upgrade the plugin.
+ *
+ * @param int $oldversion
+ * @return bool always true
+ */
+function xmldb_tool_mobile_upgrade($oldversion) {
+    global $CFG;
+
+    if ($oldversion < 2019021100) {
+        $disabledfeatures = get_config('tool_mobile', 'disabledfeatures');
+        $disabledfeatures = str_replace('remoteAddOn_', 'sitePlugin_', $disabledfeatures);
+        set_config('disabledfeatures', $disabledfeatures, 'tool_mobile');
+        upgrade_plugin_savepoint(true, 2019021100, 'tool', 'mobile');
+    }
+
+    return true;
+}
index 4584e2d..bc7dc20 100644 (file)
@@ -23,7 +23,7 @@
  */
 
 defined('MOODLE_INTERNAL') || die();
-$plugin->version   = 2018120300; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2019021100; // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2018112800; // Requires this Moodle version.
 $plugin->component = 'tool_mobile'; // Full name of the plugin (used for diagnostics).
 $plugin->dependencies = array(
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>
diff --git a/admin/tool/xmldb/actions/add_persistent_mandatory/add_persistent_mandatory.class.php b/admin/tool/xmldb/actions/add_persistent_mandatory/add_persistent_mandatory.class.php
new file mode 100644 (file)
index 0000000..a323c1e
--- /dev/null
@@ -0,0 +1,154 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * @package    tool_xmldb
+ * @copyright  2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Add the mandatory fields for persistent to the table.
+ *
+ * @package    tool_xmldb
+ * @copyright  2019 Michael Aherne
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class add_persistent_mandatory extends XMLDBAction {
+
+    function init() {
+
+        parent::init();
+
+        // Get needed strings.
+        $this->loadStrings(array(
+            'addpersistent' => 'tool_xmldb',
+            'persistentfieldsconfirm' => 'tool_xmldb',
+            'persistentfieldscomplete' => 'tool_xmldb',
+            'persistentfieldsexist' => 'tool_xmldb',
+            'back' => 'core'
+        ));
+
+    }
+
+    function getTitle() {
+        return $this->str['addpersistent'];
+    }
+
+    function invoke() {
+
+        parent::invoke();
+
+        $this->does_generate = ACTION_GENERATE_HTML;
+
+        global $CFG, $XMLDB, $OUTPUT;
+
+        $dir = required_param('dir', PARAM_PATH);
+        $dirpath = $CFG->dirroot . $dir;
+
+        if (empty($XMLDB->dbdirs)) {
+            return false;
+        }
+
+        if (!empty($XMLDB->editeddirs)) {
+            $editeddir = $XMLDB->editeddirs[$dirpath];
+            $structure = $editeddir->xml_file->getStructure();
+        }
+
+        $tableparam = required_param('table', PARAM_ALPHANUMEXT);
+
+        /** @var xmldb_table $table */
+        $table = $structure->getTable($tableparam);
+
+        $result = true;
+        // Launch postaction if exists (leave this here!)
+        if ($this->getPostAction() && $result) {
+            return $this->launch($this->getPostAction());
+        }
+
+        $confirm = optional_param('confirm', false, PARAM_BOOL);
+
+        $fields = ['usermodified', 'timecreated', 'timemodified'];
+        $existing = [];
+        foreach ($fields as $field) {
+            if ($table->getField($field)) {
+                $existing[] = $field;
+            }
+        }
+
+        $returnurl = new \moodle_url('/admin/tool/xmldb/index.php', [
+            'table' => $tableparam,
+            'dir' => $dir,
+            'action' => 'edit_table'
+        ]);
+
+        $backbutton = html_writer::link($returnurl, '[' . $this->str['back'] . ']');
+        $actionbuttons = html_writer::tag('p', $backbutton, ['class' => 'centerpara buttons']);
+
+        if (!$confirm) {
+
+            if (!empty($existing)) {
+
+                $message = html_writer::span($this->str['persistentfieldsexist']);
+                $message .= html_writer::alist($existing);
+                $this->output .= $OUTPUT->notification($message);
+
+                if (count($existing) == count($fields)) {
+                    $this->output .= $actionbuttons;
+                    return true;
+                }
+            }
+
+            $confirmurl = new \moodle_url('/admin/tool/xmldb/index.php', [
+                'table' => $tableparam,
+                'dir' => $dir,
+                'action' => 'add_persistent_mandatory',
+                'sesskey' => sesskey(),
+                'confirm' => '1'
+            ]);
+
+            $message = html_writer::span($this->str['persistentfieldsconfirm']);
+            $message .= html_writer::alist(array_diff($fields, $existing));
+            $this->output .= $OUTPUT->confirm($message, $confirmurl, $returnurl);
+
+        } else {
+
+            $fieldsadded = [];
+            foreach ($fields as $field) {
+                if (!in_array($field, $existing)) {
+                    $fieldsadded[] = $field;
+                    $table->add_field($field, XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, 0);
+                }
+            }
+
+            if (!$table->getKey('usermodified')) {
+                $table->add_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
+            }
+
+            $structure->setVersion(userdate(time(), '%Y%m%d', 99, false));
+            $structure->setChanged(true);
+
+            $message = html_writer::span($this->str['persistentfieldscomplete']);
+            $message .= html_writer::alist(array_diff($fields, $existing));
+            $this->output .= $OUTPUT->notification($message, 'success');
+
+            $this->output .= $actionbuttons;
+        }
+
+        return $result;
+    }
+
+}
index 5cd4bcd..aa82f99 100644 (file)
@@ -44,6 +44,7 @@ class edit_table extends XMLDBAction {
 
         // Get needed strings
         $this->loadStrings(array(
+            'addpersistent' => 'tool_xmldb',
             'change' => 'tool_xmldb',
             'vieworiginal' => 'tool_xmldb',
             'viewedited' => 'tool_xmldb',
@@ -177,6 +178,15 @@ class edit_table extends XMLDBAction {
         $b .= '<a href="index.php?action=view_table_sql&amp;table=' . $tableparam . '&amp;dir=' . urlencode(str_replace($CFG->dirroot, '', $dirpath)) . '">[' .$this->str['viewsqlcode'] . ']</a>';
         // The view php code button
         $b .= '&nbsp;<a href="index.php?action=view_table_php&amp;table=' . $tableparam . '&amp;dir=' . urlencode(str_replace($CFG->dirroot, '', $dirpath)) . '">[' . $this->str['viewphpcode'] . ']</a>';
+        // The add persistent fields button.
+        $url = new \moodle_url('/admin/tool/xmldb/index.php', [
+            'action' => 'add_persistent_mandatory',
+            'sesskey' => sesskey(),
+            'table' => $tableparam,
+            'dir'=> str_replace($CFG->dirroot, '', $dirpath)
+        ]);
+        $b .= '&nbsp;' . \html_writer::link($url, '[' . $this->str['addpersistent'] . ']');
+
         // The save button (if possible)
         if ($cansavenow) {
             $b .= '&nbsp;<a href="index.php?action=save_xml_file&amp;sesskey=' . sesskey() . '&amp;dir=' . urlencode(str_replace($CFG->dirroot, '', $dirpath)) . '&amp;time=' . time() . '&amp;unload=false&amp;postaction=edit_table&amp;table=' . $tableparam . '&amp;dir=' . urlencode(str_replace($CFG->dirroot, '', $dirpath)) . '">[' . $this->str['save'] . ']</a>';
index d531a86..d2efeab 100644 (file)
@@ -23,6 +23,7 @@
  */
 
 $string['actual'] = 'Actual';
+$string['addpersistent'] = 'Add mandatory persistent fields';
 $string['aftertable'] = 'After table:';
 $string['back'] = 'Back';
 $string['backtomainview'] = 'Back to main';
@@ -169,6 +170,9 @@ $string['numberincorrectwholepart'] = 'Too big whole number part for number fiel
 $string['pendingchanges'] = 'Note: You have performed changes to this file. They can be saved at any moment.';
 $string['pendingchangescannotbesaved'] = 'There are changes in this file but they cannot be saved! Please verify that both the directory and the "install.xml" within it have write permissions for the web server.';
 $string['pendingchangescannotbesavedreload'] = 'There are changes in this file but they cannot be saved! Please verify that both the directory and the "install.xml" within it have write permissions for the web server. Then reload this page and you should be able to save those changes.';
+$string['persistentfieldsconfirm'] = 'Do you want to add the following fields: ';
+$string['persistentfieldscomplete'] = 'The following fields have been added: ';
+$string['persistentfieldsexist'] = 'The following fields already exist: ';
 $string['pluginname'] = 'XMLDB editor';
 $string['primarykeyonlyallownotnullfields'] = 'Primary keys cannot be null';
 $string['reserved'] = 'Reserved';
index af95120..ceea63a 100644 (file)
@@ -82,6 +82,7 @@ abstract class calculable {
      * @return string
      */
     public function get_id() {
+        // Using get_class as get_component_classes_in_namespace returns double escaped fully qualified class names.
         return '\\' . get_class($this);
     }
 
index ddc70e0..1c10cde 100644 (file)
@@ -45,6 +45,11 @@ class manager {
      */
     protected static $predictionprocessors = null;
 
+    /**
+     * @var \core_analytics\local\target\base[]
+     */
+    protected static $alltargets = null;
+
     /**
      * @var \core_analytics\local\indicator\base[]
      */
@@ -281,6 +286,28 @@ class manager {
         return new $fullclassname();
     }
 
+    /**
+     * Return all targets in the system.
+     *
+     * @return \core_analytics\local\target\base[]
+     */
+    public static function get_all_targets() : array {
+        if (self::$alltargets !== null) {
+            return self::$alltargets;
+        }
+
+        $classes = self::get_analytics_classes('target');
+
+        self::$alltargets = [];
+        foreach ($classes as $fullclassname => $classpath) {
+            $instance = self::get_target($fullclassname);
+            if ($instance) {
+                self::$alltargets[$instance->get_id()] = $instance;
+            }
+        }
+
+        return self::$alltargets;
+    }
     /**
      * Return all system indicators.
      *
@@ -297,7 +324,6 @@ class manager {
         foreach ($classes as $fullclassname => $classpath) {
             $instance = self::get_indicator($fullclassname);
             if ($instance) {
-                // Using get_class as get_component_classes_in_namespace returns double escaped fully qualified class names.
                 self::$allindicators[$instance->get_id()] = $instance;
             }
         }
index fa6e91e..3b6c6e4 100644 (file)
@@ -338,11 +338,12 @@ class model {
      *
      * @param \core_analytics\local\target\base $target
      * @param \core_analytics\local\indicator\base[] $indicators
-     * @param string $timesplittingid The time splitting method id (its fully qualified class name)
+     * @param string|false $timesplittingid The time splitting method id (its fully qualified class name)
+     * @param string|null $processor The machine learning backend this model will use.
      * @return \core_analytics\model
      */
     public static function create(\core_analytics\local\target\base $target, array $indicators,
-                                  $timesplittingid = false, $processor = false) {
+                                  $timesplittingid = false, $processor = null) {
         global $USER, $DB;
 
         \core_analytics\manager::check_can_manage_models();
@@ -360,8 +361,8 @@ class model {
         $modelobj->usermodified = $USER->id;
 
         if ($processor &&
-                !self::is_valid($processor, '\core_analytics\classifier') &&
-                !self::is_valid($processor, '\core_analytics\regressor')) {
+                !manager::is_valid($processor, '\core_analytics\classifier') &&
+                !manager::is_valid($processor, '\core_analytics\regressor')) {
             throw new \coding_exception('The provided predictions processor \\' . $processor . '\processor is not valid');
         } else {
             $modelobj->predictionsprocessor = $processor;
@@ -462,6 +463,7 @@ class model {
 
             // It needs to be reset as the version changes.
             $this->uniqueid = null;
+            $this->indicators = null;
 
             // We update the version of the model so different time splittings are not mixed up.
             $this->model->version = $now;
@@ -1308,7 +1310,7 @@ class model {
      * @param bool $onlymodelid Preference over $subdirs
      * @return string
      */
-    protected function get_output_dir($subdirs = array(), $onlymodelid = false) {
+    public function get_output_dir($subdirs = array(), $onlymodelid = false) {
         global $CFG;
 
         $subdirstr = '';
@@ -1357,7 +1359,7 @@ class model {
     }
 
     /**
-     * Exports the model data.
+     * Exports the model data for displaying it in a template.
      *
      * @return \stdClass
      */
@@ -1379,6 +1381,58 @@ class model {
         return $data;
     }
 
+    /**
+     * Exports the model data to a zip file.
+     *
+     * @param string $zipfilename
+     * @return string Zip file path
+     */
+    public function export_model(string $zipfilename) : string {
+
+        \core_analytics\manager::check_can_manage_models();
+
+        $modelconfig = new model_config($this);
+        return $modelconfig->export($zipfilename);
+    }
+
+    /**
+     * Imports the provided model.
+     *
+     * Note that this method assumes that model_config::check_dependencies has already been called.
+     *
+     * @throws \moodle_exception
+     * @param  string $zipfilepath Zip file path
+     * @return \core_analytics\model
+     */
+    public static function import_model(string $zipfilepath) : \core_analytics\model {
+
+        \core_analytics\manager::check_can_manage_models();
+
+        $modelconfig = new \core_analytics\model_config();
+        return $modelconfig->import($zipfilepath);
+    }
+
+    /**
+     * Can this model be exported?
+     *
+     * @return bool
+     */
+    public function can_export_configuration() : bool {
+
+        if (empty($this->model->timesplitting)) {
+            return false;
+        }
+        if (!$this->get_indicators()) {
+            return false;
+        }
+
+        if ($this->is_static()) {
+            return false;
+        }
+
+        return true;
+    }
+
     /**
      * Returns the model logs data.
      *
diff --git a/analytics/classes/model_config.php b/analytics/classes/model_config.php
new file mode 100644 (file)
index 0000000..2cb32cc
--- /dev/null
@@ -0,0 +1,326 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Model configuration manager.
+ *
+ * @package   core_analytics
+ * @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Model configuration manager.
+ *
+ * @package   core_analytics
+ * @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class model_config {
+
+    /**
+     * @var \core_analytics\model
+     */
+    private $model = null;
+
+    /**
+     * The name of the file where config is held.
+     */
+    const CONFIG_FILE_NAME = 'model-config.json';
+
+    /**
+     * Constructor.
+     *
+     * @param \core_analytics\model|null $model
+     */
+    public function __construct(?model $model = null) {
+        $this->model = $model;
+    }
+
+    /**
+     * Exports a model to a zip using the provided file name.
+     *
+     * @param string $zipfilename
+     * @return string
+     */
+    public function export(string $zipfilename) : string {
+
+        if (!$this->model) {
+            throw new \coding_exception('No model object provided.');
+        }
+
+        if (!$this->model->can_export_configuration()) {
+            throw new \moodle_exception('errornoexportconfigrequirements', 'analytics');
+        }
+
+        $zip = new \zip_packer();
+        $zipfiles = [];
+
+        // Model config in JSON.
+        $modeldata = $this->export_model_data();
+
+        $exporttmpdir = make_request_directory('analyticsexport');
+        $jsonfilepath = $exporttmpdir . DIRECTORY_SEPARATOR . 'model-config.json';
+        if (!file_put_contents($jsonfilepath, json_encode($modeldata))) {
+            print_error('errornoexportconfig', 'analytics');
+        }
+        $zipfiles[self::CONFIG_FILE_NAME] = $jsonfilepath;
+
+        // ML backend.
+        if ($this->model->is_trained()) {
+            $processor = $this->model->get_predictions_processor(true);
+            $outputdir = $this->model->get_output_dir(array('execution'));
+            $mlbackenddir = $processor->export($this->model->get_unique_id(), $outputdir);
+            $mlbackendfiles = get_directory_list($mlbackenddir);
+            foreach ($mlbackendfiles as $mlbackendfile) {
+                $fullpath = $mlbackenddir . DIRECTORY_SEPARATOR . $mlbackendfile;
+                // Place the ML backend files inside a mlbackend/ dir.
+                $zipfiles['mlbackend/' . $mlbackendfile] = $fullpath;
+            }
+        }
+
+        $zipfilepath = $exporttmpdir . DIRECTORY_SEPARATOR . $zipfilename;
+        $zip->archive_to_pathname($zipfiles, $zipfilepath);
+
+        return $zipfilepath;
+    }
+
+    /**
+     * Imports the provided model configuration into a new model.
+     *
+     * Note that this method assumes that self::check_dependencies has already been called.
+     *
+     * @param  string $zipfilepath Path to the zip file to import
+     * @return \core_analytics\model
+     */
+    public function import(string $zipfilepath) : \core_analytics\model {
+
+        list($modeldata, $mlbackenddir) = $this->extract_import_contents($zipfilepath);
+
+        $target = \core_analytics\manager::get_target($modeldata->target);
+        $indicators = [];
+        foreach ($modeldata->indicators as $indicatorclass) {
+            $indicator = \core_analytics\manager::get_indicator($indicatorclass);
+            $indicators[$indicator->get_id()] = $indicator;
+        }
+        $model = \core_analytics\model::create($target, $indicators, $modeldata->timesplitting, $modeldata->processor);
+
+        // Import them disabled.
+        $model->update(false, false, false, false);
+
+        if ($mlbackenddir) {
+            $modeldir = $model->get_output_dir(['execution']);
+            if (!$model->get_predictions_processor(true)->import($model->get_unique_id(), $modeldir, $mlbackenddir)) {
+                throw new \moodle_exception('errorimport', 'analytics');
+            }
+            $model->mark_as_trained();
+        }
+
+        return $model;
+    }
+
+    /**
+     * Check that the provided model configuration can be deployed in this site.
+     *
+     * @param  \stdClass $modeldata
+     * @param  bool $ignoreversionmismatches
+     * @return string|null Error string or null if all good.
+     */
+    public function check_dependencies(\stdClass $modeldata, bool $ignoreversionmismatches) : ?string {
+
+        $siteversions = \core_component::get_all_versions();
+
+        // Possible issues.
+        $missingcomponents = [];
+        $versionmismatches = [];
+        $missingclasses = [];
+
+        // We first check that this site has the required dependencies and the required versions.
+        foreach ($modeldata->dependencies as $component => $importversion) {
+
+            if (empty($siteversions[$component])) {
+
+                if ($component === 'core') {
+                    $component = 'Moodle';
+                }
+                $missingcomponents[$component] = $component . ' (' . $importversion . ')';
+                continue;
+            }
+
+            if ($siteversions[$component] == $importversion) {
+                // All good here.
+                continue;
+            }
+
+            if (!$ignoreversionmismatches) {
+                if ($component === 'core') {
+                    $component = 'Moodle';
+                }
+                $versionmismatches[$component] = $component . ' (' . $importversion . ')';
+            }
+        }
+
+        // Checking that each of the components is available.
+        if (!$target = manager::get_target($modeldata->target)) {
+            $missingclasses[] = $modeldata->target;
+        }
+
+        if (!$timesplitting = manager::get_time_splitting($modeldata->timesplitting)) {
+            $missingclasses[] = $modeldata->timesplitting;
+        }
+
+        // Indicators.
+        foreach ($modeldata->indicators as $indicatorclass) {
+            if (!$indicator = manager::get_indicator($indicatorclass)) {
+                $missingclasses[] = $indicatorclass;
+            }
+        }
+
+        // ML backend.
+        if (!empty($modeldata->processor)) {
+            if (!$processor = \core_analytics\manager::get_predictions_processor($modeldata->processor, false)) {
+                $missingclasses[] = $indicatorclass;
+            }
+        }
+
+        if (!empty($missingcomponents)) {
+            return get_string('errorimportmissingcomponents', 'analytics', join(', ', $missingcomponents));
+        }
+
+        if (!empty($versionmismatches)) {
+            return get_string('errorimportversionmismatches', 'analytics', implode(', ', $versionmismatches));
+        }
+
+        if (!empty($missingclasses)) {
+            $a = (object)[
+                'missingclasses' => implode(', ', $missingclasses),
+            ];
+            return get_string('errorimportmissingclasses', 'analytics', $a);
+        }
+
+        // No issues found.
+        return null;
+    }
+
+    /**
+     * Returns the component the class belongs to.
+     *
+     * Note that this method does not work for global space classes.
+     *
+     * @param  string $fullclassname Qualified name including the namespace.
+     * @return string|null Frankenstyle component
+     */
+    public static function get_class_component(string $fullclassname) : ?string {
+
+        // Strip out leading backslash.
+        $fullclassname = ltrim($fullclassname, '\\');
+
+        $nextbackslash = strpos($fullclassname, '\\');
+        if ($nextbackslash === false) {
+            // Global space.
+            return 'core';
+        }
+        $component = substr($fullclassname, 0, $nextbackslash);
+
+        // All core subsystems use core's version.php.
+        if (strpos($component, 'core_') === 0) {
+            $component = 'core';
+        }
+
+        return $component;
+    }
+
+    /**
+     * Extracts the import zip contents.
+     *
+     * @param  string $zipfilepath Zip file path
+     * @return array [0] => \stdClass, [1] => string
+     */
+    public function extract_import_contents(string $zipfilepath) : array {
+
+        $importtempdir = make_request_directory('analyticsimport' . microtime(false));
+
+        $zip = new \zip_packer();
+        $filelist = $zip->extract_to_pathname($zipfilepath, $importtempdir);
+
+        if (empty($filelist[self::CONFIG_FILE_NAME])) {
+            // Missing required file.
+            throw new \moodle_exception('errorimport', 'analytics');
+        }
+
+        $jsonmodeldata = file_get_contents($importtempdir . DIRECTORY_SEPARATOR . self::CONFIG_FILE_NAME);
+
+        if (!$modeldata = json_decode($jsonmodeldata)) {
+            throw new \moodle_exception('errorimport', 'analytics');
+        }
+
+        if (empty($modeldata->target) || empty($modeldata->timesplitting) || empty($modeldata->indicators)) {
+            throw new \moodle_exception('errorimport', 'analytics');
+        }
+
+        $mlbackenddir = $importtempdir . DIRECTORY_SEPARATOR . 'mlbackend';
+        if (!is_dir($mlbackenddir)) {
+            $mlbackenddir = false;
+        }
+
+        return [$modeldata, $mlbackenddir];
+    }
+    /**
+     * Exports the configuration of the model.
+     * @return \stdClass
+     */
+    protected function export_model_data() : \stdClass {
+
+        $versions = \core_component::get_all_versions();
+
+        $data = new \stdClass();
+
+        // Target.
+        $data->target = $this->model->get_target()->get_id();
+        $requiredclasses[] = $data->target;
+
+        // Time splitting method.
+        $data->timesplitting = $this->model->get_time_splitting()->get_id();
+        $requiredclasses[] = $data->timesplitting;
+
+        // Model indicators.
+        $data->indicators = [];
+        foreach ($this->model->get_indicators() as $indicator) {
+            $indicatorid = $indicator->get_id();
+            $data->indicators[] = $indicatorid;
+            $requiredclasses[] = $indicatorid;
+        }
+
+        // Return the predictions processor this model is using, even if no predictions processor
+        // was explicitly selected.
+        $predictionsprocessor = $this->model->get_predictions_processor();
+        $data->processor = '\\' . get_class($predictionsprocessor);
+        $requiredclasses[] = $data->processor;
+
+        // Add information for versioning.
+        $data->dependencies = [];
+        foreach ($requiredclasses as $fullclassname) {
+            $component = $this->get_class_component($fullclassname);
+            $data->dependencies[$component] = $versions[$component];
+        }
+
+        return $data;
+    }
+}
diff --git a/analytics/classes/packable.php b/analytics/classes/packable.php
new file mode 100644 (file)
index 0000000..1d26f65
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Exportable machine learning backend interface.
+ *
+ * @package   core_analytics
+ * @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Exportable machine learning backend interface.
+ *
+ * @package   core_analytics
+ * @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+interface packable {
+
+    /**
+     * Exports the machine learning model.
+     *
+     * @throws \moodle_exception
+     * @param  string $uniqueid  The model unique id
+     * @param  string $modeldir  The directory that contains the trained model.
+     * @return string            The path to the directory that contains the exported model.
+     */
+    public function export(string $uniqueid, string $modeldir) : string;
+
+    /**
+     * Imports the provided machine learning model.
+     *
+     * @param  string $uniqueid The model unique id
+     * @param  string $modeldir  The directory that will contain the trained model.
+     * @param  string $importdir The directory that contains the files to import.
+     * @return bool Success
+     */
+    public function import(string $uniqueid, string $modeldir, string $importdir) : bool;
+}
index 21009ba..19ceb61 100644 (file)
@@ -28,6 +28,7 @@ require_once(__DIR__ . '/fixtures/test_indicator_max.php');
 require_once(__DIR__ . '/fixtures/test_indicator_min.php');
 require_once(__DIR__ . '/fixtures/test_indicator_fullname.php');
 require_once(__DIR__ . '/fixtures/test_target_shortname.php');
+require_once(__DIR__ . '/fixtures/test_static_target_shortname.php');
 require_once(__DIR__ . '/fixtures/test_target_course_level_shortname.php');
 require_once(__DIR__ . '/fixtures/test_analyser.php');
 
@@ -317,6 +318,95 @@ class analytics_model_testcase extends advanced_testcase {
         $this->assertLessThanOrEqual(2, $DB->count_records('analytics_used_analysables', $params));
     }
 
+    /**
+     * Test model_config::get_class_component.
+     */
+    public function test_model_config_get_class_component() {
+        $this->resetAfterTest(true);
+
+        $this->assertEquals('core',
+            \core_analytics\model_config::get_class_component('\\core\\analytics\\indicator\\read_actions'));
+        $this->assertEquals('core',
+            \core_analytics\model_config::get_class_component('core\\analytics\\indicator\\read_actions'));
+        $this->assertEquals('core',
+            \core_analytics\model_config::get_class_component('\\core_course\\analytics\\indicator\\completion_enabled'));
+        $this->assertEquals('mod_forum',
+            \core_analytics\model_config::get_class_component('\\mod_forum\\analytics\\indicator\\cognitive_depth'));
+
+        $this->assertEquals('core', \core_analytics\model_config::get_class_component('\\core_class'));
+    }
+
+    /**
+     * Test that import_model import models' configurations.
+     */
+    public function test_import_model_config() {
+        $this->resetAfterTest(true);
+
+        $this->model->enable('\\core\\analytics\\time_splitting\\quarters');
+        $zipfilepath = $this->model->export_model('yeah-config.zip');
+
+        $this->modelobj = $this->model->get_model_obj();
+
+        $importedmodelobj = \core_analytics\model::import_model($zipfilepath)->get_model_obj();
+
+        $this->assertSame($this->modelobj->target, $importedmodelobj->target);
+        $this->assertSame($this->modelobj->indicators, $importedmodelobj->indicators);
+        $this->assertSame($this->modelobj->timesplitting, $importedmodelobj->timesplitting);
+
+        $predictionsprocessor = $this->model->get_predictions_processor();
+        $this->assertSame('\\' . get_class($predictionsprocessor), $importedmodelobj->predictionsprocessor);
+    }
+
+    /**
+     * Test can export configuration
+     */
+    public function test_can_export_configuration() {
+        $this->resetAfterTest(true);
+
+        // No time splitting method.
+        $this->assertFalse($this->model->can_export_configuration());
+
+        $this->model->enable('\\core\\analytics\\time_splitting\\quarters');
+        $this->assertTrue($this->model->can_export_configuration());
+
+        $this->model->update(true, [], false);
+        $this->assertFalse($this->model->can_export_configuration());
+
+        $statictarget = new test_static_target_shortname();
+        $indicators['test_indicator_max'] = \core_analytics\manager::get_indicator('test_indicator_max');
+        $model = \core_analytics\model::create($statictarget, $indicators, '\\core\\analytics\\time_splitting\\quarters');
+        $this->assertFalse($model->can_export_configuration());
+    }
+
+    /**
+     * Test export_config
+     */
+    public function test_export_config() {
+        $this->resetAfterTest(true);
+
+        $this->model->enable('\\core\\analytics\\time_splitting\\quarters');
+
+        $modelconfig = new \core_analytics\model_config($this->model);
+
+        $method = new ReflectionMethod('\\core_analytics\\model_config', 'export_model_data');
+        $method->setAccessible(true);
+
+        $modeldata = $method->invoke($modelconfig);
+
+        $this->assertArrayHasKey('core', $modeldata->dependencies);
+        $this->assertInternalType('float', $modeldata->dependencies['core']);
+        $this->assertNotEmpty($modeldata->target);
+        $this->assertNotEmpty($modeldata->timesplitting);
+        $this->assertCount(3, $modeldata->indicators);
+
+        $indicators['test_indicator_max'] = \core_analytics\manager::get_indicator('test_indicator_max');
+        $this->model->update(true, $indicators, false);
+
+        $modeldata = $method->invoke($modelconfig);
+
+        $this->assertCount(1, $modeldata->indicators);
+    }
+
     /**
      * Generates a model log record.
      */
@@ -346,17 +436,6 @@ class analytics_model_testcase extends advanced_testcase {
  */
 class testable_model extends \core_analytics\model {
 
-    /**
-     * get_output_dir
-     *
-     * @param array $subdirs
-     * @param bool $onlymodelid
-     * @return string
-     */
-    public function get_output_dir($subdirs = array(), $onlymodelid = false) {
-        return parent::get_output_dir($subdirs, $onlymodelid);
-    }
-
     /**
      * init_analyser
      *
index 7d74868..18582c3 100644 (file)
@@ -119,23 +119,9 @@ class core_analytics_prediction_testcase extends advanced_testcase {
         $this->setAdminuser();
         set_config('enabled_stores', 'logstore_standard', 'tool_log');
 
-        $ncourses = 10;
-
         // Generate training data.
-        $params = array(
-            'startdate' => mktime(0, 0, 0, 10, 24, 2015),
-            'enddate' => mktime(0, 0, 0, 2, 24, 2016),
-        );
-        for ($i = 0; $i < $ncourses; $i++) {
-            $name = 'a' . random_string(10);
-            $courseparams = array('shortname' => $name, 'fullname' => $name) + $params;
-            $this->getDataGenerator()->create_course($courseparams);
-        }
-        for ($i = 0; $i < $ncourses; $i++) {
-            $name = 'b' . random_string(10);
-            $courseparams = array('shortname' => $name, 'fullname' => $name) + $params;
-            $this->getDataGenerator()->create_course($courseparams);
-        }
+        $ncourses = 10;
+        $this->generate_courses($ncourses);
 
         // We repeat the test for all prediction processors.
         $predictionsprocessor = \core_analytics\manager::get_predictions_processor($predictionsprocessorclass, false);
@@ -171,6 +157,10 @@ class core_analytics_prediction_testcase extends advanced_testcase {
         $this->assertEmpty($fs->get_directory_files(\context_system::instance()->id, 'analytics',
             \core_analytics\dataset_manager::UNLABELLED_FILEAREA, $model->get_id(), '/analysable/', true, false));
 
+        $params = [
+            'startdate' => mktime(0, 0, 0, 10, 24, 2015),
+            'enddate' => mktime(0, 0, 0, 2, 24, 2016),
+        ];
         $courseparams = $params + array('shortname' => 'aaaaaa', 'fullname' => 'aaaaaa', 'visible' => 0);
         $course1 = $this->getDataGenerator()->create_course($courseparams);
         $courseparams = $params + array('shortname' => 'bbbbbb', 'fullname' => 'bbbbbb', 'visible' => 0);
@@ -280,6 +270,67 @@ class core_analytics_prediction_testcase extends advanced_testcase {
         return $this->add_prediction_processors($cases);
     }
 
+    /**
+     * test_ml_export_import
+     *
+     * @param string $predictionsprocessorclass The class name
+     * @dataProvider provider_ml_export_import
+     */
+    public function test_ml_export_import($predictionsprocessorclass) {
+
+        $this->resetAfterTest(true);
+        $this->setAdminuser();
+        set_config('enabled_stores', 'logstore_standard', 'tool_log');
+
+        // Generate training data.
+        $ncourses = 10;
+        $this->generate_courses($ncourses);
+
+        // We repeat the test for all prediction processors.
+        $predictionsprocessor = \core_analytics\manager::get_predictions_processor($predictionsprocessorclass, false);
+        if ($predictionsprocessor->is_ready() !== true) {
+            $this->markTestSkipped('Skipping ' . $predictionsprocessorclass . ' as the predictor is not ready.');
+        }
+
+        $model = $this->add_perfect_model();
+        $model->update(true, false, '\core\analytics\time_splitting\quarters', get_class($predictionsprocessor));
+
+        $model->train();
+
+        $this->generate_courses(10, ['visible' => 0]);
+
+        $originalresults = $model->predict();
+
+        $zipfilename = 'model-zip-' . microtime() . '.zip';
+        $zipfilepath = $model->export_model($zipfilename);
+
+        $importmodel = \core_analytics\model::import_model($zipfilepath);
+        $importmodel->enable();
+
+        // Now predict using the imported model without prior training.
+        $importedmodelresults = $importmodel->predict();
+
+        foreach ($originalresults->predictions as $sampleid => $prediction) {
+            $this->assertEquals($importedmodelresults->predictions[$sampleid]->prediction, $prediction->prediction);
+        }
+
+        set_config('enabled_stores', '', 'tool_log');
+        get_log_manager(true);
+    }
+
+    /**
+     * provider_ml_export_import
+     *
+     * @return array
+     */
+    public function provider_ml_export_import() {
+        $cases = [
+            'case' => [],
+        ];
+
+        // We need to test all system prediction processors.
+        return $this->add_prediction_processors($cases);
+    }
     /**
      * Test the system classifiers returns.
      *
@@ -400,20 +451,7 @@ class core_analytics_prediction_testcase extends advanced_testcase {
         }
 
         // Generate training data.
-        $params = array(
-            'startdate' => mktime(0, 0, 0, 10, 24, 2015),
-            'enddate' => mktime(0, 0, 0, 2, 24, 2016),
-        );
-        for ($i = 0; $i < $ncourses; $i++) {
-            $name = 'a' . random_string(10);
-            $params = array('shortname' => $name, 'fullname' => $name) + $params;
-            $this->getDataGenerator()->create_course($params);
-        }
-        for ($i = 0; $i < $ncourses; $i++) {
-            $name = 'b' . random_string(10);
-            $params = array('shortname' => $name, 'fullname' => $name) + $params;
-            $this->getDataGenerator()->create_course($params);
-        }
+        $this->generate_courses($ncourses);
 
         // We repeat the test for all prediction processors.
         $predictionsprocessor = \core_analytics\manager::get_predictions_processor($predictionsprocessorclass, false);
@@ -579,6 +617,32 @@ class core_analytics_prediction_testcase extends advanced_testcase {
         return new \core_analytics\model($model->get_id());
     }
 
+    /**
+     * Generates $ncourses courses
+     *
+     * @param  int $ncourses The number of courses to be generated.
+     * @param  array $params Course params
+     * @return null
+     */
+    protected function generate_courses($ncourses, array $params = []) {
+
+        $params = $params + [
+            'startdate' => mktime(0, 0, 0, 10, 24, 2015),
+            'enddate' => mktime(0, 0, 0, 2, 24, 2016),
+        ];
+
+        for ($i = 0; $i < $ncourses; $i++) {
+            $name = 'a' . random_string(10);
+            $courseparams = array('shortname' => $name, 'fullname' => $name) + $params;
+            $this->getDataGenerator()->create_course($courseparams);
+        }
+        for ($i = 0; $i < $ncourses; $i++) {
+            $name = 'b' . random_string(10);
+            $courseparams = array('shortname' => $name, 'fullname' => $name) + $params;
+            $this->getDataGenerator()->create_course($courseparams);
+        }
+    }
+
     /**
      * add_prediction_processors
      *
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 67d0d03..8247355 100644 (file)
@@ -66,12 +66,16 @@ class block_login extends block_base {
 
             $this->content->text .= "\n".'<form class="loginform" id="login" method="post" action="'.get_login_url().'">';
 
-            $this->content->text .= '<div class="form-group"><label for="login_username">'.$strusername.'</label>';
-            $this->content->text .= '<input type="text" name="username" id="login_username" class="form-control" value="'.s($username).'" /></div>';
+            $this->content->text .= '<div class="form-group">';
+            $this->content->text .= '<label for="login_username">'.$strusername.'</label>';
+            $this->content->text .= '<input type="text" name="username" id="login_username" ';
+            $this->content->text .= ' class="form-control" value="'.s($username).'" autocomplete="username"/></div>';
 
             $this->content->text .= '<div class="form-group"><label for="login_password">'.get_string('password').'</label>';
 
-            $this->content->text .= '<input type="password" name="password" id="login_password" class="form-control" value="" /></div>';
+            $this->content->text .= '<input type="password" name="password" id="login_password" ';
+            $this->content->text .= ' class="form-control" value="" autocomplete="current-password"/>';
+            $this->content->text .= '</div>';
 
             if (isset($CFG->rememberusername) and $CFG->rememberusername == 2) {
                 $checked = $username ? 'checked="checked"' : '';
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 22c49e3..ed51a17 100644 (file)
  */
 
 $string['all'] = 'All';
+$string['allexcepthidden'] = 'All (except hidden)';
 $string['addtofavourites'] = 'Star this course';
 $string['aria:addtofavourites'] = 'Star for';
 $string['aria:allcourses'] = 'All courses';
+$string['aria:allcoursesexcepthidden'] = 'All courses except hidden courses';
 $string['aria:card'] = 'Switch to card view';
 $string['aria:controls'] = 'Course overview controls';
 $string['aria:courseactions'] = 'Actions for current course';
index 687e1a3..45605eb 100644 (file)
@@ -31,7 +31,7 @@
     <button id="groupingdropdown" type="button" class="btn btn-outline-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="{{#str}} aria:groupingdropdown, block_myoverview {{/str}}">
         {{#pix}} i/filter {{/pix}}
         <span class="d-sm-inline-block" data-active-item-text>
-            {{#all}}{{#str}} all, block_myoverview {{/str}}{{/all}}
+            {{#all}}{{#str}} allexcepthidden, block_myoverview {{/str}}{{/all}}
             {{#inprogress}}{{#str}} inprogress, block_myoverview {{/str}}{{/inprogress}}
             {{#future}}{{#str}} future, block_myoverview {{/str}}{{/future}}
             {{#past}}{{#str}} past, block_myoverview {{/str}}{{/past}}
     </button>
     <ul class="dropdown-menu" data-show-active-item data-active-item-text aria-labelledby="groupingdropdown">
         <li>
-            <a class="dropdown-item {{#all}}active{{/all}}" href="#" data-filter="grouping" data-value="all" data-pref="all" aria-label="{{#str}} aria:allcourses, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
-                {{#str}} all, block_myoverview {{/str}}
+            <a class="dropdown-item {{#all}}active{{/all}}" href="#" data-filter="grouping" data-value="all" data-pref="all" aria-label="{{#str}} aria:allcoursesexcepthidden, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
+                {{#str}} allexcepthidden, block_myoverview {{/str}}
             </a>
         </li>
+        <li class="dropdown-divider" role="presentation">
+            <span class="filler">&nbsp;</span>
+        </li>
         <li>
             <a class="dropdown-item {{#inprogress}}active{{/inprogress}}" href="#" data-filter="grouping" data-value="inprogress" data-pref="inprogress" aria-label="{{#str}} aria:inprogress, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
                 {{#str}} inprogress, block_myoverview {{/str}}
                 {{#str}} past, block_myoverview {{/str}}
             </a>
         </li>
+        <li class="dropdown-divider" role="presentation">
+            <span class="filler">&nbsp;</span>
+        </li>
         <li>
             <a class="dropdown-item {{#favourites}}active{{/favourites}}" href="#" data-filter="grouping" data-value="favourites"  data-pref="favourites" aria-label="{{#str}} aria:favourites, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
                 {{#str}} favourites, block_myoverview {{/str}}
             </a>
         </li>
+        <li class="dropdown-divider" role="presentation">
+            <span class="filler">&nbsp;</span>
+        </li>
         <li>
             <a class="dropdown-item {{#hidden}}active{{/hidden}}" href="#" data-filter="grouping" data-value="hidden"  data-pref="hidden" aria-label="{{#str}} aria:hiddencourses, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
                 {{#str}} hiddencourses, block_myoverview {{/str}}
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"]'
+&n